Add client-side call-tool filters for tool-call policy enforcement - #1882
Open
1aifanatic wants to merge 1 commit into
Open
1aifanatic wants to merge 1 commit into
1aifanatic wants to merge 1 commit into
Conversation
Adds McpClientOptions.Filters.Request.CallToolFilters, mirroring the server-side filter pipeline, so hosts can inspect, rewrite, or block tools/call requests (for example, based on tool annotations) before they reach the server. Every CallToolAsync overload, McpClientTool.CallAsync, and McpClientTool invocations through an IChatClient route through a single private protected CallToolCoreAsync seam, so no path bypasses the filters. Filters receive the tool definition from the existing tool cache (populated by ListToolsAsync/AddKnownTools), or null when unknown. The new APIs are marked experimental (MCPEXP002). Fixes modelcontextprotocol#1453 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p29dMDsLnn6KDFmsz2PGr
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.
Closes #1453
Motivation
Hosts that let a model choose tools need a single place to apply policy before a
tools/callleaves the client, for example requiring confirmation for tools annotateddestructiveHint: true. Today there is no client-side equivalent of the server'sCallToolFilters, so hosts have to wrap everyMcpClientToolby hand, and any path they miss (such as a directclient.CallToolAsync(...)) bypasses the policy.Change
This follows the design @elzouhery laid out in the issue thread. It adds client request filters that mirror the server-side filter shape (
McpServerOptions.Filters.Request.CallToolFilters):ModelContextProtocol.Client:McpClientFilters,McpClientRequestFilters,McpClientRequestFilter<TParams, TResult>,McpClientRequestHandler<TParams, TResult>, andMcpClientRequestContext<TParams>(Client, settableParams,Tool?). They are exposed throughMcpClientOptions.Filters.CallToolAsync(CallToolRequestParams, ...)now routes through aprivate protected virtual CallToolCoreAsyncseam, in the same wayValidateCacheableResultdoes. Every other overload,McpClientTool.CallAsync, andMcpClientToolinvoked through anIChatClientalready converge there, so all of them are filtered.McpClient's public or abstract surface doesn't change._toolCache, whichListToolsAsyncandAddKnownToolspopulate and which is already used for the SEP-2243Mcp-Param-*headers, so no new caching is added. On a cache miss,Toolisnull, and the docs say to fail closed.[Experimental(MCPEXP002)]so the shape can still change.list-of-diagnostics.mdanddocs/concepts/filters.mdare updated.Design notes
FunctionInvokingChatClientcatches an exception, the model only sees"Error: Function failed."(unlessIncludeDetailedErrorsis on). When the filter returnsIsError = true, the model sees the reason. The docs recommend that pattern, so there is noToolBlockedException.SendRequestAsync(JsonRpcRequest), which also bypasses server filters (this is documented), and task-augmented calls fromModelContextProtocol.Extensions.Tasks. Those returnCreateTaskResultthrough a separate path. I'm happy to follow up if you want them to compose.Tests
McpClientCallToolFilterTestshas 8 tests against a real in-memoryMcpServer/McpClientpair:Toolisnullwhen the tool wasn't listed, and the policy fails closedToolis populated for tools registered withAddKnownToolsCallToolAsync(string),CallToolAsync(string)with progress,CallToolAsync(CallToolRequestParams),McpClientTool.CallAsync, andAIFunction.InvokeAsyncParams(argument redaction) reach the server, and the filter can post-process the resultTo check that the tests can fail, I built once with the pipeline never installed: 7 of the 8 tests failed (the setter test doesn't depend on the pipeline).
Full local run on Windows (.NET SDK 10.0.302):
dotnet buildof the solution is clean (warnings are errors), and the new tests pass on all four target frameworks (net10.0, net9.0, net8.0, net472). The results of the full suites:ModelContextProtocol.AspNetCore.Tests: net10.0 635 passed / 0 failed, net9.0 635 / 0. On net8.0 oneClientConformanceTestsauth scenario failed once, then passed 40/40 on two reruns.ModelContextProtocol.Tests: in the full parallel run across all four TFMs, the only failures besides the two noted below wereClientIntegrationTestsagainst the npx-launched "everything" server, which timed out under load. Run on their own, they pass 59/59 (2 skipped) on every TFM.SerializeToElement). I fixed it withMcpJsonUtilities.DefaultOptionsand re-verified on all four TFMs.🤖 Generated with Claude Code
https://claude.ai/code/session_011p29dMDsLnn6KDFmsz2PGr