Skip to content

Build1 publisher3 min readPublished

The policy filter has to run before tools/list reaches the model

A dev.to post counts seven safety controls MCP leaves to whoever integrates it. The catalog filter it recommends turns every advertised tool into a per-session authorization decision somebody has to own.

The Engineer · Build desk

Illustration accompanying The policy filter has to run before tools/list reaches the model

What happened

  • A dev.to post argues that MCP genuinely solved tool discovery by standardizing how servers describe tools and how clients invoke them, and that discoverability was never authorization.
  • It counts seven controls the protocol does not inherently enforce, among them per-user authorization, least privilege, sandboxing, approval workflows and tool-result sanitization.
  • It also puts tool descriptions, schemas and tool results inside the model's context, and therefore inside the attack surface, rather than treating them as inert metadata.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Per-principal filtering makes the tool catalog session state, so there is no single cached catalog per server and no allowlist you can review once and leave alone.
  • cost Authorizing at the tool-name level puts every tool a vendor adds to their server into your policy backlog, and no upstream party is going to make that call for you.
  • decision If the client talks to servers directly there is nowhere to insert a filter, so the gateway question gets settled before any question about which servers to connect.
  • exposure A server installed as a local dev dependency inherits the developer's privileges, which makes supply-chain trust in that package the same question as handing it a shell.

A tools/list call returns names, descriptions and input schemas [1]. Those strings land in the same context window as the user's request, which is why the dev.to post treats descriptions and results as attack surface rather than metadata [4]. Nothing in that exchange asks who is on the other end of the session. According to the post, the model's only signal that a tool is available is its presence in the list [5].

The CRM example is worth reading closely because it contains no attacker [6]. A support engineer asks for a cleanup of duplicate test accounts. The catalog holds list_customers, update_customer and delete_customer, so two of the three advertised tools write and one destroys [7]. The model calls the one whose name matches the verb in the request. No injected instruction is needed for that.

The remedy the post proposes is a filter that runs before the catalog reaches the model, keyed on the authenticated principal, the session risk level and the task context [8]. The resource string in its sample code is the part to read: principal.can("mcp:tools/list", `mcp-server:${serverId}:tool:${tool.name}`) [9]. The unit of authorization is one tool on one server. Take the post's own opening figure of three connected servers and assume each looks like the three-tool CRM server, and one principal already needs nine catalog decisions before the agent does anything [10]. Production servers carry more than three tools, and roles multiply it again.

You cannot pay that cost once, either. Dynamic tool registration breaks static reviews [14], so the filter has to evaluate at request time rather than at onboarding, and "trusted server" is a runtime assumption instead of a static fact [15]. This is also the reason the seam sits upstream of consent: OAuth gives you identity, not intent [16].

Worth being clear about what the evidence here is. One practitioner's threat model, published on dev.to, with no protocol text quoted in support [19]. The claim that tool annotations are hints rather than guards [12] is asserted, not traced to specification language, and anyone designing around it should read the spec directly. Treat the seven-item gap list [3] the way you would treat someone else's benchmark table: it transfers if your deployment matches the one described. That means a client connecting straight to servers you did not write, exposing write-capable tools to sessions whose principal the server never learns.

If you wrote the servers, and they already scope every call by the caller's token, then per-user authorization and least privilege are enforced at the resource and two items come off your bill. The context item does not come off. Descriptions, schemas and results are model input regardless of who shipped the server [4]. Nor does the local case: a server running on a developer's machine runs with that developer's privileges [13].

The pre-MCP arrangement had one accidental virtue. Bespoke glue meant a one-off authentication flow per integration [18], so the auth decision lived in the integration code whether you designed it or not. Standardizing discovery separated the two, and the gateway is where they get rejoined [17]. It is the last point in the path where the request still carries a principal and the catalog is not yet prompt text.

What to watch

  • Whether MCP clients ship a supported hook for per-principal catalog filtering, or whether a gateway has to terminate the protocol itself to get one.
  • Whether the specification adds normative language on per-user scoping and annotation trust, which would move items off the integrator's list.
  • Whether any widely used server publishes a stable tool inventory, since dynamic registration is what forces request-time evaluation.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories