Repository navigation
fix(mcp): tools/call no longer 404s on a worker that has not served tools/list - #42072
Merged
Merged
Conversation
The startup tool-name fill skips servers whose upstream wants the caller's own token (true_passthrough, OAuth discovery), and mcp 2 no longer runs the list handler before an uncached tools/call, so every uvicorn worker that had not served tools/list answered 404 "Tool not found" for prefixed tools/call and the REST server_id route on those servers. On a resolution miss, execute_mcp_tool now lists the prefix-matched (or server_id-requested) server once, with the caller's credentials, through the existing tools/list path, then resolves as before. Listing failures fall through to the existing 404, a worker that already listed the server never re-lists it, and a server outside the caller's allowed set is never listed.
Contributor
Author
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
Contributor
|
Comment on lines
+2878
to
+2883
| def has_listed_tools(self, server: MCPServer) -> bool: | ||
| """True once this worker holds at least one tool row for ``server``.""" | ||
| owned: Final = self._owned_mapping_values(server) | ||
| return any( | ||
| normalize_server_name(owner) in owned for owner in self.tool_name_to_mcp_server_name_mapping.values() | ||
| ) |
Contributor
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
A tools/call on a cold worker listed the target server once and then never again, so a later caller whose credentials expose a wider upstream catalog got 404 for tools the first caller never had. Gate the pre-call listing on whether this worker already exposes the requested tool, so callers with different catalogs no longer mask each other. Removing the per-server guard also drops the empty-listing case that re-listed on every call.
Contributor
Contributor
|
bugbot run |
Contributor
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit a3e9ed3. Configure here.
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.
TLDR
Problem this solves:
tools/call404s on any worker that has not servedtools/listtrue_passthroughand OAuth-discovery server on multi-worker and multi-pod deploymentsHow it solves it:
User Flow
Before: a developer whose agent (Claude Code, in the runs below) calls tools through a
true_passthroughMCP server gets "Tool not found" from every worker that has not served atools/listsince the last restart--num_workers 2and an MCP serverrig_passthroughconfigured withauth_type: true_passthroughinitializewithAuthorization: Bearer sk-1234and gets 200 with anmcp-session-idheadernotifications/initializedwith that session id and gets 202tools/callwith{"name":"rig_passthrough-add","arguments":{"a":1,"b":2}}and gets{"content":[{"type":"text","text":"Error: Tool 'add' not found"}],"isError":true}. Every fresh session gets the same answer on both workers, and a client that senttools/listonce still gets it whenever a later call lands on the other worker{"name":"add","server_id":"<server id>","arguments":{"a":1,"b":2}}gets 500An unexpected error occurred: Tool add not foundfrom that workerAfter: the same calls return the tool result on every worker, whether or not it has served a
tools/list--num_workers 2and an MCP serverrig_passthroughconfigured withauth_type: true_passthroughinitializewithAuthorization: Bearer sk-1234and gets 200 with anmcp-session-idheadernotifications/initializedwith that session id and gets 202tools/callwith{"name":"rig_passthrough-add","arguments":{"a":1,"b":2}}and gets{"content":[{"type":"text","text":"3"}],"isError":false}on every fresh session, on both workers, with notools/listfirst{"name":"add","server_id":"<server id>","arguments":{"a":1,"b":2}}gets 200 with"text":"3"from both workersrig_passthrough-nope, still getsError: Tool 'nope' not foundRelevant issues
Affected release
Regression on
mainsince #41718 (eda1fab); not in any rc or dev tag yet, so no backport neededLinear ticket
Resolves LIT-8212
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Shared setup for both legs: one proxy booted with
--num_workers 2from the named commit (mcp 2.2.0), Postgres behind it, and three MCP servers pointing at the same local upstream, which servesadd,request_headers, andfail_toolEvery "cold" iteration below is a fresh MCP session against a freshly restarted proxy:
initialize(capturesmcp-session-id),notifications/initialized, thentools/callfor<server>-addwithAuthorization: Bearer sk-1234, and never atools/listfirst. Thepid=lines under each run come from a custom logging callback in the rig config that records which uvicorn worker served the call, so both workers are shown handling requestsThe Claude Code case drives the real Claude Code TUI (v2.1.278) under tmux against the same 2-worker proxy, started with
claude --mcp-config mcp.json --strict-mcp-config, wheremcp.jsonis{"mcpServers":{"litellm":{"type":"http","url":"http://127.0.0.1:16733/mcp","headers":{"Authorization":"Bearer sk-1234"}}}}and, on each leg, a prompt that asks it to call
rig_passthrough-addwith a=1 and b=2 six separate times, one call at a time, and report each call's resultBefore (82ddab2)
Claude Code calls the true_passthrough tool six times
tools/listat startup was served by worker pid 95887, and all sixtools/callrequests landed on worker pid 95886, which had not served a listCold tools/call on the true_passthrough server
One tools/list, then cold tools/call again
tools/list(served by one worker), then eight more cold callsREST tools/call with server_id on a cold worker
curl -sS http://127.0.0.1:16733/mcp-rest/tools/call -H 'Content-Type: application/json' -H 'Authorization: Bearer sk-1234' -d '{"name":"add","arguments":{"a":1,"b":2},"server_id":"a3298f3a30d401988d10b2c011283f74"}'repeated until a request landed on the worker that had not served a listCold tools/call on the plain http and client_credentials servers (control)
rig_http-addandrig_oauth_cc-addCold tools/call for a tool the server does not have
After (a3e9ed3)
Claude Code calls the true_passthrough tool six times
tools/listat startup was served by worker pid 17328, and all sixtools/callrequests landed on worker pid 17330, which had never served a list3, and Claude Code concluding "All six sequential calls to rig_passthrough-add with a=1 and b=2 returned 3 each time, with no errors"This is the direct mirror of the Before leg: the same one-list-then-six-calls-on-the-other-worker split, now every call resolving instead of every call 404ing
Cold tools/call on the true_passthrough server
One tools/list, then cold tools/call again
tools/list(worker pid 17328) followed by sixtools/callon the never-listed worker pid 17330REST tools/call with server_id on a cold worker
tools/list, so it lands on a worker that has not listedCold tools/call on the plain http and client_credentials servers (control)
rig_http-addandrig_oauth_cc-addCold tools/call for a tool the server does not have
rig_passthrough-nopeWhat the run saw next to the fix
Type
🐛 Bug Fix
Caveats (if any)
tools/liston that worker before the call answers "not found". This is the accepted cost of listing lazily. It runs on the error path only, never on a successful call, and is bounded to one re-list per callcall_tooldirectly, not through this pre-call listing, so a cold worker there can still answer "not found" for a passthrough tool it never listed across a multi-turn worker split. This PR neither causes nor changes that path, its resolution is byte-identical before and after, so it is pre-existing and left alone, a candidate follow-up if that surface needs the same guaranteeFinal Attestation