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.
Building persistent memory for AI agents at Mnemoverse.
I write about the unglamorous half: forgetting, contradiction, and the moment an agent confidently repeats a fact you retracted.
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.
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.
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
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.
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, becauserequire_scopeonly runs inside it. Addexport_ticketsnext year behind a newtickets:exportscope, give it its entry in that map, and forgetrequire_scope: the filter hides it from a token holding the three scopes inALL_SCOPES, the loop checksclose_ticketanddelete_ticketand passes, and the assertion that the write tools are exactly those two passes with it. Atickets:readtoken callingexport_ticketsby name still runs it, since in mcp 2.3.0call_toolnever consults the listing.On a copy of the tickets server with that filter written as a
list_toolsoverride and the test as the post now has it, under mcp 2.3.0, the test passes and the direct call withtickets:readgoes through. Enumerating the registry instead,MCPServer.list_tools(mcp)past the override, the same loop fails onexport_ticketsbecause noToolErroris raised, and passes again oncerequire_scopeis back. The registry needs no scope list, so the test keeps testing the rule and not the scopes that existed when it was written.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.