MCP Protected Resource Metadata (OAuth) - RFC9728
Claude Remote MCP requires an OAuth protected-resource metadata endpoint for discovery.
This guide covers AppTheoryMcpProtectedResource, which adds:
GET /.well-known/oauth-protected-resource/...resource path...
What Claude expects
When calling your MCP server without a token, Claude expects:
401 UnauthorizedWWW-Authenticate: Bearer resource_metadata="https://<host>/.well-known/oauth-protected-resource/mcp"
Claude then fetches the metadata endpoint and expects JSON like:
{
"resource": "https://mcp.example.com/mcp",
"authorization_servers": ["https://auth.example.com"]
}
TypeScript example
import { Stack } from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import {
AppTheoryMcpProtectedResource,
AppTheoryRemoteMcpServer,
} from "@theory-cloud/apptheory-cdk";
const stack = new Stack();
const handler = new lambda.Function(stack, "McpHandler", {
runtime: lambda.Runtime.PROVIDED_AL2023,
handler: "bootstrap",
code: lambda.Code.fromAsset("dist"),
});
const mcp = new AppTheoryRemoteMcpServer(stack, "RemoteMcp", {
handler,
apiName: "remote-mcp",
});
new AppTheoryMcpProtectedResource(stack, "ProtectedResource", {
router: mcp.router,
resource: mcp.endpoint,
authorizationServers: ["https://auth.example.com"],
});
Important notes
- this construct only adds the metadata endpoint
- your MCP Lambda still needs to enforce
Authorization: Bearer ...and emit theWWW-Authenticatechallenge on401 - the construct derives the metadata route from
resourceper RFC9728, so a resource ofhttps://mcp.example.com/mcpbecomesGET /.well-known/oauth-protected-resource/mcp - the
resourcevalue should match the actual/mcpURL the client uses, including any custom domain or base path - for API Gateway REST APIs,
/.well-known/...sits under the same stage or base path as your/mcproute - for per-actor bundles (
/mcp/{actor}), preferAppTheoryRemoteMcpServer({ actorPath: true }), which co-registers the matching discovery route automatically - on that sanctioned REST API v1 actor-path deploy shape, AppTheory accepts both the canonical discovery path and the same path with a trailing slash, so app-local slash stripping is no longer required there