diff --git a/AGENTS.md b/AGENTS.md index 96ab6ed77..01805d0f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,132 +1,59 @@ -# AGENTS.md -Sentry MCP is a Model Context Protocol server that exposes Sentry's error tracking and performance monitoring to AI assistants through 19 tools. - -## Principles - -- **Type Safety**: Prefer strict types over `any` - they catch bugs and improve tooling. Use `unknown` for truly unknown types. -- **Security**: Never log secrets. Validate external input. See docs/operations/security.md. -- **Simplicity**: Follow existing patterns. Check neighboring files before inventing new approaches. - -## Constraints - -- **Tool count**: Target ≤20, hard limit 25 (AI agents have limited tool slots). -- **Quality gate**: `pnpm run tsc && pnpm run lint && pnpm run test` must pass before committing. - -## Repository Structure - -``` -sentry-mcp/ -├── packages/ -│ ├── mcp-core/ # Core MCP implementation (private) -│ │ └── src/ -│ │ ├── tools/ # 19 tool modules -│ │ ├── server.ts # buildServer() -│ │ ├── api-client/ # Sentry API -│ │ └── internal/ # Shared utils -│ ├── mcp-server/ # stdio transport (@sentry/mcp-server on npm) -│ ├── mcp-cloudflare/ # Web app + OAuth -│ ├── mcp-server-evals/ # AI evaluation tests -│ ├── mcp-server-mocks/ # MSW mocks -│ └── mcp-test-client/ # CLI test client -└── docs/ # All documentation -``` - -## Documentation Map - -- docs/README.md — Full documentation index - -**Read before tool changes:** -- docs/contributing/adding-tools.md — Tool implementation guide -- docs/contributing/tool-responses.md — Tool output policy and QA review checklist -- docs/testing/overview.md — Testing requirements and snapshot policy -- docs/contributing/common-patterns.md — Error handling, Zod schemas, shared formatting patterns -- docs/contributing/error-handling.md — Error types and propagation - -**Contributing:** -- docs/contributing/api-patterns.md — Sentry API client usage -- docs/contributing/coding-guidelines.md — TypeScript and code style guidance -- docs/contributing/documentation-style-guide.md — Documentation style guide -- docs/contributing/pr-management.md — Commit and PR guidelines -- docs/contributing/quality-checks.md — Pre-commit checklist -- docs/contributing/search-events-api-patterns.md — Search Events API patterns - -**Testing:** -- docs/testing/overview.md — Unit, snapshot, eval, and agent CLI testing -- docs/testing/stdio.md — Stdio transport testing -- docs/testing/remote.md — Remote server and OAuth testing - -**Architecture and Operations:** -- docs/architecture/overview.md — System design -- docs/operations/security.md — Authentication and security patterns -- docs/operations/stdio-auth.md — Device code flow, token caching, client ID architecture -- docs/operations/oauth-signout-playbook.md — Remote OAuth diagnostic runbook -- docs/operations/embedded-agents.md — LLM provider configuration for AI-powered tools -- docs/operations/github-actions.md — GitHub Actions guidance -- docs/operations/logging.md — Logging guidance -- docs/operations/monitoring.md — Monitoring guidance -- docs/operations/token-cost-tracking.md — Tool definition token cost tracking - -**Cloudflare:** -- docs/cloudflare/overview.md — Cloudflare package overview -- docs/cloudflare/architecture.md — Cloudflare architecture -- docs/cloudflare/oauth-architecture.md — Cloudflare OAuth architecture - -**Integrations:** -- docs/integrations/claude-code-plugin.md — Plugin structure and agent prompts -- docs/integrations/ide-instructions-refactor.md — IDE instruction refactor notes - -**Specs:** -- docs/specs/README.md — Specs index -- docs/specs/embedded-agent-openai-routing.md — Embedded agent OpenAI routing spec -- docs/specs/search-events.md — Search Events spec -- docs/specs/subpath-constraints.md — Subpath constraints spec - -**Releases:** -- docs/releases/stdio.md — npm package release -- docs/releases/cloudflare.md — Cloudflare deployment +# Agent Instructions + +## Package Manager +- Use **pnpm**; Node.js must be `>=20`. +- `CLAUDE.md` is a symlink to this file; do not maintain a divergent copy. + +## Package Roles +| Package | Role | +|---|---| +| `packages/mcp-core` | Shared MCP server, tools, schemas, Sentry API client, and tool definitions. | +| `packages/mcp-server` | Published stdio transport package (`@sentry/mcp-server`). | +| `packages/mcp-cloudflare` | Hosted web app, HTTP `/mcp` transport, OAuth authorization server routes, and demo chat web client. | +| `packages/mcp-test-client` | Local CLI for stdio/HTTP transport QA, OAuth, DCR, CIMD, and agent-mode testing. | +| `packages/mcp-server-mocks` | MSW fixtures and mocks for tests. | + +## OAuth And Transport Boundaries +- `packages/mcp-server` uses stdio auth flows; do not mix it with hosted OAuth behavior. +- In `packages/mcp-cloudflare`, `/oauth/*` is the hosted OAuth authorization server. +- In `packages/mcp-cloudflare`, `/mcp` is the protected HTTP MCP resource; `?agent=1` switches to embedded-agent mode. +- In `packages/mcp-cloudflare`, `/api/chat` is the demo chat backend acting as an MCP client of `/mcp`. +- The demo chat OAuth client identity must stay separate from the MCP resource identity. +- Preserve Dynamic Client Registration unless a change explicitly removes it. ## Commands - -```bash -# Development -pnpm run dev # Start dev server -pnpm run build # Build all packages - -# Testing -pnpm -w run cli --transport stdio "q" # Test MCP tools -pnpm -w run cli --transport stdio --access-token=TOKEN "q" -pnpm -w run cli --transport stdio --agent "query" - -# Quality (run before committing) -pnpm run tsc && pnpm run lint && pnpm run test - -# Token overhead -pnpm run measure-tokens # Check tool definition size - -# Definitions (run after changing tools, skills, or agent prompts) -pnpm run --filter @sentry/mcp-core generate-definitions -``` - -## QA Playbook - -For MCP tool QA, use the `mcp-qa` skill at `.agents/skills/mcp-qa/SKILL.md`: -stdio-first local CLI and real agent clients; Cloudflare HTTP or `/mcp` only -for transport, OAuth, routing, or hosted-server compatibility. - -## Task Management - -Use `/dex` skill to coordinate complex work. Create tasks with full context, break down into subtasks, complete with detailed results. - -## Workflow - -1. Check neighboring files for existing patterns before writing new code. -2. When adding or modifying Sentry API endpoint usage, ALWAYS validate the endpoint behavior against the Sentry source code in `~/src/sentry` instead of assuming docs or client parameters are authoritative. -3. Update relevant docs when changing functionality. -4. Follow docs/contributing/error-handling.md for error types. -5. Follow docs/contributing/pr-management.md for commits and PRs. +| Task | Command | +|---|---| +| Full typecheck | `pnpm run tsc` | +| Full lint | `pnpm run lint` | +| Full tests | `pnpm run test` | +| Cloudflare tests | `pnpm --filter @sentry/mcp-cloudflare test` | +| Cloudflare typecheck | `pnpm --filter @sentry/mcp-cloudflare tsc` | +| CLI QA | `pnpm -w run cli --transport http --mcp-host=http://localhost:5173/mcp --list-tools` | +| Generate definitions | `pnpm run --filter @sentry/mcp-core generate-definitions` | + +## References +| Need | File | +|---|---| +| Docs index | `docs/README.md` | +| Tool changes | `docs/contributing/adding-tools.md` | +| Tool responses | `docs/contributing/tool-responses.md` | +| Error handling | `docs/contributing/error-handling.md` | +| Testing | `docs/testing/overview.md` | +| Remote/OAuth QA | `docs/testing/remote.md` | +| OAuth architecture | `docs/cloudflare/oauth-architecture.md` | +| Security | `docs/operations/security.md` | +| PR guidance | `docs/contributing/pr-management.md` | + +## Conventions +- Prefer strict TypeScript; use `unknown` instead of `any` for unknown values. +- Never log secrets or tokens. +- When changing Sentry API endpoint usage, validate behavior against `~/src/sentry`. +- Update docs for behavior changes. +- Run the relevant focused tests before the full quality gate. +- Run `pnpm run --filter @sentry/mcp-core generate-definitions` after changing tools, skills, or agent prompts. ## Commit Attribution - AI commits MUST include: ``` Co-Authored-By: (the agent model's name and attribution byline) diff --git a/README.md b/README.md index e3fd89310..dac3741b7 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ To contribute changes, you'll need to set up your local environment: - Edit `packages/mcp-cloudflare/.env` and add: - `SENTRY_CLIENT_ID=your_development_sentry_client_id` - `SENTRY_CLIENT_SECRET=your_development_sentry_client_secret` - - `COOKIE_SECRET=my-super-secret-cookie` + - `COOKIE_SECRET=my-super-secret-cookie` (optional for local dev; `pnpm dev` provides a development-only default when omitted) 4. **Start the development server:** diff --git a/docs/cloudflare/oauth-architecture.md b/docs/cloudflare/oauth-architecture.md index c99080f0d..1f7c3cb5b 100644 --- a/docs/cloudflare/oauth-architecture.md +++ b/docs/cloudflare/oauth-architecture.md @@ -101,11 +101,12 @@ sequenceDiagram The MCP OAuth Provider is built with `@cloudflare/workers-oauth-provider` and provides: -1. **Dynamic client registration** - MCP clients can register on-demand -2. **PKCE support** - Secure authorization code flow -3. **Token management** - Issues and validates MCP tokens -4. **Consent UI** - Custom approval screen for permissions -5. **Token encryption** - Stores Sentry tokens encrypted in MCP token props +1. **Client ID metadata documents (CIMD)** - MCP clients can use an HTTPS metadata URL as `client_id` +2. **Dynamic client registration fallback** - MCP clients can still register on-demand at `/oauth/register` +3. **PKCE support** - Secure authorization code flow +4. **Token management** - Issues and validates MCP tokens +5. **Consent UI** - Custom approval screen for permissions +6. **Token encryption** - Stores Sentry tokens encrypted in MCP token props ### Sentry OAuth Integration @@ -124,13 +125,19 @@ The integration with Sentry OAuth happens through: sequenceDiagram participant Agent as AI Agent participant MCPOAuth as MCP OAuth Provider + participant ClientMeta as Client Metadata URL participant KV as Cloudflare KV participant User as User participant MCP as MCP Server - Agent->>MCPOAuth: Register as client - MCPOAuth->>KV: Store client registration - MCPOAuth-->>Agent: MCP Client ID & Secret + alt CIMD client + Agent->>MCPOAuth: Use HTTPS metadata URL as client_id + MCPOAuth->>ClientMeta: Fetch client metadata document + else DCR fallback + Agent->>MCPOAuth: Register as client + MCPOAuth->>KV: Store client registration + MCPOAuth-->>Agent: MCP Client ID & Secret + end Agent->>MCPOAuth: Request authorization MCPOAuth->>User: Show MCP consent screen @@ -166,10 +173,16 @@ const oAuthProvider = new OAuthProvider({ authorizeEndpoint: "/oauth/authorize", tokenEndpoint: "/oauth/token", clientRegistrationEndpoint: "/oauth/register", + clientIdMetadataDocumentEnabled: true, scopesSupported: Object.keys(SCOPES), }); ``` +With CIMD enabled, the authorization-server metadata advertises +`client_id_metadata_document_supported: true`. URL client IDs must resolve to +metadata for a public authorization-code client; opaque dynamically registered +client IDs continue to use the stored DCR metadata. + ### 2. API Handler The `apiHandler` is a protected endpoint that requires valid OAuth tokens: diff --git a/openspec/changes/add-cimd-hosted-oauth/.openspec.yaml b/openspec/changes/add-cimd-hosted-oauth/.openspec.yaml new file mode 100644 index 000000000..3ac681e39 --- /dev/null +++ b/openspec/changes/add-cimd-hosted-oauth/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-17 diff --git a/openspec/changes/add-cimd-hosted-oauth/design.md b/openspec/changes/add-cimd-hosted-oauth/design.md new file mode 100644 index 000000000..eac01a770 --- /dev/null +++ b/openspec/changes/add-cimd-hosted-oauth/design.md @@ -0,0 +1,122 @@ +## Context + +The hosted MCP server runs behind the Cloudflare Worker in `packages/mcp-cloudflare`. OAuth is delegated to `@cloudflare/workers-oauth-provider`, while this repo owns MCP-specific routing, path-scoped protected-resource metadata, a compatibility authorization-server metadata shim for `/mcp...` paths, and the approval dialog shown before redirecting to Sentry OAuth. + +MCP `2025-11-25` recommends OAuth Client ID Metadata Documents (CIMD). In CIMD, a client can use an HTTPS metadata document URL as its OAuth `client_id`, and the authorization server fetches that document to learn the client name, redirect URIs, and supported authentication method. The installed Cloudflare OAuth provider supports this when `clientIdMetadataDocumentEnabled: true` is set and the Worker has the `global_fetch_strictly_public` compatibility flag. The Wrangler configs already include that flag, but the provider option and metadata advertisement are missing. + +The implementation must preserve the existing hosted OAuth behavior: + +- `/oauth/register` remains available for Dynamic Client Registration. +- `/mcp`, `/mcp/{org}`, and `/mcp/{org}/{project}` resources remain path-scoped. +- Protected-resource metadata continues to round-trip exact resource path and query. +- Authorization-server metadata compatibility documents continue to pre-populate RFC 8707 `resource` on the authorization endpoint. +- Expected OAuth request failures remain user-correctable 4xx responses logged with `logWarn`, not Sentry issues. + +## Goals / Non-Goals + +**Goals:** + +- Enable CIMD in the hosted Cloudflare OAuth provider. +- Advertise CIMD support from root and path-scoped authorization-server metadata. +- Preserve Dynamic Client Registration as the compatibility fallback. +- Verify URL-client authorization requests succeed only when fetched metadata is valid. +- Ensure invalid client metadata fails closed without open redirect behavior. +- Display enough client identity and redirect destination context in the consent UI for URL-based clients. +- Keep scoped MCP resource discovery and authorization behavior unchanged. + +**Non-Goals:** + +- Do not remove DCR. +- Do not change stdio auth or Sentry device-code auth. +- Do not replace the Cloudflare provider's CIMD implementation with a custom fetcher unless tests prove provider behavior is insufficient. +- Do not add enterprise allowlists, ID-JAG, or managed authorization policy in this change. +- Do not require repo-local `mcp-test-client` CIMD support before shipping server support. + Optional CIMD mode may still be added as local QA support. + +## Decisions + +### Enable CIMD through the Cloudflare provider + +Set `clientIdMetadataDocumentEnabled: true` on the existing `new OAuthProvider(...)` construction in `packages/mcp-cloudflare/src/server/index.ts`. + +Rationale: the provider already owns client lookup, auth request parsing, redirect URI validation, and DCR storage. Using its CIMD support keeps one source of truth for OAuth client behavior and minimizes security-sensitive custom code. + +Alternative considered: implement custom CIMD fetching before calling provider APIs. That would duplicate URL validation, metadata parsing, and redirect URI rules, increasing SSRF and validation risk. + +### Keep DCR enabled + +Leave `clientRegistrationEndpoint: "/oauth/register"` unchanged. + +Rationale: MCP clients should prefer CIMD when advertised, but older or simpler clients still rely on DCR. Removing DCR would be an interoperability regression. + +Alternative considered: gate DCR behind a compatibility flag. This creates rollout complexity without a current security or product requirement. + +### Advertise support in both root and scoped metadata + +Root authorization-server metadata is produced by the Cloudflare provider, so tests should confirm it includes `client_id_metadata_document_supported: true` after the provider option is enabled. The repo-owned scoped compatibility metadata in `authorization-server-metadata.ts` must add the same field directly. + +Rationale: MCP clients can discover authorization metadata through root RFC 8414 discovery or through the existing path-scoped compatibility endpoints. Both paths must expose consistent capability information so SDK clients choose CIMD instead of DCR when available. + +Alternative considered: advertise CIMD only at root. That leaves path-scoped clients on DCR even though the server supports CIMD. + +### Treat provider CIMD failures as expected OAuth 4xx failures + +Invalid client metadata, metadata fetch failures, redirect URI mismatches, and disallowed client auth methods should return OAuth client errors and be logged as warnings where this repo catches them. + +Rationale: these are user/client-correctable failures and should not generate Sentry issues. + +Alternative considered: capture all provider lookup failures. That would create noise and could log attacker-controlled metadata URLs or redirect values more broadly than necessary. + +### Harden consent display for URL-based clients + +The approval dialog already shows client name and redirect destination. For URL-shaped `client_id` values, it should also display the client metadata URL or a sanitized host/path derived from that URL. The redirect URI and redirect host must remain visible, including localhost redirect URIs. + +Rationale: CIMD changes client identity from an opaque registered client to a fetched document. A friendly client name alone is not enough for a safe consent decision. + +Alternative considered: rely only on fetched `client_name`. That is easy to spoof and weakens user trust decisions. + +### Give the hosted demo chat a separate CIMD client identity + +Expose a first-party OAuth client metadata document for the hosted demo chat at +`/.well-known/oauth-client/demo-chat.json`. The document describes the web chat +client only: `client_uri` is the deployment root, `redirect_uris` contains only +`/api/auth/callback`, and `/mcp` remains the protected resource requested through +the RFC 8707 `resource` parameter. + +Use this CIMD client ID on HTTPS origins. Keep Dynamic Client Registration for +local HTTP development, where a public HTTPS metadata URL is not available. + +Rationale: the demo chat backend connects to `/mcp` as an MCP client, but the +MCP server must not identify as its own OAuth client. A separate chat CIMD +document makes the client/resource boundary explicit and gives production a +stable path to dogfood CIMD without adding localhost redirect URIs to the +production client identity. + +Alternative considered: use the deployment root or `/mcp` URL as the chat +`client_id`. That blurs the OAuth client and protected resource roles and makes +the consent identity less precise. + +## Risks / Trade-offs + +- [Risk] Enabling URL-based client IDs expands outbound fetch behavior from the Worker. -> Mitigation: rely on the provider's CIMD validation and keep `global_fetch_strictly_public`; add tests for failure cases and avoid custom fetch bypasses. +- [Risk] Some clients may react differently once metadata advertises CIMD and stop using DCR. -> Mitigation: test an MCP SDK client with `clientMetadataUrl`, test DCR-only clients, and roll out through canary. +- [Risk] Consent UI could hide important client origin details. -> Mitigation: add focused UI tests that assert client identity URL/host, redirect URI, and redirect host are visible. +- [Risk] Scoped metadata could drift from root metadata. -> Mitigation: add tests for root, `/mcp`, `/mcp/{org}`, and `/mcp/{org}/{project}` authorization metadata. +- [Risk] Testing the third-party provider's full behavior may require Worker-runtime support or network fetch mocking. -> Mitigation: prefer integration tests through the Worker route where feasible; otherwise mock only external metadata URL fetches and keep provider APIs real. +- [Risk] The same Worker serves the chat client metadata and validates that metadata through OAuth. -> Mitigation: keep the route public read-only, request-origin-aware, and covered by route tests plus deployed HTTPS smoke testing. + +## Migration Plan + +1. Add failing tests for metadata advertisement, provider configuration, CIMD authorization behavior, invalid metadata failures, consent identity display, and unchanged scoped resource behavior. +2. Enable `clientIdMetadataDocumentEnabled: true` and add scoped metadata advertisement. +3. Update the approval dialog only if tests show URL-based client identity is not clear enough. +4. Run Cloudflare package tests and type checks. +5. Run the full quality gate before merge. +6. Deploy to canary first and watch OAuth telemetry for invalid client errors, metadata fetch failures, redirect URI failures, and DCR volume changes. + +Rollback is a code revert of the provider option and scoped metadata field. Keeping DCR enabled means clients that previously worked through DCR continue to have a fallback during rollback. + +## Open Questions + +- Should CIMD be always on in production after tests pass, or should the provider option be controlled by an environment flag for canary-only rollout? +- Should the Cloudflare OAuth provider dependency be upgraded before enabling CIMD, or is the installed version sufficient once the targeted tests pass? diff --git a/openspec/changes/add-cimd-hosted-oauth/proposal.md b/openspec/changes/add-cimd-hosted-oauth/proposal.md new file mode 100644 index 000000000..c1f248c22 --- /dev/null +++ b/openspec/changes/add-cimd-hosted-oauth/proposal.md @@ -0,0 +1,35 @@ +## Why + +Hosted MCP OAuth should support OAuth Client ID Metadata Documents (CIMD) so MCP clients can use an HTTPS metadata URL as `client_id` instead of relying only on Dynamic Client Registration. MCP `2025-11-25` makes CIMD a recommended interoperability path, and the current hosted Cloudflare OAuth proxy has the required Workers compatibility flag but does not enable or advertise CIMD support. + +## What Changes + +- Enable CIMD support in the Cloudflare OAuth provider used by the hosted MCP server. +- Advertise `client_id_metadata_document_supported: true` from root and path-scoped authorization-server metadata. +- Preserve Dynamic Client Registration as a fallback for clients that do not support CIMD. +- Validate URL-based `client_id` authorization flows against fetched metadata, including redirect URI and client authentication method constraints. +- Keep protected-resource metadata and `WWW-Authenticate` challenges scoped to the exact `/mcp...` resource path. +- Ensure the OAuth approval UI exposes enough client and redirect-origin information for users to make trust decisions. +- Give the hosted demo chat a first-party CIMD client identity while keeping it separate from the `/mcp` protected resource. + +## Capabilities + +### New Capabilities + +- `hosted-oauth-cimd`: Hosted Cloudflare OAuth supports and advertises Client ID Metadata Documents while preserving DCR fallback and scoped MCP resource behavior. + +### Modified Capabilities + +- None. + +## Impact + +- `packages/mcp-cloudflare/src/server/index.ts` +- `packages/mcp-cloudflare/src/server/authorization-server-metadata.ts` +- `packages/mcp-cloudflare/src/server/lib/approval-dialog.ts` +- `packages/mcp-cloudflare/src/server/oauth/routes/authorize.ts` +- `packages/mcp-cloudflare/src/server/routes/chat-oauth.ts` +- `packages/mcp-cloudflare/src/server/routes/chat.ts` +- Cloudflare package OAuth and metadata tests +- `packages/mcp-test-client/src/auth/oauth.ts` for optional CIMD QA and RFC 8707 token-resource parity +- `AGENTS.md` package/OAuth/transport boundary guidance diff --git a/openspec/changes/add-cimd-hosted-oauth/specs/hosted-oauth-cimd/spec.md b/openspec/changes/add-cimd-hosted-oauth/specs/hosted-oauth-cimd/spec.md new file mode 100644 index 000000000..498db9a0b --- /dev/null +++ b/openspec/changes/add-cimd-hosted-oauth/specs/hosted-oauth-cimd/spec.md @@ -0,0 +1,129 @@ +## ADDED Requirements + +### Requirement: Hosted OAuth advertises CIMD support +The hosted Cloudflare OAuth authorization server SHALL advertise OAuth Client ID Metadata Document support when CIMD is enabled. + +#### Scenario: Root authorization metadata advertises CIMD +- **WHEN** a client requests `/.well-known/oauth-authorization-server` +- **THEN** the response metadata includes `client_id_metadata_document_supported: true` +- **AND** the response continues to include `registration_endpoint` + +#### Scenario: Scoped authorization metadata for base MCP advertises CIMD +- **WHEN** a client requests `/.well-known/oauth-authorization-server/mcp` +- **THEN** the response metadata includes `client_id_metadata_document_supported: true` +- **AND** the `authorization_endpoint` includes an RFC 8707 `resource` parameter for `/mcp` + +#### Scenario: Scoped authorization metadata for organization and project advertises CIMD +- **WHEN** a client requests `/.well-known/oauth-authorization-server/mcp/{organizationSlug}/{projectSlug}` +- **THEN** the response metadata includes `client_id_metadata_document_supported: true` +- **AND** the `authorization_endpoint` includes an RFC 8707 `resource` parameter for the same organization and project path + +### Requirement: Hosted OAuth accepts valid URL client IDs +The hosted Cloudflare OAuth authorization server SHALL accept an HTTPS URL `client_id` when the referenced client metadata document is valid. + +#### Scenario: Valid CIMD authorization request reaches consent +- **WHEN** an authorization request uses `client_id` set to an HTTPS client metadata document URL +- **AND** the fetched metadata document contains a matching `client_id`, at least one matching `redirect_uri`, compatible `grant_types`, compatible `response_types`, and `token_endpoint_auth_method: "none"` +- **THEN** the authorization request reaches the approval dialog +- **AND** the approval dialog uses the fetched client metadata + +#### Scenario: Approved CIMD request validates redirect URI +- **WHEN** a user approves a CIMD authorization request +- **THEN** the server validates the selected `redirect_uri` against the fetched client metadata before redirecting upstream +- **AND** the upstream redirect is created only for a registered redirect URI + +### Requirement: Hosted OAuth rejects invalid CIMD metadata safely +The hosted Cloudflare OAuth authorization server MUST reject invalid client metadata documents without granting authorization or creating open redirects. + +#### Scenario: Metadata fetch fails +- **WHEN** an authorization request uses a URL `client_id` whose metadata fetch does not return a successful response +- **THEN** the authorization request is rejected with an OAuth client error +- **AND** no upstream authorization redirect is produced + +#### Scenario: Metadata client ID mismatch +- **WHEN** the fetched metadata document has a `client_id` that does not exactly match the URL `client_id` +- **THEN** the authorization request is rejected with an OAuth client error +- **AND** no upstream authorization redirect is produced + +#### Scenario: Metadata has no redirect URI +- **WHEN** the fetched metadata document has no usable `redirect_uris` +- **THEN** the authorization request is rejected with an OAuth client error +- **AND** no upstream authorization redirect is produced + +#### Scenario: Requested redirect URI is not listed +- **WHEN** the authorization request uses a `redirect_uri` that is not listed in the fetched metadata document +- **THEN** the authorization request is rejected with an OAuth client error +- **AND** no upstream authorization redirect is produced + +#### Scenario: Metadata declares a disallowed client authentication method +- **WHEN** the fetched metadata document declares a confidential-client authentication method such as `client_secret_post` +- **THEN** the authorization request is rejected with an OAuth client error +- **AND** no upstream authorization redirect is produced + +### Requirement: Consent UI identifies URL-based clients +The hosted OAuth approval dialog SHALL display enough client and redirect information for users to evaluate URL-based client identities. + +#### Scenario: Consent shows fetched client name and client origin +- **WHEN** the approval dialog is rendered for a CIMD client +- **THEN** the dialog displays the fetched client name +- **AND** the dialog displays the URL client ID or a sanitized host/path derived from it + +#### Scenario: Consent shows redirect destination +- **WHEN** the approval dialog is rendered with a redirect URI +- **THEN** the dialog displays the redirect URI +- **AND** the dialog displays the redirect URI hostname as the post-approval destination + +#### Scenario: Consent keeps localhost redirects visible +- **WHEN** the approval dialog is rendered with a localhost redirect URI +- **THEN** the dialog displays the localhost redirect URI in the redirect destination warning + +### Requirement: Demo chat uses a separate CIMD client identity +The hosted demo chat SHALL use a first-party OAuth client identity that is separate from the MCP protected resource identity when running on HTTPS. + +#### Scenario: Demo chat metadata describes only the chat client +- **WHEN** a client requests `/.well-known/oauth-client/demo-chat.json` +- **THEN** the response `client_id` equals the metadata URL +- **AND** `client_uri` is the deployment root +- **AND** `redirect_uris` contains `/api/auth/callback` +- **AND** `redirect_uris` does not contain `/mcp` + +#### Scenario: Demo chat authorization uses CIMD on HTTPS +- **WHEN** a browser starts demo chat OAuth from an HTTPS origin +- **THEN** the authorization redirect uses the demo chat metadata URL as `client_id` +- **AND** the authorization redirect includes an RFC 8707 `resource` parameter for `/mcp` +- **AND** the chat flow does not perform Dynamic Client Registration + +#### Scenario: Demo chat keeps DCR fallback for local HTTP +- **WHEN** a browser starts demo chat OAuth from a local HTTP origin +- **THEN** the chat flow uses Dynamic Client Registration +- **AND** the authorization redirect includes an RFC 8707 `resource` parameter for `/mcp` + +#### Scenario: Demo chat token exchange preserves resource scope +- **WHEN** the demo chat exchanges an authorization code for tokens +- **THEN** the token request includes the same `/mcp` resource identifier + +### Requirement: Dynamic Client Registration remains available +The hosted Cloudflare OAuth authorization server SHALL preserve Dynamic Client Registration for clients that do not use CIMD. + +#### Scenario: DCR endpoint remains advertised +- **WHEN** a client requests authorization-server metadata +- **THEN** the metadata includes `registration_endpoint` +- **AND** DCR-capable clients can continue registering through `/oauth/register` + +#### Scenario: Existing DCR authorization continues +- **WHEN** an authorization request uses a previously registered non-URL client ID +- **THEN** the server validates the request against the registered client metadata +- **AND** the request can reach the approval dialog without requiring a client metadata document URL + +### Requirement: Scoped MCP resource behavior remains intact +The hosted Cloudflare OAuth server SHALL preserve exact path-scoped MCP resource discovery and challenges while adding CIMD support. + +#### Scenario: Protected resource metadata preserves exact resource +- **WHEN** a client requests `/.well-known/oauth-protected-resource/mcp/{organizationSlug}/{projectSlug}` with query parameters +- **THEN** the response `resource` value includes the exact `/mcp/{organizationSlug}/{projectSlug}` path and query +- **AND** the response includes at least one authorization server + +#### Scenario: WWW-Authenticate challenge uses scoped protected resource metadata +- **WHEN** an unauthenticated request to `/mcp/{organizationSlug}/{projectSlug}` receives a 401 response +- **THEN** the `WWW-Authenticate` header includes exactly one `resource_metadata` parameter +- **AND** that parameter points to `/.well-known/oauth-protected-resource/mcp/{organizationSlug}/{projectSlug}` with the original query string preserved diff --git a/openspec/changes/add-cimd-hosted-oauth/tasks.md b/openspec/changes/add-cimd-hosted-oauth/tasks.md new file mode 100644 index 000000000..86e4e9087 --- /dev/null +++ b/openspec/changes/add-cimd-hosted-oauth/tasks.md @@ -0,0 +1,48 @@ +## 1. Metadata and Configuration Tests + +- [x] 1.1 Add/extend worker entrypoint tests to assert `new OAuthProvider(...)` receives `clientIdMetadataDocumentEnabled: true` while preserving `/oauth/register`. +- [x] 1.2 Add/extend authorization-server metadata tests for `/.well-known/oauth-authorization-server` to assert `client_id_metadata_document_supported: true` and `registration_endpoint` are present. +- [x] 1.3 Add/extend scoped authorization-server metadata tests for `/mcp`, `/mcp/{org}`, and `/mcp/{org}/{project}` to assert CIMD support and resource-bound authorization endpoints. +- [x] 1.4 Add/extend Wrangler config tests or static assertions to guard that prod, canary, and test configs keep `global_fetch_strictly_public`. + +## 2. CIMD OAuth Flow Tests + +- [x] 2.1 Add a valid URL-client authorization test where fetched metadata has matching `client_id`, allowed `redirect_uris`, compatible grant/response types, and `token_endpoint_auth_method: "none"`. +- [x] 2.2 Add an approval POST test proving the selected redirect URI is validated against fetched CIMD metadata before upstream redirect. +- [x] 2.3 Add failure tests for metadata fetch non-200, `client_id` mismatch, missing or empty `redirect_uris`, unlisted requested `redirect_uri`, and disallowed `token_endpoint_auth_method`. +- [x] 2.4 Confirm invalid CIMD requests return expected 4xx OAuth/client errors and do not produce upstream authorization redirects. + +## 3. Scoped Resource Regression Tests + +- [x] 3.1 Keep or add protected-resource metadata coverage for exact `/mcp...` path and query preservation. +- [x] 3.2 Keep or add `WWW-Authenticate` coverage proving the patched `resource_metadata` parameter is path-specific and appears exactly once. +- [x] 3.3 Confirm existing DCR authorization and `/oauth/register` tests still pass with CIMD enabled. + +## 4. Server Implementation + +- [x] 4.1 Add `clientIdMetadataDocumentEnabled: true` to the Cloudflare `OAuthProvider` options in `packages/mcp-cloudflare/src/server/index.ts`. +- [x] 4.2 Add `client_id_metadata_document_supported: true` to `createScopedAuthorizationServerMetadataResponse(...)`. +- [x] 4.3 Preserve existing DCR registration endpoint behavior and existing RFC 8707 resource binding behavior. +- [x] 4.4 Verify expected client-side OAuth failures are logged with `logWarn` and do not become Sentry-captured issues. + +## 5. Consent UI + +- [x] 5.1 Add approval-dialog tests asserting CIMD clients show fetched client name plus URL client ID or sanitized client host/path. +- [x] 5.2 Add or preserve approval-dialog tests asserting redirect URI, redirect hostname, and localhost redirect URIs remain visible. +- [x] 5.3 Update `packages/mcp-cloudflare/src/server/lib/approval-dialog.ts` if needed to display URL-based client identity clearly and safely. + +## 6. Validation and QA + +- [x] 6.1 Run `pnpm --filter @sentry/mcp-cloudflare test`. +- [x] 6.2 Run `pnpm --filter @sentry/mcp-cloudflare tsc`. +- [x] 6.3 Run `pnpm run tsc && pnpm run lint && pnpm run test` before merge. +- [ ] 6.4 Manually QA with an MCP SDK OAuth client configured with `clientMetadataUrl` and verify CIMD is preferred when advertised. +- [ ] 6.5 Manually QA a DCR-only client to confirm fallback compatibility. + +## 7. Hosted Demo Chat CIMD Dogfood + +- [x] 7.1 Add a public first-party CIMD document for the hosted demo chat at `/.well-known/oauth-client/demo-chat.json`. +- [x] 7.2 Use the demo chat CIMD URL as `client_id` on HTTPS origins while preserving DCR for local HTTP development. +- [x] 7.3 Include the `/mcp` RFC 8707 `resource` parameter in demo chat authorization and token exchange requests. +- [x] 7.4 Add route-level tests for chat metadata, HTTPS CIMD selection, local DCR fallback, and token-resource propagation. +- [x] 7.5 Update repo agent guidance to clarify package roles, OAuth roles, and transport boundaries. diff --git a/packages/mcp-cloudflare/src/server/app.test.ts b/packages/mcp-cloudflare/src/server/app.test.ts index da76a2209..08dbbebdb 100644 --- a/packages/mcp-cloudflare/src/server/app.test.ts +++ b/packages/mcp-cloudflare/src/server/app.test.ts @@ -238,6 +238,7 @@ describe("app", () => { "https://mcp.sentry.dev/oauth/authorize?resource=https%3A%2F%2Fmcp.sentry.dev%2Fmcp%2Fsentry%2Fmcp-server", token_endpoint: "https://mcp.sentry.dev/oauth/token", registration_endpoint: "https://mcp.sentry.dev/oauth/register", + client_id_metadata_document_supported: true, scopes_supported: [ "org:read", "project:write", @@ -271,5 +272,35 @@ describe("app", () => { ); expect(json.issuer).toBe("https://mcp.sentry.dev/mcp/sentry/mcp-server"); }); + + it("should advertise CIMD support for base MCP scoped metadata", async () => { + const res = await app.request( + "https://mcp.sentry.dev/.well-known/oauth-authorization-server/mcp", + { headers: TEST_HEADERS }, + ); + + expect(res.status).toBe(200); + + const json = await res.json(); + expect(json.client_id_metadata_document_supported).toBe(true); + expect(json.authorization_endpoint).toBe( + "https://mcp.sentry.dev/oauth/authorize?resource=https%3A%2F%2Fmcp.sentry.dev%2Fmcp", + ); + }); + + it("should advertise CIMD support for organization scoped metadata", async () => { + const res = await app.request( + "https://mcp.sentry.dev/.well-known/oauth-authorization-server/mcp/sentry", + { headers: TEST_HEADERS }, + ); + + expect(res.status).toBe(200); + + const json = await res.json(); + expect(json.client_id_metadata_document_supported).toBe(true); + expect(json.authorization_endpoint).toBe( + "https://mcp.sentry.dev/oauth/authorize?resource=https%3A%2F%2Fmcp.sentry.dev%2Fmcp%2Fsentry", + ); + }); }); }); diff --git a/packages/mcp-cloudflare/src/server/app.ts b/packages/mcp-cloudflare/src/server/app.ts index 86867e5fb..4f9422d6f 100644 --- a/packages/mcp-cloudflare/src/server/app.ts +++ b/packages/mcp-cloudflare/src/server/app.ts @@ -7,7 +7,7 @@ import { createRequestLogger } from "./logging"; import sentryOauth from "./oauth"; import { createProtectedResourceMetadataResponse } from "./protected-resource-metadata"; import chat from "./routes/chat"; -import chatOauth from "./routes/chat-oauth"; +import chatOauth, { getChatClientMetadata } from "./routes/chat-oauth"; import mcpRoutes from "./routes/mcp"; import metadata from "./routes/metadata"; import search from "./routes/search"; @@ -156,6 +156,13 @@ const app = new Hono<{ endpoint: `${baseUrl}/mcp`, }); }) + // OAuth Client ID Metadata Document for the hosted demo chat client. + // + // This identifies the web chat client only. The MCP server remains the + // protected resource and is requested separately through RFC 8707 `resource`. + .get("/.well-known/oauth-client/demo-chat.json", (c) => { + return c.json(getChatClientMetadata(getBaseUrl(c))); + }) // RFC 9728: OAuth 2.0 Protected Resource Metadata for /mcp resources. .get( "/.well-known/oauth-protected-resource/mcp", diff --git a/packages/mcp-cloudflare/src/server/authorization-server-metadata.ts b/packages/mcp-cloudflare/src/server/authorization-server-metadata.ts index 254c3b84a..d78efd2a4 100644 --- a/packages/mcp-cloudflare/src/server/authorization-server-metadata.ts +++ b/packages/mcp-cloudflare/src/server/authorization-server-metadata.ts @@ -49,6 +49,7 @@ export function createScopedAuthorizationServerMetadataResponse( ), token_endpoint: new URL("/oauth/token", requestUrl.origin).href, registration_endpoint: new URL("/oauth/register", requestUrl.origin).href, + client_id_metadata_document_supported: true, scopes_supported: Object.keys(SCOPES), response_types_supported: ["code"], response_modes_supported: ["query"], diff --git a/packages/mcp-cloudflare/src/server/index.test.ts b/packages/mcp-cloudflare/src/server/index.test.ts index adfa59057..ab9eb6e8a 100644 --- a/packages/mcp-cloudflare/src/server/index.test.ts +++ b/packages/mcp-cloudflare/src/server/index.test.ts @@ -149,6 +149,56 @@ describe("worker entrypoint", () => { expect(response.headers.has("Access-Control-Expose-Headers")).toBe(false); }); + it("keeps demo chat client metadata public and read-only", async () => { + mockOAuthProviderFetch.mockResolvedValueOnce( + new Response("{}", { + headers: { + "Access-Control-Allow-Origin": "https://evil.com", + "Access-Control-Allow-Methods": "*", + "Access-Control-Allow-Headers": "Authorization, *", + "Access-Control-Allow-Credentials": "true", + }, + }), + ); + + const response = await handler.fetch!( + new Request( + "https://mcp.sentry.dev/.well-known/oauth-client/demo-chat.json", + ), + env, + ctx, + ); + + expect(response.status).toBe(200); + expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); + expect(response.headers.get("Access-Control-Allow-Methods")).toBe( + "GET, OPTIONS", + ); + expect(response.headers.get("Access-Control-Allow-Headers")).toBe( + "Content-Type", + ); + expect(response.headers.has("Access-Control-Allow-Credentials")).toBe( + false, + ); + }); + + it("enables CIMD while preserving Dynamic Client Registration", async () => { + mockOAuthProviderFetch.mockResolvedValueOnce(new Response("ok")); + + await handler.fetch!( + new Request("https://mcp.sentry.dev/oauth/token", { method: "POST" }), + env, + ctx, + ); + + expect(MockOAuthProvider).toHaveBeenCalledWith( + expect.objectContaining({ + clientRegistrationEndpoint: "/oauth/register", + clientIdMetadataDocumentEnabled: true, + }), + ); + }); + it("patches MCP 401 responses with protected resource metadata", async () => { mockOAuthProviderFetch.mockResolvedValueOnce( new Response("unauthorized", { diff --git a/packages/mcp-cloudflare/src/server/index.ts b/packages/mcp-cloudflare/src/server/index.ts index 0c7e91c30..d3aa1f8e8 100644 --- a/packages/mcp-cloudflare/src/server/index.ts +++ b/packages/mcp-cloudflare/src/server/index.ts @@ -238,6 +238,7 @@ const wrappedOAuthProvider = { authorizeEndpoint: "/oauth/authorize", tokenEndpoint: "/oauth/token", clientRegistrationEndpoint: "/oauth/register", + clientIdMetadataDocumentEnabled: true, tokenExchangeCallback: (options) => tokenExchangeCallback(options, env, request, clientFamily), scopesSupported: Object.keys(SCOPES), diff --git a/packages/mcp-cloudflare/src/server/lib/approval-dialog.test.ts b/packages/mcp-cloudflare/src/server/lib/approval-dialog.test.ts index 6eb73e8fc..3d45299e4 100644 --- a/packages/mcp-cloudflare/src/server/lib/approval-dialog.test.ts +++ b/packages/mcp-cloudflare/src/server/lib/approval-dialog.test.ts @@ -164,6 +164,54 @@ describe("approval-dialog", () => { expect(html).not.toContain("