Repository navigation
fix(protocol): bind the ToolContext on fastmcp apps through their middleware; refuse only a declared reader without a seam - #162
Merged
Conversation
…dleware; refuse only a declared reader without a seam 0.1.324 refused to mount any app without mcp FastMCP's _tool_manager, which is every environment built on the standalone fastmcp package (plugins mount through AgentEnvEnvironment.mount(fastmcp.FastMCP(...))); they had to pin the protocol below 0.1.324. The check conflated "no mcp seam" with "cannot bind". Now _check_tool_binding decides per app: mcp's FastMCP keeps the call_tool wrap; a fastmcp app gets a Middleware inserted first in its chain (first runs outermost, so a handler's own middleware and the tool run under the binding), whose on_call_tool binds the ToolContext, runs on_tool_call and maps its CallToolResult onto fastmcp's ToolResult; an app with neither seam is refused only when a tool declares a ToolContext parameter or the handler overrides on_tool_call, and otherwise mounts unbound with one warning. The binding itself stays unconditional wherever a seam exists: tools and guards read ToolContext.current() without declaring anything. fastmcp>=3.4,<4 joins the dev extras so the new tests run against the real package: _meta roles to sync and async tools, the header role over HTTP with its own session, hook refusal, 24 concurrent callers, one handler per app, and the no-seam rule. The one test that asserted the old refusal wording now asserts the new one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
… import fastmcp before any route Folded from the chaos pass on the previous commit. mcp 2 hands a request's _meta over as a plain dict rather than a model with model_extra, so on fastmcp 4 the mount bound every call but read no role; _meta_entries now takes either shape. A middleware list only counts as fastmcp's when add_middleware is there too, so a foreign object carrying a list of its own is treated as having no seam, and that branch is checked before the tool manager because fastmcp 2.x keeps a manager whose dispatch has another shape. The fastmcp import moves into the check, so a missing fastmcp fails before any route or tool has been added. The no-seam warning names the module-qualified app type, the environment and a remedy; the ToolResult conversion reads mcp 2's spellings first so it never trips the deprecation shims; on_tool_call's docstring says that on fastmcp a non-error answer passes through output validation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Protocol 0.1.324 made every mount refuse an app without mcp FastMCP's
_tool_manager. That is every environment built on the standalonefastmcppackage (a plugin mounts throughAgentEnvEnvironment.mount(fastmcp.FastMCP(..., middleware=[...]))), so those plugins pinned the protocol below 0.1.324. The check conflated "no mcp seam" with "cannot bind".What changes.
_check_tool_bindingnow decides per app, before any route or tool is added:mcp>=1.25,<2)_tool_manager.call_toolwrap, unchangedmiddlewarelist)Middlewareinserted first in the chain; first runs outermost, so a handler's own middleware and the tool both run under the binding. Itson_call_toolbinds the ToolContext, runson_tool_call, and maps a returnedCallToolResultonto fastmcp'sToolResultwith the error flag keptToolContextparameter or the handler overrideson_tool_call; otherwise mounts unbound with one warning naming the app typeThe binding stays unconditional wherever a seam exists. That is the contract tools and dispatch guards rely on when they read
ToolContext.current()without declaring anything; gating it on declared parameters would have silently removed the caller from every such reader.Tests.
fastmcp>=3.4,<4joins the dev extras (root, which CI installs for the protocol tests, and the package's own), so the new module runs against the real package:_metaroles to a sync tool with an injected parameter and an async tool readingToolContext.current(), the handler's own middleware seeing the bound role, the header role over streamable HTTP with its own session, hook refusal reaching the client with the tool never run, 24 concurrent callers, one handler per app, and the no-seam rule both ways. The one existing test that asserted the old refusal wording asserts the new one. 433 protocol tests pass locally with fastmcp 3.4.8 installed; the module skips when fastmcp is absent.Not changed. The mcp FastMCP path is the same wrap as before, so environments on it (every synthetic server, the gateway) behave identically.
Chaos pass (nine agents, each a different way of instrumenting an environment, run against this branch; findings folded into the second commit).
create_app, a caller-built app with its own lifespan, directAgentEnvFastMCPApplication, 200 concurrent in-memory calls across eight sessions and four HTTP sessions with ten concurrent calls each, hook refusal, hook raising, hook returning garbage, tool raising, unknown tool, Starlette for a tool-less handler, extension routes: 15/15, and verdict-identical to the released 0.1.324 wheel run on the same file.tst/uniton this protocol: green._meta: 15/15. Baselines reproduced: 0.1.321 mounts with no caller at all, 0.1.324 refuses the mount._metaroles were dropped because mcp 2 hands_metaover as a dict. Fixed in the second commit and rechecked.create_fastmcp_appon mcp 2 is pre-existing (mcp 2 ships no bundled FastMCP).middlewarelist is a seam only withadd_middlewarebeside it; fastmcp is checked before the tool manager because fastmcp 2.x keeps a manager of another shape; fastmcp is imported in the check, before any route; the no-seam warning names the module-qualified app type, the environment and a remedy; the result conversion reads mcp 2's spellings first; theon_tool_calldocstring states that on fastmcp a non-error answer passes through output validation. Noted and left: a middleware that replays a cachedcall_nextfrom another call's task runs it under that task's caller (contextvar semantics), and the card schema advertises a fastmcpContextparameter (pre-existing).Rollout. Carries
bump-version; plugins drop their<0.1.324pin once the release is out.🤖 Generated with Claude Code


Confidence Score: 5/5
The reviewed changes appear safe to merge; no blocking issue was found.Summary
This PR adds
ToolContextbinding for standalone fastmcp apps while keeping mcp's existing dispatch wrapper.No actionable new issues were found in the changes since the last reviewed commit. No previous review threads were supplied, and GitHub access was unavailable. Tests were inspected, not run.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A["Mount environment"] --> B{"middleware list and add_middleware?"} B -->|Yes| C["Build ToolContext middleware"] B -->|No| D{"Tool manager supports dispatch?"} D -->|Yes| E["Choose dispatch wrapper"] D -->|No| F{"Declared ToolContext reader or hook?"} F -->|Yes| G["Refuse mount"] F -->|No| H["Warn and mount unbound"] C --> I["Add routes and tools"] E --> I I --> J["Install chosen binding"]Reviews (2) · Last reviewed commit: "fix(protocol): read a dict _meta, take o..." · Reviewed by Greptile