feat!: mcp 2, and the tool bridge off the retired adapter - #165
ciaransweet wants to merge 3 commits into
Conversation
`mcp` 2 renamed or removed most of what the server side was built on, and `langchain-mcp-adapters` — both halves of the LangChain/MCP bridge — was retired on 11 September. They move together: the adapter's successor, `langchain[mcp]`, reaches servers through fastmcp 4, which requires mcp 2. Server side. `FastMCP` is now `MCPServer`, and the host, port, path and statelessness it used to hold are per-call arguments. `ToolsetServer` keeps them on the server object, so `streamable_http_app()` still yields the toolset's own URL shape and stateless transport for a consumer that does its own serving. The LangChain-tool to MCP-tool conversion is ours now — `langchain.mcp` converts in the client direction only. It is some thirty lines, and the half this package already owned (the output schema derived from the return annotation, the argument model that forbids extras) is unchanged. `mcp_runtime.fastmcp_output.to_fastmcp` is `mcp_runtime.mcp_tools.to_mcp_tool`, named for what it builds now that no FastMCP is involved. Credentials. v2 publishes no request contextvar, and hands its per-request context to handlers as an argument a tool body several frames down cannot reach. So the contextvar is ours, filled by a `ServerMiddleware` every built server carries; `credential_from_header` is unchanged for callers. A new test drives it through a real HTTP request, since that is the part with no SDK equivalent left to lean on. Client side. `MultiServerMCPClient` becomes one fastmcp `Client` per connection behind `MCPAdapter`, still adapted per server so each tool records where it came from. The per-user credential factory survives intact — fastmcp's transport takes an `httpx_client_factory` the same way — on httpx2, which is the HTTP stack mcp 2 moved to. `mcp-cli` uses the v2 `Client` and the renamed wire fields (`input_schema`, `is_error`, `structured_content`). Checked against a running server, not only in tests: `mcp-serve-local` boots, `mcp-cli list` and `call` work through the local host's mounted app, and an undeclared argument is still refused by name. One behaviour change worth knowing: a server bound to 127.0.0.1 now validates the Host header (the SDK turns on DNS-rebinding protection for loopback), so a local client must reach it by an address with a port. Deployments bind 0.0.0.0, where the SDK leaves the protection off, and are unaffected. Refs #163 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Filed the follow-up as #166: One thing #166 records that is worth knowing while reviewing this PR: since |
Running both examples end to end turned up three things the suite was green through, all of them the same shape: the new client is the old one's successor in role, not in the details this package reached into. The credential factory is called by fastmcp's transport with an argument the old adapter never passed (`follow_redirects`), so every connection failed at the point of use with a `TypeError` that reads like an unreachable server. It now takes what the transport gives it. The SDK stopped reading that setting off a supplied client in 2.2.0, so dropping it changes no request. A tool's `_meta` is where a server's declarations travel, and `langchain.mcp` keeps it under its own `mcp` namespace rather than flat. Reading only the flat position found nothing: `NotAuthored` stopped narrowing, publications stopped being declared, and views stopped resolving — all silently, because declarations are advisory and an absent one is indistinguishable from a server that declared nothing. `tool_meta` reads both positions and every reader goes through it. `mcp_state.injection` forwarded LangGraph's `runtime` to the tool it wraps. The old adapter declared that parameter and absorbed it; `langchain.mcp`'s tool body is `**arguments` and sends everything it is handed to the server, so the runtime went on the wire and failed to serialise. It is forwarded only to a tool that names it. The tests missed all three because the MCP tool double mirrored the old adapter: it declared `runtime`, and it stamped `_meta` flat. The double now matches what `langchain.mcp` builds, which is what makes the three new tests here worth having. Also: `httpx2` logs every request at INFO under its own name, so both examples silence it as they already silenced `httpx`, and the session-state demo reads declarations through `tool_meta` like everything else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Ran both examples end to end. They did not work, and the three reasons were all the same shape — the new client is the old one's successor in role, not in the details this package reached into. Fixed in 5e5837f, with a test for each.
The suite was green through all three because the MCP tool double mirrored the old adapter — it declared session-state now runs clean: three servers, the full scripted conversation, all seven sections, including the narrowing and both refusals. agui-events needs a provider key, which I do not have, so the model turn is the one thing unverified. Everything up to it does work: five servers up, six tools loaded through |
|
Drove a real model turn against the agui-events example (
The activity snapshots are the part worth reading, because they are the session-state contract working against a real model rather than a script:
And the view surface behind it: Both examples are now verified end to end. Ready for review whenever you want to take it out of draft. |
Two regressions, both in corners the new SDK and client differ from the old
in, and neither exercised by a test until now.
`mcp-serve-local` on any address but loopback answered 421 to every toolset.
The local host built each server for 127.0.0.1 whatever HOST said, and the
SDK turns on DNS-rebinding protection — a check of the Host header against
loopback — for exactly that. The index at `/` still answered 200 above it,
handing out URLs nothing could use. `build_local_app` now takes the host it
is served on and builds every server for it.
`mcp-agent chat` against an unreachable server dumped a traceback. fastmcp
reports a connection that never came up as a bare `RuntimeError` with the
transport's exception as its `__cause__`, so the types `CONNECT_ERRORS`
names never reached the `except*`. `connect_failure` reads the cause, and
the message names the port again — with the `/mcp` hint back beside it.
Smaller: `ToolsetServer.run` took positional arguments and dropped them, so
`run("stdio")` ran streamable HTTP; it now runs the transport it is given
and applies the remembered settings only to streamable HTTP. `httpx2` is
declared where it is imported, as `httpx` already was. CONSUMING's "wire it
into your own agent" said `[state]` for a snippet that imports from
`mcp_agent`, which is `[agent]`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Reviewed the port with fresh eyes and fixed what it turned up, in 2454353. Two regressions, both reproduced before and after:
Smaller: Checked and holding, worth knowing: per-user credential isolation on one fastmcp Each fix has a test that fails on the old behaviour. 572 passed. |
Closes #163.
mcp2 renamed or removed most of what the server side was built on, andlangchain-mcp-adapters— both halves of the LangChain/MCP bridge — was retired on 11 September. They move together: the adapter's successor,langchain[mcp], reaches servers through fastmcp 4, which requires mcp 2. Draft because the API surface is worth a look before it releases as a major.Server side
FastMCPis nowMCPServer, and the host, port, path and statelessness it used to hold are per-call arguments.ToolsetServerkeeps them on the server object, sostreamable_http_app()still yields the toolset's own URL shape and stateless transport for a consumer that does its own serving —dss-runtimecalls exactly that and needs no change.The LangChain-tool to MCP-tool conversion is ours now, since
langchain.mcpconverts in the client direction only. It is some thirty lines, and the half this package already owned — the output schema derived from the return annotation, the argument model that forbids extras — is unchanged.mcp_runtime.fastmcp_output.to_fastmcpis nowmcp_runtime.mcp_tools.to_mcp_tool, named for what it builds now that no FastMCP is involved.Credentials
v2 publishes no request contextvar, and hands its per-request context to handlers as an argument that a tool body several frames down cannot reach. So the contextvar is ours, filled by a
ServerMiddlewareevery built server carries.credential_from_headeris unchanged for callers, and a new test drives it through a real HTTP request — this is the one piece with no SDK equivalent left to lean on.Client side
MultiServerMCPClientbecomes one fastmcpClientper connection behindMCPAdapter, still adapted per server so each tool records where it came from. The per-user credential factory survives intact, since fastmcp's transport takes anhttpx_client_factorythe same way — now on httpx2, the HTTP stack mcp 2 moved to.mcp-cliuses the v2Clientand the renamed wire fields (input_schema,is_error,structured_content).Checks
./scripts/lintclean, 565 passed, 6 skipped.mcp-serve-localboots the example toolset,/and/<toolset>/healthanswer,mcp-cli listandmcp-cli callwork through the mounted app, structured content comes back, and an undeclared argument is still refused by name.Behaviour change worth knowing
A server bound to 127.0.0.1 now validates the
Hostheader — the SDK turns on DNS-rebinding protection for loopback — so a local client must reach it by an address carrying a port. Deployments bind0.0.0.0, where the SDK leaves the protection off, so they are unaffected.Two more from the SDK, neither requiring a change here: an unexpected exception from a tool now reaches the client as
Error executing tool <name>with no detail (a returnedToolErroris unaffected), and idle-session expiry does not apply to these servers because they are stateless.Not in this PR
The extension APIs v2 added — MCP Apps as a first-class extension, and
Resolve/elicitation, which overlapviews.pyandmcp_agent.interrupt_gaterespectively — are ported as they stand, not rebuilt on the new primitives. Worth their own issue once this lands.Downstream is pin-only: neither
mcp-toolsetsnordss-agentic-ai-servicesimportsmcpdirectly.🤖 Generated with Claude Code