Skip to content

Drive it from Claude

POST /mcp is stateless Streamable HTTP, served by McpController and built per request from the caller’s grant.

An unauthenticated call answers 401 with a WWW-Authenticate header pointing at the protected resource metadata:

Terminal window
curl -sS http://demo.uris.test:4242/.well-known/oauth-protected-resource

Both /.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.

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:

Terminal window
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.

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}"
end

masks-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.

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.

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.

browser → /graphql urql + codegen + cable subscriptions
Claude → /mcp typed tools

Neither wraps the other. Both authorize through Grant. See GraphQL.