Repository navigation
Description
Please read this first
- Have you read the docs? Yes.
- Have you searched for related issues? Yes.
Related but distinct:
- docs: unified capability-visibility pattern across function tools, MCP tools, and handoffs #4618 is about composing model-visible capability filtering across function tools, MCP tools, and handoffs.
- Per-tool authorization middleware for agent tool calls #2868 discusses generic per-tool authorization.
- Add pre-execution validation for tool calls #2970 discusses generic pre-execution validation.
This request is narrower: allow locally executed MCP tools to opt into the existing tool input/output guardrail abstraction that is already available for function tools.
Describe the feature
The SDK has a useful tool_input_guardrails / tool_output_guardrails abstraction for function tools.
Today, however, the guardrails documentation explicitly scopes tool guardrails to function tools, while local MCP servers have a different set of controls:
tool_filterfor model-visible tool availability;require_approvalfor HITL approval;tool_meta_resolverfor per-call MCP_meta;failure_error_functionfor MCP failure handling.
All of those are useful, but they solve different problems.
There does not appear to be a public configuration point for saying:
For every invocation of this locally executed MCP tool, run these input guardrails immediately before execution and these output guardrails immediately after execution.
This is slightly surprising because local MCP tools are already converted internally into SDK FunctionTool objects by MCPUtil.
Production-shaped example
Consider an agent connected to an application-managed MCP server.
Before an MCP call executes, the application may need to deterministically check things such as:
- whether the model-generated arguments contain data that must not be sent to that tool;
- whether an argument violates an invariant derived from trusted run context;
- whether that particular invocation should be rejected even though the tool itself is visible and available.
After the MCP call returns, the application may need to check the result before it becomes model-visible, for example:
- reject a response containing sensitive/internal data;
- prevent an unexpected or unsafe result from entering the agent context;
- apply the same output-validation policy already used for local function tools.
Conceptually:
model chooses MCP tool
↓
tool input guardrails
↓
local MCP server.call_tool(...)
↓
tool output guardrails
↓
result becomes model-visible
We ended up implementing this kind of wrapper downstream for MCP-backed agents. What started as a sample-specific concern became shared infrastructure because the same need recurred across tools and agents.
Why the existing MCP controls are not equivalent
tool_filter is primarily about visibility / eligibility: should the model see this tool?
require_approval answers whether execution should pause for approval.
tool_meta_resolver is useful for trusted per-call metadata such as tenant or trace information.
Tool guardrails answer a separate question:
Given the exact invocation/output at this execution boundary, should it be accepted?
These mechanisms are complementary rather than alternatives.
This also means I am not proposing that client-side guardrails replace authorization in the MCP server. Resource/action authorization should still be enforced at the protected operation.
Possible API shape
The exact API is less important than parity with the existing tool-guardrail model.
One possible server-level shape would be:
server = MCPServerStreamableHttp(
name="Billing",
params={"url": "..."},
tool_input_guardrails=[validate_mcp_input],
tool_output_guardrails=[validate_mcp_output],
)A resolver/per-tool configuration could also make sense if maintainers prefer not to apply every guardrail to every tool.
Since local MCP tools are already converted to FunctionTools internally, another possibility may simply be attaching the configured guardrails during MCP → FunctionTool conversion rather than introducing a separate execution system.
Desired semantics
Ideally:
- local MCP tools can opt into the existing input-tool-guardrail semantics before
call_tool(); - local MCP tools can opt into the existing output-tool-guardrail semantics before the result is accepted/model-visible;
- configuration can distinguish tools when necessary, rather than requiring one policy for every tool on a server;
- guardrails receive normal run context plus enough MCP origin information to distinguish server/tool identity;
- behavior is unchanged when no MCP guardrails are configured;
- interaction with
require_approvalis documented and, where practical, follows the same ordering semantics as function-tool approvals + guardrails; - guardrail results remain observable/traced in the same way as existing tool-guardrail results.
Non-goals
This issue is intentionally not asking for:
- a new authorization framework;
- replacing server-side MCP authorization;
- a generic middleware architecture for every SDK tool type;
- arbitrary input/output transformation semantics beyond what existing tool guardrails support;
HostedMCPToolparity in the same change.
Hosted MCP has a materially different execution boundary because the Python process does not own the MCP round trip, so I think it should be considered separately.
Why this seems like a useful SDK-level primitive
MCP tools are often precisely where an agent crosses from model reasoning into external systems.
Having the same lightweight pre/post execution checks for:
FunctionTool
and:
local MCP-backed FunctionTool
would make it easier to apply one production policy consistently without wrapping every MCP tool manually or reimplementing the MCP integration layer.
If maintainers agree this belongs in the SDK, I'd be happy to help with a focused implementation or docs/tests around the preferred API shape.
Activity
darklordVirtual commentedon Aug 24, 2026
I think parity here would be useful. One ordering property seems important enough to make explicit in the contract:
the call a human approves should be the call that crosses the execution boundary.
For a consequential MCP tool, I would expect something close to:
resolve exact call → input guardrails → approval if required → execute → output guardrails → model context
Running deterministic input checks before approval avoids asking a human to approve something the application already knows it must reject.
More importantly, once approval is obtained, I would bind it to the same normalized tool identity and arguments that are passed to call_tool(). Any material change after approval should require a new decision rather than inheriting the old approval.
That gives require_approval and input guardrails complementary semantics:
input guardrail: is this invocation admissible under deterministic application constraints?
approval: did the required authority approve this exact admissible invocation?
server authorization: does the protected operation permit it under current resource authority?
I would also preserve those as separate trace events. Otherwise an incident record that says “guardrail passed + approved” can become ambiguous about which argument set each decision actually covered.
The output side has a similarly useful distinction: an output guardrail can prevent unsafe data from entering model context, but it should not be interpreted as verification that the external side effect itself was correct.
Would it make sense for the MCP-to-FunctionTool conversion to preserve a stable per-call identity through guardrails, approval and execution, rather than having each layer observe the call independently?
dgenio commentedon Aug 24, 2026
I agree with the invariant that an approval must not silently carry over to a materially different invocation, but I would keep the ordering semantics aligned with the SDK's existing function-tool behavior rather than make
input guardrails → approval → executionmandatory.Today, for function tools, the default is:
and
ToolExecutionConfig(pre_approval_tool_input_guardrails=True)optionally adds a pre-approval guardrail pass. Importantly, calls that pass that pre-approval check are still checked again after approval, immediately before execution. That seems useful because the post-approval check can revalidate time-sensitive application constraints.So for MCP parity I would preserve that model:
I do agree with the identity/binding concern in a narrower sense: whatever invocation the approval decision refers to should not be mutated into materially different tool arguments and then inherit the old approval. The SDK already scopes normal approvals to a tool call ID, so I would first preserve those existing semantics through MCP → FunctionTool conversion rather than introduce a new general call-identity abstraction in this issue.
That keeps the responsibilities distinct:
If MCP guardrails reuse the existing FunctionTool machinery, matching these ordering and revalidation semantics would be the behavior I'd expect.
darklordVirtual commentedon Sep 16, 2026
That makes sense. I had the ordering too narrowly specified.
Reusing the existing FunctionTool semantics is cleaner, especially since the post-approval input guardrail already provides the fresh check immediately before execution.
The invariant I was trying to preserve is narrower: an approval for one invocation must not authorize materially different tool arguments later in the path.
If the existing tool call ID already provides that binding through MCP conversion, I agree that preserving it is preferable to adding another identity abstraction here.
The optional pre-approval pass also handles the wasted-approval case without changing the default ordering.