Skip to content

Outbound MCP consent should be per server and per remote tool, not one global write grant #262

Closed

Description

@serge-ivo

Problem

mcp_call_tool is correctly marked as write-scoped because an arbitrary remote MCP tool may mutate external state. But the current consent model is too coarse:

  • read-only remote tools are blocked unless the generic write-scoped mcp_call_tool is permitted
  • granting write access to connector mcp potentially allows all calls to all configured MCP endpoints available to that instance
  • user consent UI cannot distinguish Figma read actions from Figma edits, ProAgentStore reads from destructive changes, etc.

Why this matters

MCP servers publish tool metadata, and users need understandable control over what an agent can do in each connected app. A single global mcp write toggle is not enough for valuable app-operating agents.

Expected behavior

Consent should be scoped at least by MCP connection/server, and preferably by remote tool. Remote tool annotations such as read-only/destructive hints should inform UX, but must be treated as hints rather than trusted security boundaries. PAGS should keep server-side enforcement based on user grants and explicit confirmations.

Acceptance criteria

  • Consent state distinguishes between different MCP endpoints/connections.
  • User can allow read-only remote tools without granting all remote writes.
  • User can approve individual write-capable remote tools or tool groups for a specific MCP connection.
  • Destructive/overwrite-style remote tools require an additional explicit confirmation path when identifiable.
  • Consent checks happen before dispatch to the remote MCP server.
  • Audit records show which endpoint and remote tool consent allowed or denied the call.
  • Existing connector write-consent behavior remains fail-closed for the generic escape hatch.

Related

Code pointers

  • workers/api/src/lib/tool-registry.ts
  • workers/api/src/lib/connector-consent.ts
  • workers/api/src/lib/connectors/mcp.ts
  • store/console/src/tabs/SettingsTab.tsx

Activity

  1. serge-ivo commented on Aug 6, 2026

    @serge-ivo
    ContributorAuthor

    Shipped in 4f77f33 (migration renumbered to 0079 in 4e88d80 — another agent landed a 0078
    minutes earlier).

    Verified against HEAD before building — both claims held:

    • runRegistryTool gated writes at connector granularity via instance_connector_consent
      (migration 0051), so one ('mcp','write') row authorised every MCP endpoint the instance
      could name and every tool on each of them. Real escalation, given the endpoint is
      user-supplied config: consenting to a site builder consented to whatever endpoint a later
      pipeline step — or an agent reading untrusted text — decided to point at.
    • a read-only remote tool was indeed unreachable without granting the write-scoped
      mcp_call_tool ✔

    What landed

    New workers/api/src/lib/mcp-consent.ts + migration 0079_mcp_tool_consent.sql
    (instance_mcp_consent, PK (instance_id, endpoint, tool)).

    • Endpoint is normalized to scheme://host[:port]/path — lowercased, no trailing slash,
      query and fragment dropped. That stops consent being dodged with a cache-buster, collapses
      /mcp and /mcp/ onto one key, and keeps a credential-bearing query string out of the table
      (and out of the Outbound MCP calls need audit log, redaction, and connection diagnostics #265 log).
    • Checked before anything touches the network or the vault, and resolved against the
      executor via consentInstanceOf ([coordination][security] Supervision must not escalate authority — a delegation lends a goal, never privileges #185) — a supervisor cannot lend its MCP grants
      downward. There's a test asserting a refused call mints no token.
    • * wildcard covers a server's ordinary tools but not ones whose names read as
      destructive (delete/purge/drop/remove/overwrite/…); those need approving by name,
      which is the additional explicit confirmation, and it's recorded rather than
      clicked-through. Matching is on whole words, snake_case and camelCase, so removeUser is
      caught and undeleted_items is not.
    • Deliberate deviation worth reviewing: the destructive test runs on the tool name we put
      on the wire, not on the server's destructiveHint/readOnlyHint. Those annotations are
      authored by the party we're defending against. They may inform UI later; they are not the
      boundary. This is why "allow read-only tools without granting writes" is satisfied by
      per-tool grants rather than by auto-allowing anything the server labels read-only.
    • mcp_list_tools stays ungated (read scope, names no server) — you cannot approve
      individual tools if you can't discover what they are.
    • The connector-level mcp write row is kept as the outer gate and kill switch; revoking
      it still stops everything at once. Granting a specific (server, tool) sets it in the same
      action, so the user isn't hunting a second checkbox; the implication runs one way only —
      granting the connector still names no server.

    Surfaces: GET/PUT /v1/instances/:id/mcp/consent, and an MCP servers panel under
    Settings → Permissions & Connections (add/revoke, destructive rows flagged) so the grants are
    actually usable rather than API-only.

    Acceptance criteria

    • consent distinguishes different MCP endpoints
    • read-only remote tools allowable without granting all remote writes (via per-tool grants)
    • individual write-capable remote tools approvable per connection
    • destructive/overwrite-style tools require additional explicit confirmation when identifiable
    • consent checked before dispatch
    • audit records show which endpoint and remote tool consent allowed or denied (Outbound MCP calls need audit log, redaction, and connection diagnostics #265)
    • generic escape hatch remains fail-closed

    Migration note for existing subscribers

    This tightens: an instance that only has the connector-level mcp write grant can no
    longer call anything until a (server, tool) grant exists. That includes the site-builder /
    site-deploy pipelines — their subscribers need one grant for the builder endpoint (* is
    enough; nothing in those pipelines has a destructive name). Fine pre-launch, and it is the
    whole point of the ticket, but it is a behaviour change rather than a pure addition.

    Tests: mcp-consent.test.ts (15) + the consent block in mcp.test.ts, all green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions