Skip to content

Let an mcp block fix a tool's arguments - #105

Open
haruotsu wants to merge 2 commits into
mainfrom
mcp-params
Open

haruotsu wants to merge 2 commits into
mainfrom
mcp-params

Conversation

@haruotsu

@haruotsu haruotsu commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Why

An mcp: block can choose which tools to offer with allow, but not constrain their arguments. A tool may take an argument that bounds what a call reaches. The only way to hold an agent to such a bound was to ask in the instruction, and nothing stops a call in which the model leaves it out. An openapi: block already has params for this.

What changes

  • params on an mcp: block, keyed by tool name and then argument name, with the same value form as openapi::

    - mcp:
        url: https://mcp.docs.example/mcp
        connection: docs
        allow: [search, fetch]
        effect: read
        params:
          search:
            scope: {value: team-handbook}

    Only value is offered; prefix/suffix are not, since nothing has needed them for MCP tools yet. null is refused, as for openapi:: a server reads it as no value, so nothing would be fixed.

  • What the model sees. A fixed argument is removed from the tool's inputSchema (properties and required) before ADK builds the function declaration from it.

  • What is sent. Every call carries the declared values over whatever the model wrote under the same names, so a smuggled value is overwritten, never sent.

  • validate refuses a params tool name that is not in allow, and refuses params on a block without allow: with every tool offered, a misspelt name would fix nothing and the tool it meant would be offered as the server lists it, with nobody told. The same check runs again when the runtime builds the toolset, since the archive that reached it is whatever was packed.

  • Checked where the tools are listed. validate cannot see a server's schemas; they are listed while the agent runs. So each fixed argument is checked against the listed inputSchema there. A tool whose schema does not declare one (renamed on the server, say) is not offered, and a warning names the tool and the argument, never the value. The tools are listed for every model request, so each warning is logged once per toolset rather than on every turn. Offering it would send the value under a name the server ignores, and the call would run unbounded. Raising instead would take every other tool of the agent down with it on every turn.

  • Implementation. The listed McpTool is replaced by a subclass built from its state rather than through McpTool.__init__, whose parameters change between ADK releases; that keeps the session, headers and confirmation setting it was built with, and isinstance(tool, McpTool) still holds. The narrowed schema goes on a copy of the MCP tool model, because ADK may cache a listing and build tools from it again on the next turn. ADK 2.6.0 and 2.14.0 both build the declaration from _mcp_tool's input schema and pass run_async's args to call_tool. The schema field is read as inputSchema or input_schema, since ADK 2.14 allows MCP SDK 2.x, which renames it.

  • README documents params for mcp: blocks next to allow.

Compatibility

Additive. Blocks without params build the same toolset. An agent that uses it has to be archived with a gete that includes this change.

Testing

  • Runtime (ADK's own McpTool over MCP SDK Tool models, no server): the fixed argument is gone from the declaration's properties and required list; a tool whose schema lacks it is not offered, the log names the tool and argument but not the value, and only once however often the tools are listed; the listed MCP tool is left as the server described it, and a second listing from the same objects still fixes the argument; a tool params does not name is offered as listed; does_not and per-name confirmation still reach a fixed tool; the build refuses a params name outside allow.
  • End to end, against the test MCP server (its lookup tool now takes an optional scope and echoes its arguments): the declaration the model gets from the real listing has no scope, and a call whose arguments say scope: everywhere reaches the server with the declared value.
  • Validate: a params name outside allow and params without allow are refused; the first also through gete validate, which prefixes the tool's position.
  • Schema: the value form passes; an empty mapping, a bare value, {}, null, prefix and value with suffix are refused.
  • pytest, ruff check, ruff format --check and mypy pass locally.

An openapi block can fix an argument with params; an mcp block could only
pick tools with allow. A tool may take an argument that bounds what a call
reaches, and the only way to hold the agent to it was the instruction,
which nothing enforces when the model leaves it out.

params on an mcp block names tools out of allow and fixes argument values,
mirroring openapi's value. The fixed arguments are taken out of the schema
the model is shown, and each call carries the declared values over anything
the model wrote.

validate cannot see a server's schemas, so the tool names are held against
allow, which is also why params needs allow: with every tool offered, a
misspelt name would fix nothing and nobody would know. Each fixed argument
is checked against the listed schema at run time instead; a tool whose
schema no longer declares one is not offered and the log says why, since
the value would otherwise ride under a name the server ignores.

The tool ADK listed is narrowed on a copy, because the listing may be
cached and built into tools again on the next turn.

🤖 Generated with Claude Code
The tools are listed for every model request, and a server whose schema
no longer declares a fixed argument stays that way until someone changes
the declaration or the server. Each listing logged the same warning
again, so one drift filled the log a line per turn and buried everything
else the agent wrote there.

The toolset now remembers what it has warned about and says each thing
once. The tool is still kept away on every listing.

🤖 Generated with Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant