Skip to content

Keep the signed file-action subset through MCP read-only authorization (#1103) - #1128

Merged
callumalpass merged 1 commit into
mainfrom
fix/mcp-readonly-oauth-1103
Nov 2, 2026
Merged

callumalpass merged 1 commit into
mainfrom
fix/mcp-readonly-oauth-1103

Conversation

@callumalpass

Copy link
Copy Markdown
Contributor

Fixes #1103.

Root cause

With scope=mdbase:read, MCP signs only the file actions that go with its requested operations: list and read, and no durable_mutation. In /oauth/authorization_request, Connect compared that proof with the application's full declared file set (fileRequestForRequirements), which includes the optional add, replace, move and delete. That full set requires durable_mutation, so Connect refused the proof with durable_mutation_unsupported before any authorization request existed. MCP reported the refusal as HTTP 500 (mcp_upstream_redirect_failed).

Two more problems sat behind that one in the same path:

  1. Consent and approval ignored the signed subset. The consent preview and every approval path used the full declaration as the file-action ceiling. An approval with no explicit file_actions (the portal default, and how staging extended acceptance approves) would therefore select all six actions. The hosted provider admits a replica only when its file actions are within the signed requested_files (verified_setup_evidence), so activation would have failed even after the proof was accepted.
  2. Periodic reconciliation retired subset grants. reconcileApplicationGrants, which runs on every application about every 6 hours, required a grant's file_capability to deep-equal the full declaration. Any grant with an approved optional subset, including every read-only MCP grant, was retired as application_manifest_change.

Change

  • signedFileRequestForRequirements (application-requirements.ts): both authorization request routes now expect the declared-order selection that the application signed. It must include every required action, use only declared actions and keep the declared scope. Semantic v1 still requires the exact declaration. The existing exact proof comparison does the enforcement, and nothing is widened.
  • planCollectionGrant / previewCollectionGrant take applicationFileCeiling, which is the signed requested_files.actions, the file counterpart of applicationOperationCeiling. It is passed from the consent preview and from the hosted, native, local portal/connector and activation re-plan paths. The default selection is the signed subset, and an approval that asks for an action outside it is refused.
  • Reconciliation uses fileCapabilityPermittedByRequirements: same scope, every required action, only declared actions. It replaces the exact-match check.
  • The read-only OAuth scope is not broadened, and the MCP gateway is unchanged.

Tests

  • New services/mcp/src/readonly-authorization.real-server.test.ts runs the real MCP gateway against the real Connect control plane (buildApp, memory DB). Only the Rust hosted provider is a double, and it enforces the provider's file-subset admission rule. The test covers MCP /oauth/authorize with mdbase:read, the claim, the consent preview (offers only list/read), an acceptance-style approval with no file_actions, the read-only replica enrolment, the status and callback, the MCP token exchange, and application reconciliation keeping the grant.
    • It fails on main with exactly the staging symptom (MCP 500 at /oauth/authorize).
    • With only the routes fix, approval fails (500, provider mismatch).
    • With only the routes and consent fixes, reconciliation revokes the grant.
  • grant-planner.test.ts: signed-selection canonicalisation, the consent/approval ceiling and the reconciliation predicate.
  • Remote (rpnpm) runs: server and MCP typecheck pass. Server unit suite: 171 files pass, 76 Postgres files skipped. MCP suite: 56/56. Architecture and changelog checks pass.
  • I ran the related Postgres suites locally against a disposable Postgres 17. Every failure on this branch either also fails on main on the same machine, mostly from 15 s timeouts, or passes when rerun. grant-narrowing "revocation holding the grant lock cannot be undone by PATCH" fails on both.

architecture.d/mcp-readonly-oauth-1103.json justifies +1 export. fileCapabilityForRequirements became internal, and an unused import was removed.

Compatibility

This PR does not change the protocol or MCP. Clients that sign the full declaration, which covers every currently working client, are unaffected. It merges cleanly with #1118, and its preview hunk carries over to #1118's renamed native collections block.

#1103)

Read-only MCP OAuth (scope mdbase:read) signs only the file actions paired
with its requested operations (list, read). Connect compared that proof with
the application's full declared file set, which includes the optional write
actions, so it required durable_mutation and refused the proof before an
authorization request existed. MCP surfaced that as HTTP 500.

Connect now accepts a signed request that selects declared optional file
actions (every required action, declared order, declared scope) and keeps
exactly that signed subset as the ceiling for the consent preview, every
approval path and the provider grant. Approval can no longer add file
actions the application did not sign. Periodic application reconciliation
now accepts an approved optional subset instead of retiring every grant
whose file capability is not the full declaration.

Adds a real-server regression test that runs the MCP gateway's read-only
OAuth flow against the real Connect control plane through consent, approval,
token exchange and reconciliation.
@callumalpass
callumalpass added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 3691306 Nov 2, 2026
17 checks passed
@callumalpass
callumalpass deleted the fix/mcp-readonly-oauth-1103 branch November 2, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP read-only OAuth fails before consent because optional file actions require a different signed contract

1 participant