Drive it from Claude
POST /mcp is stateless Streamable HTTP, served by McpController and built per request from the
caller’s grant.
Discovery
Section titled “Discovery”An unauthenticated call answers 401 with a WWW-Authenticate header pointing at the protected
resource metadata:
curl -sS http://demo.uris.test:4242/.well-known/oauth-protected-resourceBoth /.well-known/oauth-protected-resource and .../mcp are routed. The client reads the issuer
out of it, registers itself with that issuer, and comes back with a token.
Tokens come from masks
Section titled “Tokens come from masks”This application verifies and never signs. Granted#masks_claims_from hands the credential to
masks_resource.authenticate, which checks the signature against the issuer’s JWKS, then iss
against MASKS_ISSUER_TEMPLATE and aud against this tenant’s own /mcp URL.
Point MASKS_ISSUER_TEMPLATE at a running issuer. For a raw curl, take a token from that issuer’s
token endpoint:
curl -sS -X POST http://demo.uris.test:4242/mcp \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'tools/list returns your grant — Grant#tools filters Tool.all by the token’s scopes, so a tool
you cannot call is absent rather than refused. See MCP tools.
The browser holds a token too
Section titled “The browser holds a token too”Granted#credentials prefers the Authorization header and falls back to the session:
def session_authorization return nil unless masks_signed_in? || (masks_tokens && masks_refresh!)
"Bearer #{masks_access_token}"endmasks-rails keeps the token in the encrypted session, and it is verified on the way in exactly as
a presented one would be. A cookie and a bearer reach the same Grant by the same path.
Behind a tunnel
Section titled “Behind a tunnel”Set URIS_PUBLIC_ORIGIN to the origin you are tunnelling through. Tenant.origin prefers it over
request.base_url, and Tenant.resource_url builds the audience from it. It must be stable —
OAuth redirect URIs register against it. Leave it blank when not tunnelling.
Where every tenant answers on its own host, write it as a template over the subdomain —
https://%{subdomain}.example.com — or every tenant pairs itself against the first tenant’s
resource, and claims that tenant’s namespace pointing at somebody else’s endpoint.
Connecting a resource is a browser flow
Section titled “Connecting a resource is a browser flow”enroll_resource returns a signed link with a 30-minute window; the credential is captured at
/enroll/:token in the browser. command_resource cannot set credentials however the command is
phrased.
Two interfaces, one domain layer
Section titled “Two interfaces, one domain layer”browser → /graphql urql + codegen + cable subscriptionsClaude → /mcp typed toolsNeither wraps the other. Both authorize through Grant. See GraphQL.