DEV Community

Discussion on: Your MCP server probably accepts tokens that were never meant for it

Collapse
 
sampop3 profile image
sam ros •

You're right on both counts, and thanks for testing it. I reproduced the empty-loop case in CI: with a list_tools override that filters by the token's scopes, the old test passed while a tickets:read token could still delete. The test now lists tools with an all-scopes token, asserts the write tools it found are exactly close_ticket and delete_ticket, and a separate test builds your filtered server with the scope check removed and confirms the rule still catches it. I also rewrote the hiding advice: tools behind a scope the user can grant stay listed so the insufficient_scope challenge can reach them, and only tools behind scopes the caller can never get are hidden. The post is updated and credits you.

Collapse
 
izgorodin profile image
Edward Izgorodin •

Listing as an all-scopes caller fixes the empty loop, but it hands the test's coverage to ALL_SCOPES, a list someone has to keep current. A scope filter needs each tool's scope declared somewhere outside the tool body, because require_scope only runs inside it. Add export_tickets next year behind a new tickets:export scope, give it its entry in that map, and forget require_scope: the filter hides it from a token holding the three scopes in ALL_SCOPES, the loop checks close_ticket and delete_ticket and passes, and the assertion that the write tools are exactly those two passes with it. A tickets:read token calling export_tickets by name still runs it, since in mcp 2.3.0 call_tool never consults the listing.

On a copy of the tickets server with that filter written as a list_tools override and the test as the post now has it, under mcp 2.3.0, the test passes and the direct call with tickets:read goes through. Enumerating the registry instead, MCPServer.list_tools(mcp) past the override, the same loop fails on export_tickets because no ToolError is raised, and passes again once require_scope is back. The registry needs no scope list, so the test keeps testing the rule and not the scopes that existed when it was written.

Thread Thread
 
sampop3 profile image
sam ros •

You’re right, Edward. Listing as an all-scopes caller only moved the blind spot: the test now covers the scopes ALL_SCOPES held when it was written, so a tool behind a scope added later can be hidden by the filter and skipped, while call_tool still runs it because it never looks at the listing. Enumerating the registry directly, MCPServer.list_tools(mcp) past the override, needs no scope list, so it keeps testing the rule instead of a snapshot of the scopes. That’s the version I’d keep. Thanks for running it against a real server again.