You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Outbound MCP consent should be per server and per remote tool, not one global write grant #262
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.
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).
* 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
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.
Activity
feat(mcp): dual-era protocol, per-server consent, redacted trace (#260 …
serge-ivo commentedon Aug 6, 2026
Shipped in 4f77f33 (migration renumbered to
0079in 4e88d80 — another agent landed a0078minutes earlier).
Verified against HEAD before building — both claims held:
runRegistryToolgated writes at connector granularity viainstance_connector_consent(migration 0051), so one
('mcp','write')row authorised every MCP endpoint the instancecould 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.
mcp_call_tool✔What landed
New
workers/api/src/lib/mcp-consent.ts+ migration0079_mcp_tool_consent.sql(
instance_mcp_consent, PK(instance_id, endpoint, tool)).scheme://host[:port]/path— lowercased, no trailing slash,query and fragment dropped. That stops consent being dodged with a cache-buster, collapses
/mcpand/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).
executor via
consentInstanceOf([coordination][security] Supervision must not escalate authority — a delegation lends a goal, never privileges #185) — a supervisor cannot lend its MCP grantsdownward. There's a test asserting a refused call mints no token.
*wildcard covers a server's ordinary tools but not ones whose names read asdestructive (
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
removeUseriscaught and
undeleted_itemsis not.on the wire, not on the server's
destructiveHint/readOnlyHint. Those annotations areauthored 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_toolsstays ungated (read scope, names no server) — you cannot approveindividual tools if you can't discover what they are.
mcpwrite row is kept as the outer gate and kill switch; revokingit 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 underSettings → Permissions & Connections (add/revoke, destructive rows flagged) so the grants are
actually usable rather than API-only.
Acceptance criteria
Migration note for existing subscribers
This tightens: an instance that only has the connector-level
mcpwrite grant can nolonger call anything until a (server, tool) grant exists. That includes the
site-builder/site-deploypipelines — their subscribers need one grant for the builder endpoint (*isenough; 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 inmcp.test.ts, all green.docs(api): spec the outbound-MCP consent routes (#262)
feat(mcp): connect an MCP server by testing it, not by guessing (#266, …
feat(mcp): connect an OAuth MCP server instead of pasting a token it …
feat(mcp): read the server's resources and prompts — fenced, capped, …
feat(mcp): a granted remote tool is a real tool, not a string the mod…
test(security): make the invariants fail the build when the next call…
fix(api,console): the tool listing reports the write-consent gate it …
feat(mcp): pause a remote call that asks for user input, and resume i…
fix(agent): the tool list states the consent an agent HAS, not the ru…
fix(connectors): a write tool's description no longer contradicts its…
fix(connectors): each write-consent checkbox says what granting IT pe…