diff --git a/.agents/skills/logging-observability/SKILL.md b/.agents/skills/logging-observability/SKILL.md index 56d551374..e05d6c32c 100644 --- a/.agents/skills/logging-observability/SKILL.md +++ b/.agents/skills/logging-observability/SKILL.md @@ -28,21 +28,21 @@ ApiError (base class) ### Application Errors (`packages/mcp-core/src/internal/errors.ts`) -| Error Type | Sentry Issue? | Use Case | -|------------|---------------|----------| -| `UserInputError` | No | Validation failures the user can fix | -| `ConfigurationError` | No | Missing or invalid configuration | -| `LLMProviderError` | No | AI provider service unavailable (e.g., region restrictions) | +| Error Type | Sentry Issue? | Use Case | +| -------------------- | ------------- | ----------------------------------------------------------- | +| `UserInputError` | No | Validation failures the user can fix | +| `ConfigurationError` | No | Missing or invalid configuration | +| `LLMProviderError` | No | AI provider service unavailable (e.g., region restrictions) | ## Logging Functions (`packages/mcp-core/src/telem/logging.ts`) -| Function | Creates Sentry Issue? | Use For | -|----------|----------------------|---------| -| `logDebug()` | No | Development debugging information | -| `logInfo()` | No | Routine operational information | -| `logWarn()` | No | Expected errors (4xx), recoverable conditions | -| `logError()` | No | Errors that do NOT need alerting | -| `logIssue()` | **Yes** | System failures (5xx), unexpected errors that need investigation | +| Function | Creates Sentry Issue? | Use For | +| ------------ | --------------------- | ---------------------------------------------------------------- | +| `logDebug()` | No | Development debugging information | +| `logInfo()` | No | Routine operational information | +| `logWarn()` | No | Expected errors (4xx), recoverable conditions | +| `logError()` | No | Errors that do NOT need alerting | +| `logIssue()` | **Yes** | System failures (5xx), unexpected errors that need investigation | ### When to Use Each Function @@ -63,7 +63,7 @@ ApiError (base class) ```typescript // WRONG: Creates Sentry noise from user errors if (error instanceof ApiClientError) { - logIssue(error); // Creates unnecessary Sentry issue + logIssue(error); // Creates unnecessary Sentry issue } // CORRECT: Log without creating an issue @@ -79,7 +79,7 @@ if (error instanceof ApiClientError) { ```typescript // WRONG: Server errors go undetected if (error instanceof ApiServerError) { - logError(error); // No Sentry issue created + logError(error); // No Sentry issue created return formatError(error); } @@ -114,7 +114,7 @@ logError(error, { try { await apiService.fetchData(); } catch (error) { - logIssue(error); // Even 404s create issues + logIssue(error); // Even 404s create issues } // CORRECT: Discriminate by error type @@ -126,7 +126,7 @@ try { } else if (error instanceof ApiClientError) { logWarn(error, { loggerScope: ["api"] }); } else { - logIssue(error); // Unknown errors need investigation + logIssue(error); // Unknown errors need investigation } } ``` @@ -179,6 +179,7 @@ export async function handleTool(params: Params) { ``` The MCP server wrapper (`formatErrorForUser`) automatically: + - Formats `ApiClientError` as "Input Error" (no Sentry issue) - Formats `ApiServerError` as "Error" with Event ID (creates Sentry issue) - Formats `UserInputError` as "Input Error" (no Sentry issue) @@ -204,6 +205,7 @@ export function createMyTool(apiService: SentryApiService) { ``` The `agentTool()` wrapper automatically: + - Returns `{ result: data }` on success - Returns `{ error: "Input Error: ..." }` for `UserInputError` and `ApiClientError` - Returns `{ error: "Server Error: ..." }` with Event ID for `ApiServerError` @@ -285,14 +287,14 @@ When reviewing code for logging correctness, verify each item: ### Common Issues -| Pattern Found | Severity | Action | -|---------------|----------|--------| -| `logIssue(ApiClientError)` | High | Change to `logWarn()` | -| Missing `logIssue(ApiServerError)` | High | Add `logIssue()` call | -| Raw `console.error()` | Medium | Change to `logError()` or `logIssue()` | -| `captureException()` directly | Medium | Change to `logIssue()` | -| Missing `loggerScope` | Low | Add component identifier | -| Missing `contexts`/`extra` | Low | Add structured debugging info | +| Pattern Found | Severity | Action | +| ---------------------------------- | -------- | -------------------------------------- | +| `logIssue(ApiClientError)` | High | Change to `logWarn()` | +| Missing `logIssue(ApiServerError)` | High | Add `logIssue()` call | +| Raw `console.error()` | Medium | Change to `logError()` or `logIssue()` | +| `captureException()` directly | Medium | Change to `logIssue()` | +| Missing `loggerScope` | Low | Add component identifier | +| Missing `contexts`/`extra` | Low | Add structured debugging info | ## Reference Files diff --git a/.agents/skills/mcp-audit/SOURCES.md b/.agents/skills/mcp-audit/SOURCES.md index 9e449940e..c7b11d377 100644 --- a/.agents/skills/mcp-audit/SOURCES.md +++ b/.agents/skills/mcp-audit/SOURCES.md @@ -9,35 +9,35 @@ This file records the source material synthesized into `mcp-audit`. ## Current source inventory -| Source | Type | Trust tier | Retrieved | Confidence | Contribution | Usage constraints | Notes | -|---|---|---|---|---|---|---|---| -| `https://modelcontextprotocol.io/specification/2025-11-25/server/tools` | external spec | canonical | 2026-04-21 | high | Anchored tool-definition, annotation, tool-result, and task-support guidance | Released spec baseline, not draft | Primary tools source | -| `https://modelcontextprotocol.io/specification/2025-11-25/schema` | external schema | canonical | 2026-04-21 | high | Confirmed field-level wire shape for tool metadata and tool results | Released schema baseline, not draft | Used for precise field coverage | -| `https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle` | external spec | canonical | 2026-04-21 | high | Anchored initialize and initialized flow plus capability negotiation guidance | Released spec baseline, not draft | Lifecycle audit source | -| `https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization` | external spec | canonical | 2026-04-21 | high | Anchored protected-resource metadata, `WWW-Authenticate`, scope, and audience guidance | Applies to HTTP-capable servers, not pure `stdio` servers | Auth audit source | -| `https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices` | external guidance | canonical | 2026-04-21 | high | Added concrete security checks around local server compromise, DNS rebinding, SSRF-adjacent exposure, consent, and least privilege | Best-practices guidance, not a version-pinned normative spec | Security audit source | -| `https://modelcontextprotocol.io/specification/2025-11-25/server/index` | external spec | canonical | 2026-04-21 | high | Reinforced the control hierarchy across prompts, resources, and tools | Released spec baseline, not draft | Used for control-boundary guidance | -| `https://modelcontextprotocol.io/specification/2025-11-25/changelog` | external changelog | canonical | 2026-04-21 | high | Captured released-version deltas to watch for in older repos | Released changelog only | Used for compatibility watchpoints | -| `https://modelcontextprotocol.io/specification/draft/changelog` | external draft changelog | canonical | 2026-04-21 | medium | Captured draft-only watchpoints that should not be enforced as current released requirements | Draft only, do not enforce by default | Used for version-watchpoint separation | -| `docs/adding-tools.md` | repo doc | canonical | 2026-04-21 | high | Supplied local tool-count guidance and repo-specific metadata conventions layered on top of the spec | Repo-local semantics may differ elsewhere | Local compatibility source | -| `docs/quality-checks.md` | repo doc | canonical | 2026-04-21 | high | Confirmed that audits should refresh generated definitions and finish with repo validation when appropriate | Repo-local workflow requirement | Validation guidance source | -| `packages/mcp-core/src/server.test.ts` | local example | contextual | 2026-04-21 | high | Demonstrated why exported wire metadata should be verified, not just source declarations | Example only, not portable guidance | Informed real-surface validation guidance | -| `packages/mcp-core/src/tools/tools.test.ts` | local example | contextual | 2026-04-21 | high | Showed the shape of narrow structural checks without hardcoding a repo-specific inventory into the skill body | Example only, not portable guidance | Informed automation guidance | -| `skill-writer/references/mode-selection.md` | authoring guide | canonical | 2026-04-21 | high | Confirmed `workflow-process` as the right class for this skill | Applies to skill authoring, not audit execution | Guided class selection | -| `skill-writer/references/synthesis-path.md` | authoring guide | canonical | 2026-04-21 | high | Required broader source coverage, provenance, and depth gates before revising the skill | Applies to skill authoring, not audit execution | Guided synthesis coverage | -| `skill-writer/references/examples/workflow-process-skill.md` | authoring guide | contextual | 2026-04-21 | medium | Reinforced preconditions, ordered flow, safety boundaries, and failure handling as first-class artifacts | Example only | Used as structure prior art | +| Source | Type | Trust tier | Retrieved | Confidence | Contribution | Usage constraints | Notes | +| --------------------------------------------------------------------------------- | ------------------------ | ---------- | ---------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------- | +| `https://modelcontextprotocol.io/specification/2025-11-25/server/tools` | external spec | canonical | 2026-04-21 | high | Anchored tool-definition, annotation, tool-result, and task-support guidance | Released spec baseline, not draft | Primary tools source | +| `https://modelcontextprotocol.io/specification/2025-11-25/schema` | external schema | canonical | 2026-04-21 | high | Confirmed field-level wire shape for tool metadata and tool results | Released schema baseline, not draft | Used for precise field coverage | +| `https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle` | external spec | canonical | 2026-04-21 | high | Anchored initialize and initialized flow plus capability negotiation guidance | Released spec baseline, not draft | Lifecycle audit source | +| `https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization` | external spec | canonical | 2026-04-21 | high | Anchored protected-resource metadata, `WWW-Authenticate`, scope, and audience guidance | Applies to HTTP-capable servers, not pure `stdio` servers | Auth audit source | +| `https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices` | external guidance | canonical | 2026-04-21 | high | Added concrete security checks around local server compromise, DNS rebinding, SSRF-adjacent exposure, consent, and least privilege | Best-practices guidance, not a version-pinned normative spec | Security audit source | +| `https://modelcontextprotocol.io/specification/2025-11-25/server/index` | external spec | canonical | 2026-04-21 | high | Reinforced the control hierarchy across prompts, resources, and tools | Released spec baseline, not draft | Used for control-boundary guidance | +| `https://modelcontextprotocol.io/specification/2025-11-25/changelog` | external changelog | canonical | 2026-04-21 | high | Captured released-version deltas to watch for in older repos | Released changelog only | Used for compatibility watchpoints | +| `https://modelcontextprotocol.io/specification/draft/changelog` | external draft changelog | canonical | 2026-04-21 | medium | Captured draft-only watchpoints that should not be enforced as current released requirements | Draft only, do not enforce by default | Used for version-watchpoint separation | +| `docs/adding-tools.md` | repo doc | canonical | 2026-04-21 | high | Supplied local tool-count guidance and repo-specific metadata conventions layered on top of the spec | Repo-local semantics may differ elsewhere | Local compatibility source | +| `docs/quality-checks.md` | repo doc | canonical | 2026-04-21 | high | Confirmed that audits should refresh generated definitions and finish with repo validation when appropriate | Repo-local workflow requirement | Validation guidance source | +| `packages/mcp-core/src/server.test.ts` | local example | contextual | 2026-04-21 | high | Demonstrated why exported wire metadata should be verified, not just source declarations | Example only, not portable guidance | Informed real-surface validation guidance | +| `packages/mcp-core/src/tools/tools.test.ts` | local example | contextual | 2026-04-21 | high | Showed the shape of narrow structural checks without hardcoding a repo-specific inventory into the skill body | Example only, not portable guidance | Informed automation guidance | +| `skill-writer/references/mode-selection.md` | authoring guide | canonical | 2026-04-21 | high | Confirmed `workflow-process` as the right class for this skill | Applies to skill authoring, not audit execution | Guided class selection | +| `skill-writer/references/synthesis-path.md` | authoring guide | canonical | 2026-04-21 | high | Required broader source coverage, provenance, and depth gates before revising the skill | Applies to skill authoring, not audit execution | Guided synthesis coverage | +| `skill-writer/references/examples/workflow-process-skill.md` | authoring guide | contextual | 2026-04-21 | medium | Reinforced preconditions, ordered flow, safety boundaries, and failure handling as first-class artifacts | Example only | Used as structure prior art | ## Coverage matrix -| Dimension | Coverage status | Sources | Notes | -|---|---|---|---| -| Released spec baseline | complete | tools spec, schema, lifecycle, server overview | Latest released baseline confirmed as `2025-11-25` | -| Lifecycle and capability negotiation | complete | lifecycle spec, server overview | Includes initialize, initialized, claimed capabilities, and control hierarchy | -| Tools and structured output | complete | tools spec, schema, local server-test example | Expanded beyond hints into titles, icons, schemas, execution metadata, and tool-result semantics | -| Prompts and resources | complete | server overview plus released spec synthesis | Added explicit audit branches even when a repo only implements tools today | -| Transport, auth, and security | complete | authorization spec, security best practices | Separated `stdio` from HTTP-specific checks | -| Version and draft variance | complete | released changelog, draft changelog | Added watchpoints so the skill does not over-enforce unreleased behavior | -| Repo-local compatibility rules | complete | adding-tools, quality-checks, local tests | Kept the skill generic while preserving local generation and validation expectations | +| Dimension | Coverage status | Sources | Notes | +| ------------------------------------ | --------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| Released spec baseline | complete | tools spec, schema, lifecycle, server overview | Latest released baseline confirmed as `2025-11-25` | +| Lifecycle and capability negotiation | complete | lifecycle spec, server overview | Includes initialize, initialized, claimed capabilities, and control hierarchy | +| Tools and structured output | complete | tools spec, schema, local server-test example | Expanded beyond hints into titles, icons, schemas, execution metadata, and tool-result semantics | +| Prompts and resources | complete | server overview plus released spec synthesis | Added explicit audit branches even when a repo only implements tools today | +| Transport, auth, and security | complete | authorization spec, security best practices | Separated `stdio` from HTTP-specific checks | +| Version and draft variance | complete | released changelog, draft changelog | Added watchpoints so the skill does not over-enforce unreleased behavior | +| Repo-local compatibility rules | complete | adding-tools, quality-checks, local tests | Kept the skill generic while preserving local generation and validation expectations | ## Decisions diff --git a/.agents/skills/mcp-audit/references/checklist.md b/.agents/skills/mcp-audit/references/checklist.md index c13a9fc15..669108dd3 100644 --- a/.agents/skills/mcp-audit/references/checklist.md +++ b/.agents/skills/mcp-audit/references/checklist.md @@ -54,11 +54,13 @@ Use this checklist for repeatable MCP protocol and compatibility audits in any r 9. Treat orchestration tools conservatively based on the strongest child tool they can reach. 10. Treat conditional writes as mutating even if the write happens only on some code paths. 11. Verify safety hints: - - every tool defines `readOnlyHint` - - every tool defines `openWorldHint` - - every write-capable tool defines `destructiveHint` - - only truly repeatable no-extra-effect tools define `idempotentHint` - - read-only tools are never marked destructive + +- every tool defines `readOnlyHint` +- every tool defines `openWorldHint` +- every write-capable tool defines `destructiveHint` +- only truly repeatable no-extra-effect tools define `idempotentHint` +- read-only tools are never marked destructive + 12. Verify `structuredContent` matches `outputSchema` when `outputSchema` is declared. 13. Verify tool naming rules against the targeted spec revision. diff --git a/.agents/skills/mcp-qa/SKILL.md b/.agents/skills/mcp-qa/SKILL.md index f9e90e4ec..9b55fd1b4 100644 --- a/.agents/skills/mcp-qa/SKILL.md +++ b/.agents/skills/mcp-qa/SKILL.md @@ -77,7 +77,6 @@ that is not part of the tool response. Review raw tool output against and free of raw API JSON, internal implementation IDs, empty placeholders, and unrelated instructions. - ## 3. Real Agent Clients Use these when validating Claude Code, Codex, or behavior that only reproduces @@ -88,6 +87,7 @@ pnpm -w run build ``` What this verifies: + - The CLI is installed and can see the named MCP server - The provider can run a connectivity prompt against Sentry MCP - The final answer includes the authenticated email from `whoami` diff --git a/.agents/skills/testing-guidelines/SKILL.md b/.agents/skills/testing-guidelines/SKILL.md index ad5bef03e..82e9a4707 100644 --- a/.agents/skills/testing-guidelines/SKILL.md +++ b/.agents/skills/testing-guidelines/SKILL.md @@ -72,6 +72,7 @@ Tests are how we validate **ANY** functionality works before manual testing. ### Test Isolation Every test must: + - Run independently without affecting other tests - Use temporary directories for file operations - Clean up resources after completion diff --git a/.craft.yml b/.craft.yml index 9fe0df2f4..0b9618e15 100644 --- a/.craft.yml +++ b/.craft.yml @@ -1,4 +1,4 @@ -minVersion: '2.33.0' +minVersion: "2.33.0" workspaces: packages/cli: changelog: @@ -16,23 +16,23 @@ workspaces: config: artifacts: Build: - - '/^sentry-.*$/' - - 'npm-package' - - 'vercel' + - "/^sentry-.*$/" + - "npm-package" + - "vercel" targets: - name: npm includeNames: '/^sentry-\d.*\.tgz$/' - name: github - tagPrefix: 'cli@' + tagPrefix: "cli@" - name: vercel projectId: prj_wVkOnIwI3eIQbGD1Gw7qS1MqFFIK - name: registry apps: - 'app:sentry': - name: 'Sentry CLI' - packageUrl: 'https://www.npmjs.com/package/sentry' - mainDocsUrl: 'https://cli.sentry.dev' - urlTemplate: 'https://github.com/getsentry/toolkit/releases/download/cli@{{version}}/{{file}}' + "app:sentry": + name: "Sentry CLI" + packageUrl: "https://www.npmjs.com/package/sentry" + mainDocsUrl: "https://cli.sentry.dev" + urlTemplate: "https://github.com/getsentry/toolkit/releases/download/cli@{{version}}/{{file}}" includeNames: '/^sentry-.*\.gz$/' checksums: - algorithm: sha256 @@ -96,5 +96,5 @@ workspaces: - name: github includeNames: '/^sentry-mcp-server-\d.*\.tgz$/' - name: npm - id: '@sentry/mcp-server' + id: "@sentry/mcp-server" includeNames: '/^sentry-mcp-server-\d.*\.tgz$/' diff --git a/.github/workflows/cli-build.yml b/.github/workflows/cli-build.yml index d3174836e..fff8ae74c 100644 --- a/.github/workflows/cli-build.yml +++ b/.github/workflows/cli-build.yml @@ -182,7 +182,7 @@ jobs: version: ${{ needs.nightly-version.outputs.version }} registry: ghcr.io repo: getsentry/toolkit - binary-glob: 'sentry-*' + binary-glob: "sentry-*" new-binaries-dir: new-binaries new-gz-dir: new-binaries patches-dir: patches @@ -232,7 +232,7 @@ jobs: version: ${{ needs.nightly-version.outputs.version }} registry: ghcr.io repo: getsentry/toolkit - binary-glob: 'sentry-*' + binary-glob: "sentry-*" artifacts-dir: artifacts binaries-dir: binaries patches-dir: patches diff --git a/.github/workflows/merge-jobs.yml b/.github/workflows/merge-jobs.yml index b60c43073..c021908dc 100644 --- a/.github/workflows/merge-jobs.yml +++ b/.github/workflows/merge-jobs.yml @@ -19,8 +19,8 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version-file: 'package.json' - cache: 'pnpm' + node-version-file: "package.json" + cache: "pnpm" - name: Install dependencies run: pnpm install diff --git a/.github/workflows/pr-risk-labels-test.yml b/.github/workflows/pr-risk-labels-test.yml index eae41c54b..e3c082354 100644 --- a/.github/workflows/pr-risk-labels-test.yml +++ b/.github/workflows/pr-risk-labels-test.yml @@ -24,5 +24,5 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: - node-version: '24.18.0' + node-version: "24.18.0" - run: node --test .github/tests/pr-risk-labels.test.mjs diff --git a/.oxfmtrc.json b/.oxfmtrc.json index d0950ee41..7bf3ff175 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -4,6 +4,10 @@ "tabWidth": 2, "sortPackageJson": false, "ignorePatterns": [ + ".agents/skills/openspec-*/**", + ".claude/commands/opsx/**", + ".codex/skills/openspec-*/**", + ".cursor/commands/opsx-*.md", "apps/cli-docs/**", "packages/cli/plugins/**", "packages/cli/README.md", diff --git a/TELEMETRY.md b/TELEMETRY.md index df8b2dc38..96f0b4c4f 100644 --- a/TELEMETRY.md +++ b/TELEMETRY.md @@ -12,16 +12,16 @@ the pivots and recipes below. ## Where To Query -| Starting Point | Query Surface | Pivot | Answers | Next Step | -| -------------- | ------------- | ----- | ------- | --------- | -| `trace_id` from an issue, span, or log | Sentry Traces and Logs | `span_id` | full request/tool timeline and failing span | inspect child spans and logs | -| Sentry `event_id` | Sentry Issue/Event | `trace_id`, `http.route`, `gen_ai.tool.name` | exception context and owning request/tool | query trace logs | -| HTTP route or status symptom | Sentry Metrics and Logs | `http.route`, `http.response.status_code` | route volume, status mix, local rate limits | inspect matching traces | -| OAuth sign-out or refresh symptom | Sentry Metrics and Logs | `app.client.family`, `app.access.*`, `app.oauth.*` metrics | refresh outcome, revoked grants, client family | inspect user trace or request logs | -| MCP tool name | Sentry Spans and Issues | `gen_ai.tool.name` | failing or slow tool calls | inspect tool span and Sentry API spans | -| Sentry resource URL/type | Sentry Spans | `app.resource.type` | `get_sentry_resource` dispatch behavior | inspect resolved type and downstream tool | -| Agent/model/token symptom | Sentry Spans | `gen_ai.*` | provider, model, token, and agent behavior | inspect agent and tool spans | -| Client or transport symptom | Sentry Logs, Spans, Metrics | `app.transport`, `app.client.family`, `user_agent.original` | stdio vs HTTP, client bucket, and request family | compare route or OAuth metrics | +| Starting Point | Query Surface | Pivot | Answers | Next Step | +| -------------------------------------- | --------------------------- | ----------------------------------------------------------- | ------------------------------------------------ | ----------------------------------------- | +| `trace_id` from an issue, span, or log | Sentry Traces and Logs | `span_id` | full request/tool timeline and failing span | inspect child spans and logs | +| Sentry `event_id` | Sentry Issue/Event | `trace_id`, `http.route`, `gen_ai.tool.name` | exception context and owning request/tool | query trace logs | +| HTTP route or status symptom | Sentry Metrics and Logs | `http.route`, `http.response.status_code` | route volume, status mix, local rate limits | inspect matching traces | +| OAuth sign-out or refresh symptom | Sentry Metrics and Logs | `app.client.family`, `app.access.*`, `app.oauth.*` metrics | refresh outcome, revoked grants, client family | inspect user trace or request logs | +| MCP tool name | Sentry Spans and Issues | `gen_ai.tool.name` | failing or slow tool calls | inspect tool span and Sentry API spans | +| Sentry resource URL/type | Sentry Spans | `app.resource.type` | `get_sentry_resource` dispatch behavior | inspect resolved type and downstream tool | +| Agent/model/token symptom | Sentry Spans | `gen_ai.*` | provider, model, token, and agent behavior | inspect agent and tool spans | +| Client or transport symptom | Sentry Logs, Spans, Metrics | `app.transport`, `app.client.family`, `user_agent.original` | stdio vs HTTP, client bucket, and request family | compare route or OAuth metrics | ## Event Validation Diagnostics @@ -32,44 +32,44 @@ Read it alongside `validationIssues`; another field may have failed. ## Investigation Pivots -| Pivot | Meaning | Found In | First Query | -| ----- | ------- | -------- | ----------- | -| `trace_id` | one request, tool call, or agent run trace | issues, logs, spans | open trace | -| `span_id` | one request, tool, model, or API span | logs, spans | inspect span | -| `event_id` | captured Sentry error | Sentry issue/event | open event | -| `user.id` | authenticated Sentry user ID | events, logs, metrics | user request or OAuth history | -| `http.route` | normalized Cloudflare route template | metrics, logs, spans | route health | -| `http.response.status_code` | final HTTP response code | metrics, logs, spans | response distribution | -| `app.response.reason` | application local response reason | metrics | local rate-limit diagnosis | -| `app.rate_limit.scope` | local rate-limit scope | metrics | IP vs user rate limits | -| `app.route.group` | coarse route family | metrics | `mcp`, `oauth`, `chat`, `search` | -| `app.transport` | MCP transport | tags, spans | `http`, `sse`, or `stdio` | -| `app.server.mode.experimental` | experimental-mode flag | metrics, spans, tags | `?experimental=1` or stdio `--experimental` adoption | -| `mcp.session.id` | MCP session identity | spans | session timeline | -| `gen_ai.tool.name` | MCP tool being called | spans, issues | tool timeline | -| `mcp.tool.name` | canonical MCP tool name on the span | spans | tool timeline for renamed tools | -| `gen_ai.tool.call.result` | JSON tool result payload | spans | returned tool output | -| `gen_ai.tool.call.result.count` | number of results returned by a tool | spans | zero-result tool calls | -| `app.resource.type` | resolved Sentry resource type | spans | resource dispatch | -| `app.constraint.organization_slug` | active organization constraint | spans | constrained session behavior | -| `app.constraint.project_slug` | active project constraint | spans | constrained session behavior | -| `gen_ai.tool.call.arguments.` | effective tool arguments | spans | called tool input | -| `app.client.family` | bucketed MCP client family | metrics, spans | client-specific OAuth behavior | -| `app.access.method` | upstream access method | spans | `mcp_grant` or `sentry_access` | -| `app.client.registration.method` | CIMD vs DCR client registration method | metrics, auth spans | `cimd`, `dcr`, or `unknown` on auth paths only | -| `app.access.refresh.outcome` | OAuth refresh outcome | metrics | token refresh diagnosis | -| `app.access.grant.revoked_reason` | wrapper grant revoke reason | metrics | sign-out diagnosis | -| `app.consent.skill` | skill granted during approval | metrics | per-skill adoption | -| `app.consent.skill..granted` | skill granted on an MCP request | spans | tool behavior by enabled skills | -| `app.access.probe.status_code` | upstream probe HTTP status | metrics | Sentry token validity probe result | -| `app.access.probe.reason` | indeterminate probe bucket | metrics | upstream instability | -| `app.upstream.host` | configured Sentry host | tags, spans | host-specific behavior | -| `app.server.version` | MCP server package version | tags, spans | release/version behavior | -| `app.utm_source` | sanitized `X-Sentry-Utm-Source` header or `utm_source` query param | spans | in-product / plugin attribution | -| `app.referrer.family` | low-cardinality bucket of the `Referer` host | spans | external traffic attribution | -| `gen_ai.provider.name` | GenAI provider | spans, tags | provider-specific model behavior | -| `gen_ai.request.model` | requested GenAI model | spans | model-specific behavior | -| `user_agent.original` | original HTTP user agent | request data, spans | client identification | +| Pivot | Meaning | Found In | First Query | +| ----------------------------------- | ------------------------------------------------------------------ | --------------------- | ---------------------------------------------------- | +| `trace_id` | one request, tool call, or agent run trace | issues, logs, spans | open trace | +| `span_id` | one request, tool, model, or API span | logs, spans | inspect span | +| `event_id` | captured Sentry error | Sentry issue/event | open event | +| `user.id` | authenticated Sentry user ID | events, logs, metrics | user request or OAuth history | +| `http.route` | normalized Cloudflare route template | metrics, logs, spans | route health | +| `http.response.status_code` | final HTTP response code | metrics, logs, spans | response distribution | +| `app.response.reason` | application local response reason | metrics | local rate-limit diagnosis | +| `app.rate_limit.scope` | local rate-limit scope | metrics | IP vs user rate limits | +| `app.route.group` | coarse route family | metrics | `mcp`, `oauth`, `chat`, `search` | +| `app.transport` | MCP transport | tags, spans | `http`, `sse`, or `stdio` | +| `app.server.mode.experimental` | experimental-mode flag | metrics, spans, tags | `?experimental=1` or stdio `--experimental` adoption | +| `mcp.session.id` | MCP session identity | spans | session timeline | +| `gen_ai.tool.name` | MCP tool being called | spans, issues | tool timeline | +| `mcp.tool.name` | canonical MCP tool name on the span | spans | tool timeline for renamed tools | +| `gen_ai.tool.call.result` | JSON tool result payload | spans | returned tool output | +| `gen_ai.tool.call.result.count` | number of results returned by a tool | spans | zero-result tool calls | +| `app.resource.type` | resolved Sentry resource type | spans | resource dispatch | +| `app.constraint.organization_slug` | active organization constraint | spans | constrained session behavior | +| `app.constraint.project_slug` | active project constraint | spans | constrained session behavior | +| `gen_ai.tool.call.arguments.` | effective tool arguments | spans | called tool input | +| `app.client.family` | bucketed MCP client family | metrics, spans | client-specific OAuth behavior | +| `app.access.method` | upstream access method | spans | `mcp_grant` or `sentry_access` | +| `app.client.registration.method` | CIMD vs DCR client registration method | metrics, auth spans | `cimd`, `dcr`, or `unknown` on auth paths only | +| `app.access.refresh.outcome` | OAuth refresh outcome | metrics | token refresh diagnosis | +| `app.access.grant.revoked_reason` | wrapper grant revoke reason | metrics | sign-out diagnosis | +| `app.consent.skill` | skill granted during approval | metrics | per-skill adoption | +| `app.consent.skill..granted` | skill granted on an MCP request | spans | tool behavior by enabled skills | +| `app.access.probe.status_code` | upstream probe HTTP status | metrics | Sentry token validity probe result | +| `app.access.probe.reason` | indeterminate probe bucket | metrics | upstream instability | +| `app.upstream.host` | configured Sentry host | tags, spans | host-specific behavior | +| `app.server.version` | MCP server package version | tags, spans | release/version behavior | +| `app.utm_source` | sanitized `X-Sentry-Utm-Source` header or `utm_source` query param | spans | in-product / plugin attribution | +| `app.referrer.family` | low-cardinality bucket of the `Referer` host | spans | external traffic attribution | +| `gen_ai.provider.name` | GenAI provider | spans, tags | provider-specific model behavior | +| `gen_ai.request.model` | requested GenAI model | spans | model-specific behavior | +| `user_agent.original` | original HTTP user agent | request data, spans | client identification | ## Query Recipes @@ -236,7 +236,7 @@ attached to ordinary `/mcp` request spans. HTTPS URL `client_id` values map to Named under `app.client.*` (not `app.oauth.*`) so default Sentry data scrubbing does not redact values via the `auth` substring in `oauth`. -Diagnostic OAuth *attributes* use the readable `app.access.*` namespace for +Diagnostic OAuth _attributes_ use the readable `app.access.*` namespace for the OAuth access lifecycle. This avoids scrubbed substrings (`auth`/`oauth`, `token`, `bearer`, `credentials`); error/reason/method buckets avoid them too. Metric names remain under `app.oauth.*`. @@ -284,14 +284,14 @@ Attributes: `gen_ai.provider.name`, `gen_ai.request.model`, ## Configuration -| Setting | Controls | Default | -| ------- | -------- | ------- | -| `VITE_SENTRY_DSN` | Cloudflare Sentry telemetry | disabled when unset | -| `VITE_SENTRY_ENVIRONMENT` | Cloudflare Sentry environment | development | -| `SENTRY_DSN` | stdio and Node telemetry | disabled when unset | -| `SENTRY_RELEASE` | stdio release tag | unset | -| `SENTRY_HOST` | upstream Sentry host and `app.upstream.host` | `sentry.io` | -| `NODE_ENV` | stdio environment | production fallback in packaged runtime | +| Setting | Controls | Default | +| ------------------------- | -------------------------------------------- | --------------------------------------- | +| `VITE_SENTRY_DSN` | Cloudflare Sentry telemetry | disabled when unset | +| `VITE_SENTRY_ENVIRONMENT` | Cloudflare Sentry environment | development | +| `SENTRY_DSN` | stdio and Node telemetry | disabled when unset | +| `SENTRY_RELEASE` | stdio release tag | unset | +| `SENTRY_HOST` | upstream Sentry host and `app.upstream.host` | `sentry.io` | +| `NODE_ENV` | stdio environment | production fallback in packaged runtime | ## Attribute Notes diff --git a/docs/cloudflare/architecture.md b/docs/cloudflare/architecture.md index 122e9ed12..51b28889e 100644 --- a/docs/cloudflare/architecture.md +++ b/docs/cloudflare/architecture.md @@ -45,6 +45,7 @@ export default new Hono() ``` **Features:** + - OAuth 2.0 flow with Sentry - Token storage in Cloudflare KV - Automatic token refresh @@ -59,12 +60,13 @@ React-based chat UI with real-time streaming: export function Chat() { const { messages, handleSubmit } = useChat({ api: "/api/chat", - headers: { Authorization: `Bearer ${authToken}` } + headers: { Authorization: `Bearer ${authToken}` }, }); } ``` **Features:** + - Message streaming with Vercel AI SDK - Tool call visualization - Slash commands (/help, /prompts, /clear) @@ -82,12 +84,13 @@ const mcpClient = await experimental_createMCPClient({ transport: { type: "sse", url: sseUrl, - headers: { Authorization: `Bearer ${accessToken}` } - } + headers: { Authorization: `Bearer ${accessToken}` }, + }, }); ``` **Features:** + - Server-sent events (SSE) for MCP communication - Automatic tool discovery - Prompt metadata endpoint @@ -102,11 +105,12 @@ const result = streamText({ model: openai("gpt-4o"), messages: processedMessages, tools: mcpTools, - system: "You are an AI assistant for testing Sentry MCP..." + system: "You are an AI assistant for testing Sentry MCP...", }); ``` **Features:** + - Streaming responses - Tool execution - Prompt template processing @@ -115,11 +119,13 @@ const result = streamText({ ## Data Flow 1. **User Authentication**: + ``` User → OAuth Login → Sentry → OAuth Callback → KV Storage ``` 2. **Chat Message Flow**: + ``` User Input → Chat API → Process Prompts → AI Model → Stream Response ↓ diff --git a/docs/cloudflare/oauth-architecture.md b/docs/cloudflare/oauth-architecture.md index 2e456d58e..bf05b71a7 100644 --- a/docs/cloudflare/oauth-architecture.md +++ b/docs/cloudflare/oauth-architecture.md @@ -5,6 +5,7 @@ The Sentry MCP implementation involves **two completely separate OAuth providers**: ### 1. MCP OAuth Provider (Our Server) + - **What it is**: Our own OAuth 2.0 server built with `@cloudflare/workers-oauth-provider` - **Purpose**: Authenticates MCP clients (like Cursor, VS Code, etc.) - **Tokens issued**: MCP access tokens and MCP refresh tokens @@ -12,6 +13,7 @@ The Sentry MCP implementation involves **two completely separate OAuth providers - **Endpoints**: `/oauth/register`, `/oauth/authorize`, `/oauth/token` ### 2. Sentry OAuth Provider (Sentry's Server) + - **What it is**: Sentry's official OAuth 2.0 server at `sentry.io` - **Purpose**: Authenticates users and grants API access to Sentry - **Tokens issued**: Sentry access tokens and Sentry refresh tokens @@ -21,6 +23,7 @@ The Sentry MCP implementation involves **two completely separate OAuth providers ## High-Level Flow The system uses a dual-token approach: + 1. **MCP clients** authenticate with **MCP OAuth** to get MCP tokens 2. **MCP OAuth** authenticates with **Sentry OAuth** to get Sentry tokens 3. **MCP tokens** contain encrypted **Sentry tokens** in their payload @@ -80,17 +83,18 @@ sequenceDiagram ### Token Types -| Token Type | Issued By | Used By | Contains | Purpose | -|------------|-----------|---------|----------|----------| -| **MCP Access Token** | MCP OAuth Provider | MCP Clients | Encrypted Sentry tokens | Authenticate to MCP Server | -| **MCP Refresh Token** | MCP OAuth Provider | MCP Clients | Grant reference | Refresh MCP access tokens | -| **Sentry Access Token** | Sentry OAuth | MCP Server | User credentials | Call Sentry API | -| **Sentry Refresh Token** | Sentry OAuth | MCP OAuth Provider | Refresh credentials | Refresh Sentry tokens | -| **Explicit Sentry API Token** | External client or provider | MCP Server | Opaque Sentry API token | Direct remote auth with `Sentry-Bearer` | +| Token Type | Issued By | Used By | Contains | Purpose | +| ----------------------------- | --------------------------- | ------------------ | ----------------------- | --------------------------------------- | +| **MCP Access Token** | MCP OAuth Provider | MCP Clients | Encrypted Sentry tokens | Authenticate to MCP Server | +| **MCP Refresh Token** | MCP OAuth Provider | MCP Clients | Grant reference | Refresh MCP access tokens | +| **Sentry Access Token** | Sentry OAuth | MCP Server | User credentials | Call Sentry API | +| **Sentry Refresh Token** | Sentry OAuth | MCP OAuth Provider | Refresh credentials | Refresh Sentry tokens | +| **Explicit Sentry API Token** | External client or provider | MCP Server | Opaque Sentry API token | Direct remote auth with `Sentry-Bearer` | ### Not a Simple Proxy **Important**: MCP is NOT an HTTP proxy that forwards requests. Instead: + - MCP implements the **Model Context Protocol** (tools, prompts, resources) - Clients send MCP protocol messages, not HTTP requests - MCP Server executes these commands using Sentry's API @@ -163,9 +167,9 @@ const oAuthProvider = new OAuthProvider({ "/sse": createMcpHandler("/sse", true), "/mcp": createMcpHandler("/mcp", false), }, - defaultHandler: app, // Hono app for non-OAuth routes + defaultHandler: app, // Hono app for non-OAuth routes authorizeEndpoint: "/oauth/authorize", - tokenEndpoint: "/oauth/token", + tokenEndpoint: "/oauth/token", clientRegistrationEndpoint: "/oauth/register", clientIdMetadataDocumentEnabled: true, clientIdMetadataDocuments: CLIENT_ID_METADATA_DOCUMENTS, @@ -218,6 +222,7 @@ The `apiHandler` is a protected endpoint that requires valid OAuth tokens: - `/mcp` - MCP protocol endpoint (HTTP transport) The handler receives: + - `request`: The incoming request - `env`: Cloudflare environment bindings - `ctx`: Execution context with `ctx.props` containing decrypted user data @@ -228,15 +233,15 @@ MCP tokens contain encrypted properties including Sentry tokens: ```typescript interface WorkerProps { - id: string; // Sentry user ID - accessToken: string; // Sentry access token - refreshToken?: string; // Sentry refresh token - accessTokenExpiresAt?: number; // Cached validity deadline; extended after probes - sessionStartedAt?: number; // MCP grant creation timestamp - upstreamExpiresAt?: number; // Original Sentry expiry timestamp - clientId: string; // MCP client ID - scope: string; // MCP permissions granted - grantedSkills?: string[]; // Skills granted (primary authorization) + id: string; // Sentry user ID + accessToken: string; // Sentry access token + refreshToken?: string; // Sentry refresh token + accessTokenExpiresAt?: number; // Cached validity deadline; extended after probes + sessionStartedAt?: number; // MCP grant creation timestamp + upstreamExpiresAt?: number; // Original Sentry expiry timestamp + clientId: string; // MCP client ID + scope: string; // MCP permissions granted + grantedSkills?: string[]; // Skills granted (primary authorization) // grantedScopes is deprecated and will be removed Jan 1, 2026 } ``` @@ -256,6 +261,7 @@ The MCP server needs to support URL-based constraints like `/mcp/sentry/javascri The MCP handler parses URL path segments to extract organization and project constraints: **Example URLs:** + - `/mcp` - No constraints (full access within granted scopes) - `/mcp/sentry` - Organization constraint (limited to "sentry" org) - `/mcp/sentry/javascript` - Organization + project constraints @@ -280,17 +286,17 @@ When a user completes the full OAuth flow, the MCP OAuth Provider stores Sentry const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({ // ... other params props: { - id: payload.user.id, // From Sentry - accessToken: payload.access_token, // Sentry's access token - refreshToken: payload.refresh_token, // Sentry's refresh token + id: payload.user.id, // From Sentry + accessToken: payload.access_token, // Sentry's access token + refreshToken: payload.refresh_token, // Sentry's refresh token accessTokenExpiresAt: Date.now() + payload.expires_in * 1000, sessionStartedAt: Date.now(), upstreamExpiresAt: Date.now() + payload.expires_in * 1000, - clientId: oauthReqInfo.clientId, // MCP client ID - scope: oauthReqInfo.scope.join(" "), // MCP scopes - grantedSkills: Array.from(validSkills), // Skills granted (primary authorization) + clientId: oauthReqInfo.clientId, // MCP client ID + scope: oauthReqInfo.scope.join(" "), // MCP scopes + grantedSkills: Array.from(validSkills), // Skills granted (primary authorization) // ... other fields - } + }, }); ``` @@ -322,7 +328,12 @@ When an MCP client's token expires: ```typescript // tokenExchangeCallback in src/server/oauth/helpers.ts -export async function tokenExchangeCallback(options, env, request, clientFamily) { +export async function tokenExchangeCallback( + options, + env, + request, + clientFamily, +) { // Only handle MCP refresh_token requests if (options.grantType !== "refresh_token") { return undefined; @@ -337,7 +348,7 @@ export async function tokenExchangeCallback(options, env, request, clientFamily) if (sentryTokenExpiresAt && Number.isFinite(sentryTokenExpiresAt)) { const remainingMs = sentryTokenExpiresAt - Date.now(); const SAFE_WINDOW_MS = 2 * 60 * 1000; // 2 minutes safety - + if (remainingMs > SAFE_WINDOW_MS) { // Sentry token still valid - return new MCP token with cached Sentry token return { @@ -462,6 +473,7 @@ The MCP Server then uses the Sentry access token from context to make Sentry API ### Why Not Direct Sentry OAuth? If MCP clients used Sentry OAuth directly: + - Clients would need to manage Sentry token lifetime and re-authentication directly - No way to add MCP-specific permissions - Clients would have raw Sentry API access (security risk) @@ -470,6 +482,7 @@ If MCP clients used Sentry OAuth directly: ### Implementation Complexity The MCP OAuth Provider (via `@cloudflare/workers-oauth-provider`) provides: + - OAuth 2.0 authorization flows - Dynamic client registration - Token issuance and validation diff --git a/docs/cloudflare/overview.md b/docs/cloudflare/overview.md index d5256b4c3..d29dd0d68 100644 --- a/docs/cloudflare/overview.md +++ b/docs/cloudflare/overview.md @@ -7,13 +7,14 @@ This directory contains documentation for the Cloudflare-hosted web chat applica The Cloudflare chat application (`packages/mcp-cloudflare`) is a **separate web application** that demonstrates how to build a chat interface using MCP. It is not part of the MCP protocol or server itself. Think of it as: + - **MCP Server**: The backend service that provides Sentry functionality via the Model Context Protocol - **Cloudflare Chat**: A frontend web app (like ChatGPT) that connects to and uses the MCP server ## What This Application Provides - Web-based chat UI with OAuth authentication -- AI-powered assistant using OpenAI's GPT-4 +- AI-powered assistant using OpenAI's GPT-4 - Integration with Sentry MCP tools via HTTP transport - Cloudflare Workers deployment for global edge hosting diff --git a/docs/contributing/adding-tools.md b/docs/contributing/adding-tools.md index bfce2fd22..da9f780cd 100644 --- a/docs/contributing/adding-tools.md +++ b/docs/contributing/adding-tools.md @@ -20,6 +20,7 @@ We also expect upstream consumers (Claude Code plugins, Cursor, etc.) to use **t Target ~20 publicly visible tools. Never exceed 25. AI agents have limited tool slots (Cursor caps at 45 total across all providers), so Sentry MCP cannot consume all available slots. Before adding a new tool, consider if it could be: + 1. Combined with an existing tool 2. Implemented as a parameter variant 3. Added to the searchable catalog instead of the top-level MCP surface @@ -45,6 +46,7 @@ Do not add search-only summaries or catalog-only schemas. `search_sentry_tools` ## Tool Structure Each tool consists of: + 1. **Tool Module** - Single file in `src/tools/catalog/your-tool-name.ts` with definition and handler 2. **Tests** - Unit tests in `src/tools/catalog/your-tool-name.test.ts`, including a baseline happy-path inline snapshot 3. **Mocks** - API responses in `mcp-server-mocks` @@ -107,6 +109,7 @@ an absent hint is a silent gap. Read-only tools must set `destructiveHint: false (not leave it undefined). **Available annotations:** + - `readOnlyHint` (boolean): Tool doesn't modify data - `destructiveHint` (boolean): Tool may modify/delete existing data - `idempotentHint` (boolean, optional): Repeated calls with same arguments have no additional effect @@ -141,6 +144,7 @@ annotations: { ### Writing LLM-Friendly Descriptions Critical for LLM success: + - Start with "Use this tool when you need to:" - Include concrete examples - Reference related tools @@ -173,7 +177,7 @@ async handler(params, context: ServerContext) { // 5. Format response let output = `# Results in **${params.organizationSlug}**\n\n`; - + if (data.length === 0) { return output + "No results found.\n"; } @@ -222,9 +226,9 @@ describe("your_tool_name", () => { accessToken: "test-token", userId: "1", organizationSlug: null, - } + }, ); - + expect(result).toMatchInlineSnapshot(` "# Results in **test-org** @@ -235,6 +239,7 @@ describe("your_tool_name", () => { ``` **Testing Requirements:** + - Input validation (see [../testing/overview.md](../testing/overview.md#testing-error-cases)) - Error handling (use patterns from [error-handling.md](error-handling.md)) - Output formatting with snapshots @@ -246,6 +251,7 @@ describe("your_tool_name", () => { - API integration with MSW mocks **After changing output, update snapshots:** + ```bash cd packages/mcp-server pnpm vitest --run -u @@ -264,7 +270,7 @@ In `packages/mcp-server-mocks/src/handlers/`: if (!params.org) { return HttpResponse.json("Invalid org", { status: 400 }); } - + // Return fixture return HttpResponse.json(yourDataFixture); } @@ -282,7 +288,7 @@ describeEval("your-tool", { data: async () => [ { input: `Primary use case in ${FIXTURES.organizationSlug}`, - expected: "Expected response" + expected: "Expected response", }, // Maximum 2-3 scenarios! ], @@ -359,7 +365,7 @@ async handler(params, context) { sort: params.sort, statsPeriod: params.period, }; - + // Tool executes either the repaired request or the direct parameters. const results = await apiService.searchEvents({ query: request.query, @@ -368,7 +374,7 @@ async handler(params, context) { sort: request.sort, statsPeriod: request.statsPeriod, }); - + return formatResults(results); } ``` @@ -390,6 +396,7 @@ of hiding them behind an un-repaired direct search. ### Error Handling Philosophy **DO NOT retry internally**. When the embedded agent fails: + 1. Throw a clear `UserInputError` or `LLMProviderError` with specific guidance 2. Let the calling agent (Claude/Cursor) see the error 3. The calling agent can retry with corrections if needed @@ -463,13 +470,13 @@ The `search_docs` tool demonstrates this pattern: // In the Worker (routes/search.ts) export default new Hono().post("/", async (c) => { const { query, maxResults } = await c.req.json(); - + // Access Worker-specific bindings const results = await c.env.AI.autorag("sentry-docs").aiSearch({ query, max_num_results: maxResults, }); - + return c.json({ results }); }); @@ -478,15 +485,15 @@ search_docs: async (context, params) => { const response = await fetch(`${context.host}/api/search`, { method: "POST", headers: { - "Authorization": `Bearer ${context.accessToken}`, + Authorization: `Bearer ${context.accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify(params), }); - + const data = await response.json(); // Format and return results -} +}; ``` This pattern works with both Cloudflare-hosted and stdio transports. diff --git a/docs/contributing/common-patterns.md b/docs/contributing/common-patterns.md index bb22f7a0c..dda599ec0 100644 --- a/docs/contributing/common-patterns.md +++ b/docs/contributing/common-patterns.md @@ -16,7 +16,9 @@ Define once, use everywhere: export const ParamOrganizationSlug = z .string() .trim() - .describe("The organization's slug. You can find a list using the `find_organizations()` tool."); + .describe( + "The organization's slug. You can find a list using the `find_organizations()` tool.", + ); export const ParamRegionUrl = z .string() @@ -31,13 +33,15 @@ See: `packages/mcp-core/src/schema.ts` ```typescript // Support multiple ID formats -z.union([z.string(), z.number()]) +z.union([z.string(), z.number()]); // Optional with transforms -z.string().optional().transform(val => val?.trim()) +z.string() + .optional() + .transform((val) => val?.trim()); // Partial objects with passthrough -IssueSchema.partial().passthrough() +IssueSchema.partial().passthrough(); ``` ### Type Derivation @@ -92,9 +96,9 @@ return { { uri: url.toString(), mimeType: "application/json", - text: JSON.stringify(data, null, 2) - } - ] + text: JSON.stringify(data, null, 2), + }, + ], }; ``` @@ -105,7 +109,7 @@ return { ```typescript if (!params.requiredParam) { throw new UserInputError( - "Required parameter is missing. Please provide requiredParam." + "Required parameter is missing. Please provide requiredParam.", ); } ``` @@ -119,7 +123,7 @@ if (params.issueUrl) { // Use direct parameters } else { throw new UserInputError( - "Either issueUrl or both organizationSlug and issueId must be provided" + "Either issueUrl or both organizationSlug and issueId must be provided", ); } ``` @@ -136,7 +140,7 @@ type ZodifyRecord> = { // Const assertions for literal types export const TOOL_NAMES = ["tool1", "tool2"] as const; -export type ToolName = typeof TOOL_NAMES[number]; +export type ToolName = (typeof TOOL_NAMES)[number]; ``` ## References diff --git a/docs/contributing/error-handling.md b/docs/contributing/error-handling.md index 2c3485c4b..8202ba6c2 100644 --- a/docs/contributing/error-handling.md +++ b/docs/contributing/error-handling.md @@ -10,7 +10,7 @@ This document describes how errors are handled throughout the MCP server tool sy ApiError (base class) ├─ ApiClientError (4xx - user errors, NOT sent to Sentry) │ ├─ ApiPermissionError (403) -│ ├─ ApiNotFoundError (404) +│ ├─ ApiNotFoundError (404) │ ├─ ApiValidationError (400, 422) │ ├─ ApiAuthenticationError (401) │ └─ ApiRateLimitError (429) @@ -18,7 +18,8 @@ ApiError (base class) ``` **Key Method:** -- `ApiClientError.toUserMessage()` - Returns `"API error (status): message"` + +- `ApiClientError.toUserMessage()` - Returns `"API error (status): message"` - For 404s with generic messages, adds: "Please verify that the organization, project, or resource ID is correct and that you have access to it." - For 404s with specific messages, adds: "Please verify the parameters are correct." @@ -44,6 +45,7 @@ ApiError (base class) - 5xx / network errors (provider down) → Also converted to `LLMProviderError` so tools degrade gracefully instead of flooding issues **Conversion Flow:** + - `callEmbeddedAgent` converts provider `APICallError` (4xx, 5xx, and missing status) → `LLMProviderError` immediately after the AI SDK call - `callEmbeddedAgent` converts unexpected agent failures (including `NoOutputGeneratedError`) → `logIssue` once + `AgentExecutionError` - Defensive handling in `handleAgentToolError` and `formatErrorForUser` for any that slip through @@ -53,6 +55,7 @@ ApiError (base class) ### Error Categories **User-Facing / Expected Errors (Should NOT create Sentry issues):** + - All `ApiClientError` subclasses - `UserInputError` - `ConfigurationError` (except HTTP transport, which notifies operators once via `logIssue` with a generic user message) @@ -60,6 +63,7 @@ ApiError (base class) - `APICallError` from the upstream AI provider (converted to `LLMProviderError` / logged as warnings) **System Errors (Should be captured by Sentry):** + - `ApiServerError` - Unexpected runtime errors outside the AI provider boundary - Unexpected embedded-agent failures (`AgentExecutionError` / `NoOutputGeneratedError`) — captured once in `callEmbeddedAgent`, then handled gracefully downstream @@ -67,35 +71,44 @@ ApiError (base class) ## Critical Principles ### 1. Let Errors Bubble Up Naturally + **API errors should bubble up naturally to the appropriate handler.** The API client throws properly typed errors that are caught at the right level: + - In MCP tools → Bubble up to MCP server wrapper → `formatErrorForUser` - In embedded agent tools → Caught by `agentTool` → Formatted for AI ### 2. Typed Error Handling + **The API client uses a factory pattern (`createApiError`) to create properly typed errors:** + - 4xx → `ApiClientError` subclass (ApiPermissionError, ApiNotFoundError, etc.) - 5xx → `ApiServerError` - Each error type has specific behaviors and helper methods ### 3. **SECURITY CRITICAL** - Trusted Error Messages Only + **🚨 NEVER return untrusted error messages to AI agents - this creates prompt injection vulnerabilities.** In our system, we ONLY return trusted error messages from: + - **Sentry API responses** (trusted - Sentry controls these messages) - **Our own validation errors** (`UserInputError` - we control the message content) - **Pre-formatted system messages** (hardcoded error templates we control) **Why this matters:** + - AI agents receive error messages as part of their context - Malicious error messages could contain prompt injection attacks - Untrusted input could manipulate agent behavior or extract sensitive information **What we trust:** + - Sentry's API error messages (via `ApiClientError.toUserMessage()`) - Our own `UserInputError` messages (application-controlled) - System-generated error templates with Event IDs **What we DON'T trust:** + - User-provided input in error scenarios (never directly returned to agents) - Third-party API error messages (would need sanitization) - Database error messages (could contain sensitive schema information) @@ -103,6 +116,7 @@ In our system, we ONLY return trusted error messages from: ## Logging vs Capturing ### Key Principle + - **UserInputError** → Log to `console.warn()` in wrapAgentToolExecute (for Sentry logging, not as exception) - **ApiClientError** → Log to `console.warn()` in wrapAgentToolExecute (for Sentry logging, not as exception) - **ApiServerError/System errors (5xx)** → Let bubble up to be captured with `captureException()` @@ -129,6 +143,7 @@ const result = await apiService.someMethod({ organizationSlug }); ``` **What happens:** + - API client throws typed errors via `createApiError` factory: - 4xx → `ApiClientError` subclass (ApiPermissionError, ApiNotFoundError, etc.) - 5xx → `ApiServerError` @@ -157,6 +172,7 @@ return agentTool({ ``` **What happens:** + - API client throws `ApiClientError` or `ApiServerError` - `agentTool` catches ALL API errors and returns structured responses: - **Success:** `{ result: }` @@ -166,6 +182,7 @@ return agentTool({ - Other errors (unexpected) → Re-thrown to parent tool **Key Benefits:** + - **Structured responses:** AI agents receive consistent `{error?, result?}` objects instead of thrown errors - **Better error handling:** Agents can check for `error` property and handle failures gracefully - **Retry logic:** Agents can analyze error messages and determine if retry is worthwhile @@ -175,9 +192,9 @@ return agentTool({ #### Example 1: Permission Error in Embedded Agent Tool -``` +```` 1. User calls search_events tool -2. search_events uses AI agent with datasetAttributesTool +2. search_events uses AI agent with datasetAttributesTool 3. datasetAttributesTool calls fetchCustomAttributes() 4. fetchCustomAttributes calls apiService.listTraceItemAttributes() directly 5. API returns 403 "no multi-project access" @@ -187,20 +204,20 @@ return agentTool({ 9. Logs to console.warn() for Sentry logging 10. Returns structured response: ``` - { - error: "Input Error: API error (403): You do not have access to query across multiple projects. Please select a project for your query. You may be able to resolve this by addressing the concern and trying again." + { + error: "Input Error: API error (403): You do not have access to query across multiple projects. Please select a project for your query. You may be able to resolve this by addressing the concern and trying again." } ``` 11. AI agent receives the structured response and can check the error property 12. AI agent analyzes error message and retries with a specific project -``` +```` #### Example 2: Server Error ``` 1. User calls get_sentry_resource for an issue 2. Tool calls apiService.getIssue() directly (no withApiErrorHandling) -3. API returns 502 Bad Gateway +3. API returns 502 Bad Gateway 4. API client creates ApiServerError via createApiError factory and throws 5. Error bubbles up naturally to MCP server wrapper 6. formatErrorForUser handles ApiServerError, logs to Sentry with captureException @@ -210,6 +227,7 @@ return agentTool({ ## Best Practices ### DO: + - Call API methods directly and let errors bubble up naturally - Use `agentTool()` for embedded agent tools - Let typed errors (ApiClientError, ApiServerError) bubble up @@ -219,6 +237,7 @@ return agentTool({ - **SECURITY: Only return trusted error messages to AI agents** ### DON'T: + - Don't wrap API calls in try/catch unless adding value - Don't use `withApiErrorHandling` anymore (deprecated) - Don't use the old `wrapAgentToolExecute` function (use `agentTool` instead) @@ -230,18 +249,24 @@ return agentTool({ ### Security Guidelines for Agent Error Messages: **✅ SAFE - These are trusted and can be returned to agents:** + ```typescript // Sentry API errors (controlled by Sentry) -return { error: `Input Error: ${apiError.toUserMessage()}. You may be able to resolve this...` }; +return { + error: `Input Error: ${apiError.toUserMessage()}. You may be able to resolve this...`, +}; // Our own validation errors (controlled by our code) throw new UserInputError("Invalid organization slug format"); // System-generated templates (controlled by our code) -return { error: `Server Error (${status}): ${message}. Event ID: ${eventId}...` }; +return { + error: `Server Error (${status}): ${message}. Event ID: ${eventId}...`, +}; ``` **❌ UNSAFE - These could enable prompt injection:** + ```typescript // User input directly in error (NEVER do this) return { error: `Invalid input: ${userProvidedValue}` }; // 🚨 DANGEROUS @@ -263,7 +288,7 @@ createApiError Factory └─ 5xx → ApiServerError ↓ Thrown directly to tool (no withApiErrorHandling) - ↓ + ↓ In Embedded Agent Tool? ├─ Yes → agentTool │ ├─ UserInputError → Returns { error: "Input Error: ..." } @@ -296,6 +321,7 @@ When running in Cloudflare Workers: ### For Regular MCP Tools: 1. **Call the API directly without wrappers:** + ```typescript // Just call the API - errors bubble up naturally const result = await apiService.someMethod({ organizationSlug }); @@ -311,6 +337,7 @@ When running in Cloudflare Workers: ### For Embedded Agent Tools: 1. **Use `agentTool()` instead of the regular `tool()` function:** + ```typescript return agentTool({ description: "Tool description", @@ -324,6 +351,7 @@ When running in Cloudflare Workers: ``` 2. **Inside the tool, call the API directly:** + ```typescript // No error handling needed - agentTool handles it automatically const data = await apiService.someMethod(params); @@ -332,7 +360,7 @@ When running in Cloudflare Workers: 3. **The wrapper will automatically:** - Return `{ result: }` on success - Return `{ error: "formatted message" }` on failure - - Log UserInputError/ApiClientError to console.warn for Sentry logging + - Log UserInputError/ApiClientError to console.warn for Sentry logging - Include Event IDs for ApiServerError in error messages ### Error Message Formats: @@ -353,11 +381,12 @@ When running in Cloudflare Workers: ## Testing Error Handling When testing tools, verify: + 1. 404 errors include helpful hints via toUserMessage(): - Generic messages get detailed help about checking org/project/resource IDs - Specific messages get brief parameter verification hint 2. 403 errors are returned to agents as formatted markdown -3. 5xx errors are captured by Sentry with Event IDs +3. 5xx errors are captured by Sentry with Event IDs 4. Network errors bubble up appropriately 5. UserInputErrors have clear, actionable messages 6. ApiClientError in agent tools returns formatted markdown with "**Input Error**" header diff --git a/docs/contributing/quality-checks.md b/docs/contributing/quality-checks.md index a5b3b2601..5077344f9 100644 --- a/docs/contributing/quality-checks.md +++ b/docs/contributing/quality-checks.md @@ -10,6 +10,8 @@ pnpm run tsc && pnpm run lint && pnpm run test Do not proceed if any check fails. +`pnpm run lint` includes `oxfmt --check`. If it fails, run `pnpm run format`. + ## Tool Count Limits See [adding-tools.md](adding-tools.md#tool-count-limits) for current limits and guidance on when to add vs. combine tools. diff --git a/docs/integrations/claude-code-plugin.md b/docs/integrations/claude-code-plugin.md index 09c976881..d198e416c 100644 --- a/docs/integrations/claude-code-plugin.md +++ b/docs/integrations/claude-code-plugin.md @@ -8,10 +8,10 @@ The plugin registers a `sentry-mcp` subagent that Claude Code automatically dele Two variants are published: -| Plugin | MCP URL | Purpose | -|--------|---------|---------| -| `sentry-mcp` | `https://mcp.sentry.dev/mcp` | Default catalog gateway surface | -| `sentry-mcp-experimental` | `https://mcp.sentry.dev/mcp?experimental=1` | Forward-looking feature flags | +| Plugin | MCP URL | Purpose | +| ------------------------- | ------------------------------------------- | ------------------------------- | +| `sentry-mcp` | `https://mcp.sentry.dev/mcp` | Default catalog gateway surface | +| `sentry-mcp-experimental` | `https://mcp.sentry.dev/mcp?experimental=1` | Forward-looking feature flags | ## Directory Layout @@ -46,11 +46,11 @@ description: Sentry error tracking and performance monitoring agent. Use when the user asks about errors, exceptions, issues, stack traces, performance, traces, releases, or provides a Sentry URL. mcpServers: - - sentry # References the server name from .mcp.json + - sentry # References the server name from .mcp.json allowedTools: - analyze_issue_with_seer - search_issues - - ... # Default-grant direct MCP tool names + - ... # Default-grant direct MCP tool names --- ``` @@ -70,12 +70,14 @@ pnpm run --filter @sentry/mcp-core generate-definitions ``` This script: + 1. Imports all tools from `packages/mcp-core/src/tools/index.ts` 2. Imports all skills from `packages/mcp-core/src/skills.ts` 3. Writes `toolDefinitions.json` and `skillDefinitions.json` to `packages/mcp-core/src/` 4. Updates `allowedTools` in both `plugins/sentry-mcp/agents/sentry-mcp.md` and `plugins/sentry-mcp-experimental/agents/sentry-mcp.md`, using the default-grant direct surface for each plugin mode. Optional-skill tools remain reachable through `search_sentry_tools` and `execute_sentry_tool` when available. The script runs automatically as a `prebuild` and `pretest` hook in `packages/mcp-core/package.json`. Run it explicitly after: + - Adding, removing, or renaming tools - Changing tool skills assignments - Modifying agent prompts (to verify frontmatter stays valid) diff --git a/docs/integrations/ide-instructions-refactor.md b/docs/integrations/ide-instructions-refactor.md index 0ea0f1f59..2bab2d2e2 100644 --- a/docs/integrations/ide-instructions-refactor.md +++ b/docs/integrations/ide-instructions-refactor.md @@ -66,6 +66,7 @@ export function ClaudeCodeInstructions({ transport }: InstructionProps) { ``` **Benefits:** + - All Claude Code instructions (both transports) in one file - Easy to maintain - single source of truth for each IDE - Clear ownership - one file per IDE @@ -96,10 +97,12 @@ useEffect(() => { ``` **URL Format:** + - `?ide=claude-code&transport=cloud` - Cloud instructions for Claude Code - `?ide=cursor&transport=stdio` - Stdio instructions for Cursor **Default Values:** + - `ide=claude-code` (most popular) - `transport=cloud` (recommended for new users) @@ -133,14 +136,17 @@ export default function InstallTabs({ } // Call appropriate callback - const setActive = React.useCallback((next: number) => { - const tabId = items[next]?.props.id; - if (selectedTab && onTabChange && tabId) { - onTabChange(tabId); // String-based control - } else { - onChange?.(next); // Numeric control - } - }, [selectedTab, onTabChange, onChange, items]); + const setActive = React.useCallback( + (next: number) => { + const tabId = items[next]?.props.id; + if (selectedTab && onTabChange && tabId) { + onTabChange(tabId); // String-based control + } else { + onChange?.(next); // Numeric control + } + }, + [selectedTab, onTabChange, onChange, items], + ); } ``` @@ -164,12 +170,14 @@ export default function InstallTabs({ ### 1. `stdio-setup.tsx` **Changes:** + - Added imports for all IDE instruction components - Added `StdioSetupTabsProps` interface with `selectedIde` and `onIdeChange` props - Replaced inline Tab content with `` - Removed unused imports **Before:** + ```typescript
    @@ -180,6 +188,7 @@ export default function InstallTabs({ ``` **After:** + ```typescript @@ -189,6 +198,7 @@ export default function InstallTabs({ ### 2. `remote-setup.tsx` **Changes:** + - Added imports for all IDE instruction components - Added `RemoteSetupTabsProps` interface - Replaced inline Tab content with `` @@ -197,6 +207,7 @@ export default function InstallTabs({ ### 3. `getting-started.tsx` **Changes:** + - Added URL-based state management for `ide` parameter - Modified `stdio` state to read from URL (`transport` param) - Added `useEffect` to update URL when IDE or transport changes @@ -205,6 +216,7 @@ export default function InstallTabs({ ### 4. `install-tabs.tsx` **Changes:** + - Added `selectedTab?: string` and `onTabChange?: (tabId: string) => void` props - Modified active index calculation to support both numeric and string-based control - Updated `setActive` callback to call `onTabChange` with tab ID string @@ -278,12 +290,14 @@ const iconsByID: Record = { ## Benefits Summary ### For Developers + - ✅ **Easier IDE Management**: Create one file instead of editing two separate files - ✅ **Better Code Organization**: All IDE instructions co-located - ✅ **Reduced Duplication**: Single source of truth per IDE - ✅ **Clearer Ownership**: One file per IDE makes changes obvious ### For Users + - ✅ **URL Sharing**: Can share links like `?ide=cursor&transport=stdio` - ✅ **State Preservation**: IDE selection maintained when switching Cloud ↔ Stdio - ✅ **Familiar UI**: No changes to the existing interface @@ -302,6 +316,7 @@ const iconsByID: Record = { ## Migration Notes This refactor is **100% backward compatible**: + - InstallTabs still supports numeric index control (used elsewhere) - No breaking changes to component APIs - All existing functionality preserved @@ -310,6 +325,7 @@ This refactor is **100% backward compatible**: ## Future Improvements Potential future enhancements: + 1. **Analytics**: Track which IDE/transport combinations are popular 2. **IDE Detection**: Auto-detect user's IDE and pre-select it (if worth complexity) 3. **Favorites**: Allow users to mark favorite IDEs for quick access diff --git a/docs/operations/embedded-agents.md b/docs/operations/embedded-agents.md index da36ebd34..5bc42b3e6 100644 --- a/docs/operations/embedded-agents.md +++ b/docs/operations/embedded-agents.md @@ -5,6 +5,7 @@ Configuration guide for embedded AI agents used by AI-powered search tools in Se ## Overview Sentry MCP uses embedded AI agents for the following tools: + - `search_errors`, `search_logs`, `search_traces`, `search_metrics`, `search_profiles`, `search_replays` - Natural language search over one dataset each (share one handler; `search_events` remains as a deprecated catalog alias) - `search_issues` - Natural language search across issues - `search_issue_events` - Search events within a specific issue @@ -112,6 +113,7 @@ In your MCP settings (e.g., Claude Desktop config): When using Sentry MCP with tools like the Claude Agent SDK, the SDK may inject `ANTHROPIC_API_KEY` into the environment. This causes conflicts if you want to use OpenAI instead. **Before (incorrect auto-detection):** + ```bash # Claude SDK injects ANTHROPIC_API_KEY # Your MCP config only sets OPENAI_API_KEY @@ -119,6 +121,7 @@ When using Sentry MCP with tools like the Claude Agent SDK, the SDK may inject ` ``` **After (explicit provider selection):** + ```bash # Claude SDK injects ANTHROPIC_API_KEY # Your MCP config sets OPENAI_API_KEY and EMBEDDED_AGENT_PROVIDER=openai @@ -234,16 +237,19 @@ npx @sentry/mcp-server --access-token=... 2>&1 | grep "Using" ``` Expected output: + ``` Using openai responses API for AI-powered search tools (from EMBEDDED_AGENT_PROVIDER). ``` Or: + ``` Using anthropic for AI-powered search tools (auto-detected). ``` Or: + ``` Using openrouter for AI-powered search tools (auto-detected). ``` @@ -253,6 +259,7 @@ Using openrouter for AI-powered search tools (auto-detected). ### Provider not detected **Check environment variables:** + ```bash echo $ANTHROPIC_API_KEY echo $OPENAI_API_KEY @@ -261,6 +268,7 @@ echo $EMBEDDED_AGENT_PROVIDER ``` **Verify API key format:** + - OpenAI: `sk-...` (starts with `sk-`) - Anthropic: `sk-ant-...` (starts with `sk-ant-`) - OpenRouter: `sk-or-...` (starts with `sk-or-`) @@ -286,6 +294,7 @@ npx @sentry/mcp-server --access-token=... ``` Should output: + ``` Using openai responses API for AI-powered search tools (from EMBEDDED_AGENT_PROVIDER). ``` diff --git a/docs/operations/logging.md b/docs/operations/logging.md index 3d288ff8a..e1ce17e71 100644 --- a/docs/operations/logging.md +++ b/docs/operations/logging.md @@ -5,10 +5,12 @@ How logging works in the Sentry MCP server using LogTape and Sentry. For tracing ## Overview We use [LogTape](https://logtape.org/) for structured logging with two sinks: + - **Console sink**: JSON Lines format for log aggregation - **Sentry sink**: Sends logs to Sentry's Logs product using `Sentry.logger.*` API Log levels are controlled by: + 1. `LOG_LEVEL` environment variable (e.g., `"debug"`, `"info"`, `"warning"`, `"error"`) 2. Falls back to `NODE_ENV`: `"debug"` in development, `"info"` in production @@ -25,6 +27,7 @@ This is enforced in `packages/mcp-core/src/telem/logging.ts` via `STDERR_CONSOLE Regression history: #922 — LogTape's default level map sent `info`/`debug` records through `console.info`/`console.debug`, landing structured JSON on stdout and breaking stdio clients. **Rules for new code:** + - Do not call `console.log`, `console.info`, or `console.debug` anywhere reachable from the stdio entry point. Use `logInfo`/`logDebug` instead. - Do not introduce new LogTape sinks that write to stdout. - Do not change `STDERR_CONSOLE_LEVEL_MAP` to map any level to a non-stderr method. The regression test in `logging.test.ts` enforces this for `info`. @@ -32,70 +35,79 @@ Regression history: #922 — LogTape's default level map sent `info`/`debug` rec ## Using the Log Helpers ### logInfo() - Routine telemetry + ```typescript import { logInfo } from "@sentry/mcp-server/telem/logging"; logInfo("MCP server started", { loggerScope: ["server", "lifecycle"], - extra: { port: 3000 } + extra: { port: 3000 }, }); ``` ### logWarn() - Operational warnings + ```typescript import { logWarn } from "@sentry/mcp-server/telem/logging"; logWarn("Rate limit approaching", { loggerScope: ["api", "rate-limit"], - extra: { remaining: 10, limit: 100 } + extra: { remaining: 10, limit: 100 }, }); ``` ### logError() - Operational errors (no Sentry issue) + ```typescript import { logError } from "@sentry/mcp-server/telem/logging"; logError(error, { loggerScope: ["tools", "fetch-trace"], - extra: { traceId: "abc123" } + extra: { traceId: "abc123" }, }); ``` ### logIssue() - System errors (creates Sentry issue) + ```typescript import { logIssue } from "@sentry/mcp-server/telem/logging"; const eventId = logIssue(error, { loggerScope: ["oauth", "token-refresh"], contexts: { - oauth: { client_id: "..." } - } + oauth: { client_id: "..." }, + }, }); ``` ## When to Use Each Helper **Use `logIssue()` for:** + - System errors (5xx, network failures, unexpected exceptions) - Critical failures that need investigation - Creates a Sentry Issue + emits a log entry with Event ID **Use `logError()` for:** + - Expected operational errors (trace fetch failed, encoding error) - Errors that are handled gracefully - Sends to Sentry Logs only (no Issue created) **Use `logWarn()` for:** + - Rate limit approaching - Deprecated API usage - Configuration warnings **Use `logInfo()` for:** + - Request lifecycle (connection established, tool invoked) - Configuration output - Routine telemetry **Skip logging:** + - `UserInputError` - Expected validation failures - 4xx API responses - Client errors (except 429 rate limits) - See [Error Handling](../contributing/error-handling.md) for complete rules @@ -106,9 +118,9 @@ All helpers accept: ```typescript interface LogOptions { - loggerScope?: string | readonly string[]; // e.g., ["cloudflare", "oauth"] - extra?: Record; // Additional context - contexts?: Record>; // Sentry contexts + loggerScope?: string | readonly string[]; // e.g., ["cloudflare", "oauth"] + extra?: Record; // Additional context + contexts?: Record>; // Sentry contexts } ``` @@ -117,10 +129,12 @@ interface LogOptions { ## Configuration **Environment Variables:** + - `LOG_LEVEL` - Override log level (`"debug"`, `"info"`, `"warning"`, `"error"`) - `NODE_ENV` - Determines default level (`"development"` = debug, else info) **Sinks:** + - Console: JSON Lines format for log aggregation - Sentry: Sends logs to Sentry for monitoring (uses `Sentry.init` config) diff --git a/docs/operations/monitoring.md b/docs/operations/monitoring.md index 00215d644..31bd2c1fc 100644 --- a/docs/operations/monitoring.md +++ b/docs/operations/monitoring.md @@ -5,6 +5,7 @@ Observability patterns using Sentry across the MCP server. ## Architecture Different Sentry SDKs for different environments: + - **Core server**: `@sentry/core` (platform-agnostic) - **Cloudflare Workers**: `@sentry/cloudflare` - **Node.js stdio**: `@sentry/node` @@ -24,7 +25,7 @@ for the `contextType` diagnostic and its privacy constraints. ```typescript export async function createTracedToolHandler( name: T, - handler: ToolHandlerFunction + handler: ToolHandlerFunction, ): Promise<[T, ToolHandlerFunction]> { return [ name, @@ -34,10 +35,8 @@ export async function createTracedToolHandler( ...extractMcpParameters(params), }; - return await withActiveSpan( - `tools/call ${name}`, - attributes, - async () => handler(context, params) + return await withActiveSpan(`tools/call ${name}`, attributes, async () => + handler(context, params), ); }, ]; @@ -70,13 +69,13 @@ For values set at startup, put the same keys in both `initialScope.tags` and async function withActiveSpan( name: string, attributes: Record, - fn: () => Promise + fn: () => Promise, ): Promise { const activeSpan = getActiveSpan(); const span = activeSpan?.startSpan(name) ?? startInactiveSpan({ name }); - + span.setAttributes(attributes); - + try { return await fn(); } catch (error) { @@ -99,9 +98,7 @@ export default function getSentryConfig(env: Env, context: ExecutionContext) { dsn: env.VITE_SENTRY_DSN, environment: env.VITE_SENTRY_ENVIRONMENT || "development", context, - integrations: [ - Sentry.rewriteFramesIntegration({ root: "/" }), - ], + integrations: [Sentry.rewriteFramesIntegration({ root: "/" })], beforeSend(event) { // Redact sensitive data if (event.request?.headers?.authorization) { @@ -116,16 +113,13 @@ export default function getSentryConfig(env: Env, context: ExecutionContext) { ### Worker Instrumentation ```typescript -export default Sentry.withSentry( - (env) => getSentryConfig(env), - { - async fetch(request, env, ctx): Promise { - // Attach OAuth provider to request - request.OAUTH_PROVIDER = oAuthProvider.configure(/* ... */); - return app.fetch(request, env, ctx); - }, - } -); +export default Sentry.withSentry((env) => getSentryConfig(env), { + async fetch(request, env, ctx): Promise { + // Attach OAuth provider to request + request.OAUTH_PROVIDER = oAuthProvider.configure(/* ... */); + return app.fetch(request, env, ctx); + }, +}); ``` ## Node.js Stdio Setup @@ -137,9 +131,7 @@ import * as Sentry from "@sentry/node"; Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV || "production", - integrations: [ - Sentry.nodeProfilingIntegration(), - ], + integrations: [Sentry.nodeProfilingIntegration()], tracesSampleRate: 0.1, profilesSampleRate: 0.1, }); @@ -156,6 +148,7 @@ process.on("uncaughtException", (error) => { Sentry follows OpenTelemetry semantic conventions for consistent observability. ### MCP Attributes (Model Context Protocol) + Based on the current [OpenTelemetry MCP conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/), the standard MCP semantic attributes are: - `mcp.method.name` - The name of the request or notification method (e.g., "notifications/cancelled", "initialize", "tools/call") @@ -175,6 +168,7 @@ Current MCP spans also use related OpenTelemetry attributes outside the `mcp.*` - `gen_ai.tool.call.result.count` - Sentry MCP extension for result counts, including zero-result tool calls ### Application-Owned Attributes + These are Sentry MCP application attributes that are not part of the MCP semantic convention: - `app.resource.type` - Resolved Sentry resource type for `get_sentry_resource` @@ -196,11 +190,13 @@ Following [OpenTelemetry semantic conventions for user agent](https://openteleme **Cloudflare Transport**: Captured from the initial SSE/WebSocket connection request headers and cached for the session ### Network Attributes + Based on [OpenTelemetry network conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/network.md): - `network.transport` - Transport protocol used ("pipe" for stdio, "tcp" for SSE) ### GenAI Attributes (Generative AI) + Based on [OpenTelemetry GenAI conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/gen-ai.md): - `gen_ai.provider.name` - The AI provider name (e.g., "anthropic") @@ -211,6 +207,7 @@ Based on [OpenTelemetry GenAI conventions](https://github.com/open-telemetry/sem - `gen_ai.usage.output_tokens` - Number of tokens in response ### Span Naming Pattern + Follows the format: `{mcp.method.name} {target}` per OpenTelemetry MCP semantic conventions - Tools: `tools/call {tool_name}` (e.g., `tools/call find_issues`) @@ -221,6 +218,7 @@ Follows the format: `{mcp.method.name} {target}` per OpenTelemetry MCP semantic **Note**: Span names are flexible and can be adjusted based on your needs. However, attribute names MUST follow the OpenTelemetry semantic conventions exactly as specified above. ### Example Attributes + ```typescript // MCP Tool Execution { @@ -260,11 +258,13 @@ Follows the format: `{mcp.method.name} {target}` per OpenTelemetry MCP semantic ## Error Classification ### Skip Logging For: + - `UserInputError` - Expected user errors - 4xx API responses (except 429) - Validation errors ### Always Log: + - 5xx errors - Network failures - Unexpected exceptions @@ -333,6 +333,7 @@ Interpretation: do not include `app.response.reason` ### Traces Configuration + ```typescript { tracesSampleRate: 0.1, // 10% in production @@ -343,6 +344,7 @@ Interpretation: ## Environment Variables ### Required for Monitoring + ```bash # Cloudflare (build-time) VITE_SENTRY_DSN=https://...@sentry.io/... diff --git a/docs/operations/oauth-signout-playbook.md b/docs/operations/oauth-signout-playbook.md index eddcd444c..a3b61531e 100644 --- a/docs/operations/oauth-signout-playbook.md +++ b/docs/operations/oauth-signout-playbook.md @@ -26,19 +26,19 @@ MCP client ──(tool call)──> /mcp ──> mcp-handler ──> tool handle ## Failure modes -| Mode | Trigger | Currently visible in telemetry | Detected by | -|---|---|---|---| -| **Natural 30d expiry** | Upstream `expires_at` passes | Yes | Probe path in `tokenExchangeCallback` → `upstream_rejected` | -| **Premature Sentry-side invalidation (SSO / org / password / admin)** | Sentry revokes before `expires_at` | Yes | Tool call surfaces 401 → `app.access.grant.revoked_reason:upstream_rejected_in_use` + grant revoked via `onUpstreamUnauthorized` callback | -| **Stale grants from before `refreshToken` was stored in props** | Old grants from pre-#537 deploys | Yes | `mcp-handler` → `app.access.grant.revoked_reason:stale_props_no_refresh` | -| **Client-side state loss (DCR state reset, fresh install, reinstall)** | Client drops its stored `client_id`/`refresh_token` | Indirectly | High `/oauth/register` volume and `register:callback` ratio per `app.client.family` | -| **Invalid redirect URI at authorize** | Client sends an `redirect_uri` that's not registered | Yes | Log `OAuth authorization failed: Invalid redirect URI` with `clientId`/`redirectUri`/`registeredUris`/`clientName` | -| **Upstream probe transient (5xx / rate limit / network)** | Sentry side instability | Yes | `app.access.refresh.outcome:verification_indeterminate` with `app.access.probe.reason` (does not force sign-out) | -| **Same-user concurrent session interference** | Another session of the same user re-authorizes | Resolved by passing `revokeExistingGrants: false` — see Gap #4 below | n/a after fix | +| Mode | Trigger | Currently visible in telemetry | Detected by | +| ---------------------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| **Natural 30d expiry** | Upstream `expires_at` passes | Yes | Probe path in `tokenExchangeCallback` → `upstream_rejected` | +| **Premature Sentry-side invalidation (SSO / org / password / admin)** | Sentry revokes before `expires_at` | Yes | Tool call surfaces 401 → `app.access.grant.revoked_reason:upstream_rejected_in_use` + grant revoked via `onUpstreamUnauthorized` callback | +| **Stale grants from before `refreshToken` was stored in props** | Old grants from pre-#537 deploys | Yes | `mcp-handler` → `app.access.grant.revoked_reason:stale_props_no_refresh` | +| **Client-side state loss (DCR state reset, fresh install, reinstall)** | Client drops its stored `client_id`/`refresh_token` | Indirectly | High `/oauth/register` volume and `register:callback` ratio per `app.client.family` | +| **Invalid redirect URI at authorize** | Client sends an `redirect_uri` that's not registered | Yes | Log `OAuth authorization failed: Invalid redirect URI` with `clientId`/`redirectUri`/`registeredUris`/`clientName` | +| **Upstream probe transient (5xx / rate limit / network)** | Sentry side instability | Yes | `app.access.refresh.outcome:verification_indeterminate` with `app.access.probe.reason` (does not force sign-out) | +| **Same-user concurrent session interference** | Another session of the same user re-authorizes | Resolved by passing `revokeExistingGrants: false` — see Gap #4 below | n/a after fix | ## Telemetry surface -All metrics live in the `mcp-server` project on Sentry. Attribute *keys* avoid sensitive substrings (`auth`/`oauth`, `token`, `bearer`, `credentials`) via `app.access.*`, and values deliberately avoid those same substrings, because Sentry's default PII scrubber replaces matching keys/values with `[Filtered]` at ingest (see PR #916 and #1219). +All metrics live in the `mcp-server` project on Sentry. Attribute _keys_ avoid sensitive substrings (`auth`/`oauth`, `token`, `bearer`, `credentials`) via `app.access.*`, and values deliberately avoid those same substrings, because Sentry's default PII scrubber replaces matching keys/values with `[Filtered]` at ingest (see PR #916 and #1219). ### `app.oauth.token_exchange` (counter) @@ -197,7 +197,7 @@ Fixed alongside Gap #1. `ApiAuthenticationError` now propagates unwrapped and tr ### Gap #4 — Same-user concurrent session interference — **closed** -`workers-oauth-provider`'s `completeAuthorization` defaulted to revoking every existing grant for `(userId, clientId)` whenever a new authorization completed. Because Claude Code persists its DCR `client_id` across processes (e.g., across project folders, or after an update triggers re-auth in one session), one process re-authorizing would silently invalidate every other active session for the same user. The affected sessions saw 401s from the OAuth library on their next `/mcp` request — *before* reaching our handler — so no `grant_revoked` metric ever fired. See issue #924. +`workers-oauth-provider`'s `completeAuthorization` defaulted to revoking every existing grant for `(userId, clientId)` whenever a new authorization completed. Because Claude Code persists its DCR `client_id` across processes (e.g., across project folders, or after an update triggers re-auth in one session), one process re-authorizing would silently invalidate every other active session for the same user. The affected sessions saw 401s from the OAuth library on their next `/mcp` request — _before_ reaching our handler — so no `grant_revoked` metric ever fired. See issue #924. We now pass `revokeExistingGrants: false` to `completeAuthorization`. Multiple grants for the same `(userId, clientId)` coexist; each lives until its own `refreshTokenTTL` (30d) or an explicit revoke. This matches how mainstream OAuth servers (Google, GitHub, Auth0, etc.) behave; the library's prior default was an unusual policy. diff --git a/docs/operations/security.md b/docs/operations/security.md index 91c811541..4ff6e78c2 100644 --- a/docs/operations/security.md +++ b/docs/operations/security.md @@ -182,7 +182,7 @@ interface ServerContext { userIpAddress?: string; clientId: string; accessToken: string; - grantedSkills: Set; // Primary authorization method + grantedSkills: Set; // Primary authorization method // grantedScopes is deprecated and will be removed Jan 1, 2026 constraints: Constraints; sentryHost: string; @@ -240,7 +240,7 @@ Example protection in tools: ```typescript // URLs must be valid and from expected domains -if (!issueUrl.includes('sentry.io')) { +if (!issueUrl.includes("sentry.io")) { throw new UserInputError("Invalid Sentry URL"); } @@ -257,10 +257,10 @@ The OAuth `state` is a compact HMAC-signed payload with a 10‑minute expiry: type OAuthState = { clientId: string; redirectUri: string; // must be a valid URL - scope: string[]; // from OAuth provider parseAuthRequest + scope: string[]; // from OAuth provider parseAuthRequest permissions?: string[]; // user selections from approval - iat: number; // issued at (ms) - exp: number; // expires at (ms) + iat: number; // issued at (ms) + exp: number; // expires at (ms) }; // Sign: `${hex(hmacSHA256(json))}.${btoa(json)}` using COOKIE_SECRET @@ -285,11 +285,11 @@ All user inputs sanitized: // Signed cookie for approved clients const cookie = await signCookie( `approved_clients=${JSON.stringify(approvedClients)}`, - COOKIE_SECRET + COOKIE_SECRET, ); // Cookie attributes -"HttpOnly; Secure; SameSite=Lax; Max-Age=2592000" // 30 days +("HttpOnly; Secure; SameSite=Lax; Max-Age=2592000"); // 30 days ``` ## Error Handling @@ -324,7 +324,7 @@ catch (error) { ```typescript // Verify organization access const { organizations } = await apiService.listOrganizations(); -if (!organizations.find(org => org.slug === requestedOrg)) { +if (!organizations.find((org) => org.slug === requestedOrg)) { throw new UserInputError("No access to organization"); } ``` @@ -335,7 +335,7 @@ Required for OAuth: ```bash SENTRY_CLIENT_ID=your_oauth_app_id -SENTRY_CLIENT_SECRET=your_oauth_app_secret +SENTRY_CLIENT_SECRET=your_oauth_app_secret COOKIE_SECRET=random_32_char_string ``` @@ -343,10 +343,7 @@ COOKIE_SECRET=random_32_char_string ```typescript // Allowed origins for OAuth flow -const ALLOWED_ORIGINS = [ - "https://sentry.io", - "https://*.sentry.io" -]; +const ALLOWED_ORIGINS = ["https://sentry.io", "https://*.sentry.io"]; ``` ## References diff --git a/docs/operations/stdio-auth.md b/docs/operations/stdio-auth.md index 522d6fc90..7deaa2c74 100644 --- a/docs/operations/stdio-auth.md +++ b/docs/operations/stdio-auth.md @@ -67,10 +67,10 @@ The Cloudflare deployment uses a separate OAuth application (configured via `SEN Both transports use the `SENTRY_CLIENT_ID` env var name for overriding, but they're separate deployments with separate values: -| Transport | Default client ID | Grant type | Client secret | -|-----------|------------------|------------|---------------| -| stdio | Bundled in `auth/constants.ts` | Device code (RFC 8628) | None (public client) | -| Cloudflare | `SENTRY_CLIENT_ID` env var | Authorization code | `SENTRY_CLIENT_SECRET` env var | +| Transport | Default client ID | Grant type | Client secret | +| ---------- | ------------------------------ | ---------------------- | ------------------------------ | +| stdio | Bundled in `auth/constants.ts` | Device code (RFC 8628) | None (public client) | +| Cloudflare | `SENTRY_CLIENT_ID` env var | Authorization code | `SENTRY_CLIENT_SECRET` env var | ## Token Cache @@ -126,15 +126,15 @@ These commands support `--host` and `--url` flags with the same precedence as th ## Key Files -| File | Purpose | -|------|---------| -| `packages/mcp-server/src/auth/constants.ts` | Bundled client ID, OAuth endpoints, scopes | -| `packages/mcp-server/src/auth/device-code-flow.ts` | RFC 8628 implementation (request, poll, browser open) | -| `packages/mcp-server/src/auth/token-cache.ts` | Persistent token storage at `~/.sentry/mcp.json` | -| `packages/mcp-server/src/auth/resolve-token.ts` | Orchestration: cache check → TTY check → device code flow | -| `packages/mcp-server/src/auth/types.ts` | Zod schemas for API responses, `CachedToken` type | -| `packages/mcp-server/src/cli/commands/auth.ts` | `auth login/logout/status` subcommands | -| `packages/mcp-core/src/scopes.ts` | Shared OAuth scope definitions | +| File | Purpose | +| -------------------------------------------------- | --------------------------------------------------------- | +| `packages/mcp-server/src/auth/constants.ts` | Bundled client ID, OAuth endpoints, scopes | +| `packages/mcp-server/src/auth/device-code-flow.ts` | RFC 8628 implementation (request, poll, browser open) | +| `packages/mcp-server/src/auth/token-cache.ts` | Persistent token storage at `~/.sentry/mcp.json` | +| `packages/mcp-server/src/auth/resolve-token.ts` | Orchestration: cache check → TTY check → device code flow | +| `packages/mcp-server/src/auth/types.ts` | Zod schemas for API responses, `CachedToken` type | +| `packages/mcp-server/src/cli/commands/auth.ts` | `auth login/logout/status` subcommands | +| `packages/mcp-core/src/scopes.ts` | Shared OAuth scope definitions | ## Token Refresh diff --git a/docs/operations/token-cost-tracking.md b/docs/operations/token-cost-tracking.md index 0b2044385..3c043d4b9 100644 --- a/docs/operations/token-cost-tracking.md +++ b/docs/operations/token-cost-tracking.md @@ -5,21 +5,25 @@ Measures the static overhead of MCP tool definitions - the tokens sent to LLM cl ## What's Being Measured The token cost of tool metadata that MCP sends to clients via `tools/list`: + - Tool names and descriptions - Parameter schemas (JSON Schema) - Total overhead per tool and across all tools **Exclusions:** + - Runtime token usage by embedded agents (search_events, search_issues, search_issue_events) ## Running Locally **Display table (default):** + ```bash pnpm run measure-tokens ``` **Output:** + ``` 📊 MCP Server Token Cost Report ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -40,6 +44,7 @@ Per-Tool Breakdown: ``` **Write JSON to file:** + ```bash # From repository root pnpm run measure-tokens -- -o token-stats.json @@ -50,6 +55,7 @@ pnpm run measure-tokens -- -o token-stats.json ``` JSON format: + ```json { "total_tokens": 14068, @@ -67,11 +73,13 @@ JSON format: GitHub Actions workflow runs on every PR and push to main: **On Pull Requests:** + - 📝 **PR Comment:** Automatic comment with full report (updated on each push) - 📊 **Job Summary:** Detailed per-tool breakdown in Actions tab - 📦 **Artifact:** `token-stats-{sha}.json` stored for 90 days **On Main Branch:** + - 📊 **Job Summary:** Detailed per-tool breakdown in Actions tab - 📦 **Artifact:** `token-stats-{sha}.json` stored for 90 days @@ -83,10 +91,12 @@ GitHub Actions workflow runs on every PR and push to main: - ~586 tokens/tool average **Tool count limits:** + - **Target:** ≤20 tools (current best practice) - **Maximum:** ≤25 tools (hard limit for AI agents) **When to investigate:** + - Total tokens increase >10% without new tools - Individual tool >1,000 tokens (indicates overly verbose descriptions) - New tool adds >500 tokens (review description clarity) @@ -98,6 +108,7 @@ GitHub Actions workflow runs on every PR and push to main: **Script location:** `packages/mcp-core/scripts/measure-token-cost.ts` **CLI options:** + ```bash tsx measure-token-cost.ts # Display table tsx measure-token-cost.ts -o file.json # Write JSON to file @@ -107,16 +118,19 @@ tsx measure-token-cost.ts --help # Show help ## Optimizing Token Cost **Reduce description verbosity:** + - Be concise - LLMs don't need hand-holding - Remove redundant examples - Focus on unique, non-obvious details **Simplify parameter schemas:** + - Use `.describe()` sparingly - Avoid duplicate descriptions in nested schemas - Combine related parameters **Consolidate tools:** + - Before adding a new tool, check if existing tools can handle it - Consider parameter variants instead of separate tools diff --git a/docs/releases/cloudflare.md b/docs/releases/cloudflare.md index dc02a5bf8..7178ac797 100644 --- a/docs/releases/cloudflare.md +++ b/docs/releases/cloudflare.md @@ -5,6 +5,7 @@ Cloudflare Workers deployment configuration and release process. ## Architecture Overview The deployment consists of: + - **Worker**: Stateless HTTP server with OAuth flow and MCP handler - **KV Storage**: OAuth token storage - **Static Assets**: React UI for setup instructions @@ -20,26 +21,29 @@ The deployment consists of: "compatibility_date": "2025-03-21", "compatibility_flags": [ "nodejs_compat", - "nodejs_compat_populate_process_env" + "nodejs_compat_populate_process_env", ], "keep_vars": true, // Bindings - "kv_namespaces": [{ - "binding": "OAUTH_KV", - "id": "your-kv-namespace-id" - }], + "kv_namespaces": [ + { + "binding": "OAUTH_KV", + "id": "your-kv-namespace-id", + }, + ], // SPA configuration "site": { - "bucket": "./dist/client" - } + "bucket": "./dist/client", + }, } ``` ### Environment Variables Required in production: + ```bash SENTRY_CLIENT_ID=your_oauth_app_id SENTRY_CLIENT_SECRET=your_oauth_app_secret @@ -47,6 +51,7 @@ COOKIE_SECRET=32_char_random_string ``` Optional overrides for self-hosted deployments: + ```bash # Leave unset to target the SaaS host SENTRY_HOST=sentry.example.com # Hostname only (self-hosted only) @@ -57,6 +62,7 @@ self-hosted Sentry instance; no additional host variables are required for the SaaS service. Development (.dev.vars): + ```bash SENTRY_CLIENT_ID=dev_client_id SENTRY_CLIENT_SECRET=dev_secret @@ -72,7 +78,11 @@ import { experimental_createMcpHandler as createMcpHandler } from "agents/mcp"; import { buildServer } from "@sentry/mcp-server/server"; const mcpHandler: ExportedHandler = { - async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext, + ): Promise { // Extract auth props from ExecutionContext (set by OAuth provider) const oauthCtx = ctx as OAuthExecutionContext; @@ -81,7 +91,7 @@ const mcpHandler: ExportedHandler = { userId: oauthCtx.props.userId, clientId: oauthCtx.props.clientId, accessToken: oauthCtx.props.accessToken, - grantedSkills, // Primary authorization method + grantedSkills, // Primary authorization method constraints: verification.constraints, sentryHost, mcpUrl: oauthCtx.props.mcpUrl, @@ -179,6 +189,7 @@ For feature branches, GitHub Actions automatically uploads new versions without 4. Use Cloudflare dashboard to gradually roll out the version Manual version upload: + ```bash pnpm cf:versions:upload ``` @@ -186,6 +197,7 @@ pnpm cf:versions:upload ### Creating Resources First-time setup: + ```bash # Create KV namespace for OAuth token storage npx wrangler kv:namespace create OAUTH_KV @@ -196,6 +208,7 @@ npx wrangler kv:namespace create OAUTH_KV ## Multi-Region Considerations Cloudflare Workers run globally, but consider: + - KV is eventually consistent globally - Workers are stateless and edge-deployed - Use regional hints for performance @@ -205,10 +218,7 @@ Cloudflare Workers run globally, but consider: ### CORS Settings ```typescript -const ALLOWED_ORIGINS = [ - "https://sentry.io", - "https://*.sentry.io" -]; +const ALLOWED_ORIGINS = ["https://sentry.io", "https://*.sentry.io"]; // Apply to responses response.headers.set("Access-Control-Allow-Origin", origin); @@ -219,7 +229,7 @@ response.headers.set("Access-Control-Allow-Credentials", "true"); ```typescript // Secure cookie settings -"HttpOnly; Secure; SameSite=Lax; Max-Age=2592000" +"HttpOnly; Secure; SameSite=Lax; Max-Age=2592000"; ``` ## Monitoring @@ -245,6 +255,7 @@ export default { ### Worker Analytics Monitor via Cloudflare dashboard: + - Request rates - Error rates - CPU time and memory usage diff --git a/docs/releases/stdio.md b/docs/releases/stdio.md index d6814fcb1..ee2889151 100644 --- a/docs/releases/stdio.md +++ b/docs/releases/stdio.md @@ -5,6 +5,7 @@ npm package release process for the MCP server stdio transport. ## Overview The MCP server is published to npm as `@sentry/mcp-server` for use with: + - Claude Desktop - Cursor IDE - VS Code with MCP extension @@ -13,6 +14,7 @@ The MCP server is published to npm as `@sentry/mcp-server` for use with: ## Package Structure Published package includes: + - Compiled TypeScript (`dist/`) - stdio transport implementation - Type definitions @@ -32,6 +34,7 @@ Update version in `packages/mcp-server/package.json`: ``` Follow semantic versioning: + - **Major**: Breaking changes to tool interfaces - **Minor**: New tools or non-breaking features - **Patch**: Bug fixes @@ -44,9 +47,11 @@ Document changes in `CHANGELOG.md`: ## [1.2.3] - 2025-01-16 ### Added + - New `search_docs` tool for documentation search ### Fixed + - Fix context propagation in tool handlers ``` @@ -123,6 +128,7 @@ Users install via npx in their MCP client configuration: ``` Config location: + - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` @@ -148,10 +154,12 @@ Add to `.cursor/mcp.json`: ## Environment Variables Required: + - `SENTRY_ACCESS_TOKEN` - Sentry API access token - `SENTRY_HOST` - Sentry instance hostname (default: `sentry.io`) Optional: + - `SENTRY_ORG` - Default organization slug - `SENTRY_PROJECT` - Default project slug - `MCP_DISABLE_SKILLS` - Disable specific skills, comma-separated (e.g. `seer`) @@ -193,6 +201,7 @@ npm publish --tag beta ``` Users install with: + ```json { "args": ["-y", "@sentry/mcp-server@beta"] @@ -202,14 +211,17 @@ Users install with: ## Troubleshooting ### Package Not Found + - Verify package name: `@sentry/mcp-server` (with scope) - Check npm registry: `npm view @sentry/mcp-server` ### Version Mismatch + - Users may have cached version: `npx clear-npx-cache` - Recommend version pinning for stability ### Build Failures + - Ensure `pnpm run build` succeeds before publishing - Check TypeScript compilation errors - Verify all dependencies are listed in package.json diff --git a/docs/specs/README.md b/docs/specs/README.md index 469af5633..331da131a 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -40,6 +40,7 @@ When adding a new feature specification: to reference it **Important Guidelines**: + - Keep specs in a single file under `docs/specs/` - Focus on WHAT and WHY, not HOW - Include code examples for interfaces and usage diff --git a/docs/specs/ai-conversations.md b/docs/specs/ai-conversations.md index b8ca9a485..30dc7e322 100644 --- a/docs/specs/ai-conversations.md +++ b/docs/specs/ai-conversations.md @@ -441,7 +441,7 @@ search_agent_conversations({ organizationSlug: "my-org", query: "recent conversations", period: "24h", -}) +}); ``` Find failed conversations: @@ -451,7 +451,7 @@ search_agent_conversations({ organizationSlug: "my-org", query: "failed conversations", period: "7d", -}) +}); ``` Fetch a transcript from a result: @@ -460,7 +460,7 @@ Fetch a transcript from a result: get_agent_conversation_details({ organizationSlug: "my-org", conversationId: "conversation-123", -}) +}); ``` Fetch from a Sentry URL: @@ -468,7 +468,7 @@ Fetch from a Sentry URL: ```typescript get_sentry_resource({ url: "https://my-org.sentry.io/explore/conversations/conversation-123/", -}) +}); ``` ## Testing diff --git a/docs/specs/project-management.md b/docs/specs/project-management.md index f045a97e9..caa8a3270 100644 --- a/docs/specs/project-management.md +++ b/docs/specs/project-management.md @@ -11,6 +11,7 @@ catalog-only: discover them with `search_sentry_tools` and execute them with `execute_sentry_tool`. Implementation references: + - Tool catalog: `packages/mcp-core/src/tools/catalog/` - Availability rules: `packages/mcp-core/src/tools/catalog-runtime/availability.ts` - API client: `packages/mcp-core/src/api-client/client.ts` @@ -27,35 +28,36 @@ create_project({ organizationSlug: "my-org", teamSlug: "my-team", name: "My Project", - slug: "my-project", // optional - platform: "javascript", // optional - repository: "acme/app", // optional, must already be connected -}) + slug: "my-project", // optional + platform: "javascript", // optional + repository: "acme/app", // optional, must already be connected +}); update_project({ organizationSlug: "my-org", projectSlug: "my-project", - name: "New Name", // at least one metadata field is required + name: "New Name", // at least one metadata field is required slug: "new-slug", platform: "python", -}) +}); add_team_to_project({ organizationSlug: "my-org", projectSlug: "my-project", teamSlug: "backend", -}) +}); remove_team_from_project({ organizationSlug: "my-org", projectSlug: "my-project", teamSlug: "backend", -}) +}); ``` ## Creation Contract `create_project` accepts core setup fields: + - `organizationSlug` - `teamSlug` - `name` @@ -67,6 +69,7 @@ It does not expose ownership rules, alert rule setup, `defaultRules`, or broader project settings. Successful responses must include: + - Project ID - Project slug - Project name @@ -87,6 +90,7 @@ slug and DSN and notes that the repository must be linked manually. ## Metadata Updates `update_project` is metadata-only. It updates only: + - `name` - `slug` - `platform` @@ -106,6 +110,7 @@ successful no-op response with the current team list. `remove_team_from_project` revokes team access and is marked destructive. It lists current project teams before deleting and rejects: + - Teams that are not currently assigned to the project - Removing the last assigned team @@ -116,6 +121,7 @@ accidentally leave a project without a team. Organization and project constraints are enforced by catalog availability and parameter injection: + - Organization-scoped sessions filter `organizationSlug` from schemas and inject the constrained organization. - Project-scoped sessions filter `projectSlug` from update/team-access schemas @@ -129,9 +135,11 @@ are filtered before handler execution. ## Migration Previous behavior mixed unrelated operations into project tools: + - `update_project` could grant team access through `teamSlug`. Use these replacements: + - Repository linking: use `create_project(..., repository: "owner/repo")` during creation. - Team grant: `add_team_to_project` @@ -140,6 +148,7 @@ Use these replacements: ## Testing Required coverage lives with the tools and catalog runtime: + - `packages/mcp-core/src/tools/catalog/create-project.test.ts` - `packages/mcp-core/src/tools/catalog/update-project.test.ts` - `packages/mcp-core/src/tools/catalog/add-team-to-project.test.ts` diff --git a/docs/specs/subpath-constraints.md b/docs/specs/subpath-constraints.md index 5a2808c51..f5cdd9eab 100644 --- a/docs/specs/subpath-constraints.md +++ b/docs/specs/subpath-constraints.md @@ -32,11 +32,13 @@ Examples: ## Access verification When you connect with a scoped path, we validate that: + - The slugs are well-formed - The organization exists and you have access - If a project is included, the project exists and you have access If there’s a problem, you’ll receive a clear HTTP error when connecting: + - 400: Invalid slug format - 401: Missing authentication - 403: You don’t have access to the specified org/project @@ -66,6 +68,7 @@ For Sentry Cloud, your organization may be hosted in a regional cluster. When yo ## Reference Supported URL patterns: + ``` /mcp/{organizationSlug}/{projectSlug} /mcp/{organizationSlug} @@ -77,5 +80,6 @@ Supported URL patterns: ``` For implementation details and security notes, see: + - `docs/specs/subpath-constraints.md` - `docs/architecture/overview.md` diff --git a/docs/testing/overview.md b/docs/testing/overview.md index 984018f22..e81d6f128 100644 --- a/docs/testing/overview.md +++ b/docs/testing/overview.md @@ -33,6 +33,7 @@ Our testing approach prioritizes **functional coverage** over implementation det ### What to Test ✅ **DO test:** + - Tool functionality: "Does find_issues return correct data?" - API integration: "Does our client handle API errors correctly?" - Error handling: "Do we provide helpful error messages?" @@ -40,6 +41,7 @@ Our testing approach prioritizes **functional coverage** over implementation det - Configuration: "Can the server be configured for different deployment modes?" ❌ **DON'T test:** + - Standard library behavior (Promise.all, Array.map, etc.) - Third-party package internals (Zod validation, MSW mocking, etc.) - Implementation details (private methods, internal state) @@ -49,6 +51,7 @@ Our testing approach prioritizes **functional coverage** over implementation det ### Example: Context Passing **Bad approach** (testing language features): + ```typescript // ❌ Don't test that closures capture variables it("closures capture context", async () => { @@ -59,6 +62,7 @@ it("closures capture context", async () => { ``` **Good approach** (testing our functionality): + ```typescript // ✅ Test that server configuration works it("builds server with context for tool handlers", async () => { @@ -70,7 +74,9 @@ it("builds server with context for tool handlers", async () => { ## Testing Levels ### 1. Functional Tests + Fast, focused tests of actual functionality: + - Located alongside source files (`*.test.ts`) - Use Vitest with inline snapshots - Mock external APIs only (Sentry API, OpenAI) with MSW @@ -89,13 +95,16 @@ Fast, focused tests of actual functionality: formatted output stays user-facing and avoids raw internals. ### 2. Evaluation Tests + Real-world scenarios with LLM: + - Located in `packages/mcp-server-evals` - Use actual AI models - Verify end-to-end functionality - Test complete workflows ### 3. Manual Testing + Interactive testing with the MCP test client (preferred for testing MCP changes): ```bash @@ -111,6 +120,7 @@ pnpm -w run cli --access-token=TOKEN "query" ``` **When to use manual testing:** + - Verifying end-to-end MCP server behavior - Testing OAuth flows - Debugging tool interactions @@ -120,6 +130,7 @@ pnpm -w run cli --access-token=TOKEN "query" **Note:** The CLI defaults to `http://localhost:5173` for easier local development. Override with `--mcp-host` or set `MCP_URL` environment variable to test against different servers. ### 4. Real Agent CLI Testing + Use the agent CLI harness when you need to verify behavior through the actual Claude Code or Codex client, not just the MCP test client. ```bash @@ -133,6 +144,7 @@ Use the agent CLI harness when you need to verify behavior through the actual Cl ``` This harness: + - Uses the real local CLI session for the selected provider - Checks the configured MCP server entry before running the prompt - Runs a real authenticated-user smoke prompt and verifies the final response contains an authenticated email @@ -142,6 +154,7 @@ Use `--setup repo --server sentry` to target the hosted server instead of the lo Because real clients launch stdio servers non-interactively, first-run device-code auth does not start inside Claude or Codex. Warm that cache from a real TTY first: ```bash + ``` When the harness fails, rerun the provider directly with debug enabled so you can inspect the exact MCP startup failure: @@ -168,9 +181,9 @@ describe("tool_name", () => { it("returns formatted output", async () => { const result = await TOOL_HANDLERS.tool_name(mockContext, { organizationSlug: "test-org", - param: "value" + param: "value", }); - + expect(result).toMatchInlineSnapshot(` "# Expected Output @@ -186,20 +199,19 @@ describe("tool_name", () => { ```typescript it("validates required parameters", async () => { - await expect( - TOOL_HANDLERS.tool_name(mockContext, {}) - ).rejects.toThrow(UserInputError); + await expect(TOOL_HANDLERS.tool_name(mockContext, {})).rejects.toThrow( + UserInputError, + ); }); it("handles API errors gracefully", async () => { server.use( - http.get("*/api/0/issues/*", () => - HttpResponse.json({ detail: "Not found" }, { status: 404 }) - ) + http.get("*/api/0/issues/*", () => + HttpResponse.json({ detail: "Not found" }, { status: 404 }), + ), ); - - await expect(handler(mockContext, params)) - .rejects.toThrow("Issue not found"); + + await expect(handler(mockContext, params)).rejects.toThrow("Issue not found"); }); ``` @@ -212,12 +224,14 @@ See [../contributing/api-patterns.md](../contributing/api-patterns.md) for MSW m ### When to Use Snapshots Use inline snapshots for: + - Tool output formatting - Error message text - Markdown responses - JSON structure validation For MCP tools specifically: + - Every tool test suite must include at least one representative successful call that snapshots the full handler response. - Use targeted substring assertions only for additional branch-specific checks, @@ -267,13 +281,13 @@ describeEval("tool-name", { data: async () => [ { input: "Natural language request", - expected: "Expected response content" - } + expected: "Expected response content", + }, ], - task: TaskRunner(), // Uses AI to call tools + task: TaskRunner(), // Uses AI to call tools scorers: [Factuality()], // Validates output threshold: 0.6, - timeout: 30000 + timeout: 30000, }); ``` @@ -298,7 +312,7 @@ import { issueFixture } from "@sentry-mcp/mocks"; const customIssue = { ...issueFixture, status: "resolved", - id: "CUSTOM-123" + id: "CUSTOM-123", }; ``` @@ -310,7 +324,7 @@ function createTestIssues(count: number) { return Array.from({ length: count }, (_, i) => ({ ...issueFixture, id: `TEST-${i}`, - title: `Test Issue ${i}` + title: `Test Issue ${i}`, })); } ``` @@ -320,12 +334,16 @@ function createTestIssues(count: number) { ### Timeout Configuration ```typescript -it("handles large datasets", async () => { - const largeDataset = createTestIssues(1000); - - const result = await handler(mockContext, params); - expect(result).toBeDefined(); -}, { timeout: 10000 }); // 10 second timeout +it( + "handles large datasets", + async () => { + const largeDataset = createTestIssues(1000); + + const result = await handler(mockContext, params); + expect(result).toBeDefined(); + }, + { timeout: 10000 }, +); // 10 second timeout ``` ### Memory Testing @@ -333,9 +351,9 @@ it("handles large datasets", async () => { ```typescript it("streams large responses efficiently", async () => { const initialMemory = process.memoryUsage().heapUsed; - + await processLargeDataset(); - + const memoryIncrease = process.memoryUsage().heapUsed - initialMemory; expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024); // < 50MB }); @@ -353,11 +371,13 @@ for mock setup. ## CI/CD Integration Tests run automatically on: + - Pull requests - Main branch commits - Pre-release checks Coverage requirements: + - Statements: 80% - Branches: 75% - Functions: 80% diff --git a/docs/testing/remote.md b/docs/testing/remote.md index d6357a8fb..f56b51164 100644 --- a/docs/testing/remote.md +++ b/docs/testing/remote.md @@ -7,6 +7,7 @@ Complete playbook for building, deploying, and testing the remote MCP server via The remote MCP server runs on Cloudflare Workers and provides HTTP-based access to the MCP protocol. Clients connect via HTTP/SSE instead of stdio pipes. **When to use remote:** + - Testing OAuth flows - Testing explicit Sentry API token auth over HTTP - Testing constraint-based access control (org/project filtering) @@ -15,6 +16,7 @@ The remote MCP server runs on Cloudflare Workers and provides HTTP-based access - Multi-user scenarios **When to use stdio instead:** + - Self-hosted Sentry without a Cloudflare deployment - IDE integration testing - Local development without network @@ -57,6 +59,7 @@ make setup-env ### 4. Configure Environment Variables **Edit `packages/mcp-cloudflare/.env`:** + ```bash SENTRY_CLIENT_ID=your_development_client_id SENTRY_CLIENT_SECRET=your_development_client_secret @@ -67,12 +70,14 @@ OPENAI_API_KEY=your-openai-key ``` **Edit `.env` (root):** + ```bash # For testing CLI client and search tools OPENAI_API_KEY=your-openai-key ``` **Generate COOKIE_SECRET:** + ```bash # Using OpenSSL openssl rand -base64 32 @@ -86,6 +91,7 @@ node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" ### Option 1: Local Development Server **Start the dev server:** + ```bash # From repo root pnpm dev @@ -98,6 +104,7 @@ pnpm dev Server runs at: `http://localhost:5173` **What this does:** + - Starts Cloudflare Workers local dev environment - Enables hot reload for code changes - Uses local KV storage (not persisted) @@ -127,6 +134,7 @@ pnpm -w run cli "your query" ### Basic Usage **Test against local dev server (default):** + ```bash # Single query pnpm -w run cli "who am I?" @@ -139,11 +147,13 @@ pnpm -w run cli ``` **Test against production:** + ```bash pnpm -w run cli --mcp-host=https://mcp.sentry.dev "who am I?" ``` **Test with specific MCP URL:** + ```bash # Custom deployment pnpm -w run cli --mcp-host=https://your-worker.workers.dev "query" @@ -153,15 +163,16 @@ export MCP_URL=https://mcp.sentry.dev pnpm -w run cli "query" ``` - ### OAuth Flow Testing **First run triggers OAuth:** + ```bash pnpm -w run cli "who am I?" ``` **Flow:** + 1. CLI opens browser to `http://localhost:5173/oauth/authorize` 2. You're redirected to Sentry OAuth 3. Approve access and grant permissions @@ -169,6 +180,7 @@ pnpm -w run cli "who am I?" 5. CLI receives tokens and executes query **Subsequent runs use cached tokens:** + - Tokens stored in `~/.sentry-mcp-tokens.json` - Automatically refreshed when expired - To force re-auth: delete the token file @@ -178,10 +190,10 @@ pnpm -w run cli "who am I?" The callback server defaults to `127.0.0.1:8765`, which a browser on the host cannot reach. Three environment variables override it: -| Variable | Default | Purpose | -| --- | --- | --- | -| `MCP_OAUTH_PORT` | `8765` | Port the callback server listens on | -| `MCP_OAUTH_HOST` | `127.0.0.1` | Address the callback server binds to | +| Variable | Default | Purpose | +| ------------------------ | ------------------------------------------- | ------------------------------------- | +| `MCP_OAUTH_PORT` | `8765` | Port the callback server listens on | +| `MCP_OAUTH_HOST` | `127.0.0.1` | Address the callback server binds to | | `MCP_OAUTH_REDIRECT_URI` | `http://localhost:$MCP_OAUTH_PORT/callback` | Redirect URI sent to the OAuth server | ```bash @@ -235,12 +247,14 @@ accept or reject the provided token for the requested org/project. Constraints limit access to specific organizations/projects: **Organization constraint:** + ```bash # Access limited to "sentry" org pnpm -w run cli --mcp-host=http://localhost:5173/mcp/sentry "list my projects" ``` **Organization + Project constraint:** + ```bash # Access limited to "sentry/javascript" pnpm -w run cli \ @@ -249,6 +263,7 @@ pnpm -w run cli \ ``` **Verify constraints work:** + ```bash # Should only return projects in "sentry" org pnpm -w run cli --mcp-host=http://localhost:5173/mcp/sentry "find_projects()" @@ -258,10 +273,12 @@ pnpm -w run cli --mcp-host=http://localhost:5173/mcp/sentry "find_projects(organ ``` **Verify OAuth consent shows the constraint:** + - Start a fresh OAuth flow against a constrained MCP URL such as `http://localhost:5173/mcp/sentry` or `http://localhost:5173/mcp/sentry/javascript` - On the approval page, confirm the "Session scope" banner matches the constrained organization or project before approving **Note:** When testing with constraints, some tools are automatically filtered: + - With org constraint: `find_organizations` is not available (18 tools instead of 19) - With org + project constraints: both `find_organizations` and `find_projects` are not available (17 tools instead of 19) @@ -272,33 +289,39 @@ The web UI provides a chat interface for testing the MCP server. ### Access the Interface **Local development:** + 1. Start dev server: `pnpm dev` 2. Open browser: `http://localhost:5173` 3. Follow OAuth flow to authenticate **Production:** + 1. Navigate to `https://mcp.sentry.dev` 2. Follow OAuth flow ### Testing Workflow **1. Authentication:** + - Click "Connect to Sentry" - Authorize the application - Grant permissions (select skills) **2. Basic Queries:** + - "Who am I?" - Test authentication - "Show my organizations" - Test data access - "List projects in [org-name]" - Test specific queries **3. Complex Operations:** + - "Find unresolved errors in [project]" - "Show me performance issues from yesterday" - "Search for 'database timeout' errors" **4. Test Constraints:** Navigate to constrained URL: + - `http://localhost:5173/mcp/your-org` - Org constraint - `http://localhost:5173/mcp/your-org/your-project` - Org + project constraint @@ -307,6 +330,7 @@ Verify queries are limited to that scope. ### Chat Interface Features **Available in chat:** + - Natural language queries - Tool call visualization - Streaming responses @@ -314,6 +338,7 @@ Verify queries are limited to that scope. - Token refresh handling **Not available (use CLI for these):** + - Direct tool calls with parameters - Agent mode toggle - Multiple simultaneous queries @@ -334,17 +359,20 @@ Opens at `http://localhost:6274` ### Connect to Remote Server **1. Add Server:** + - Click "Add Server" - Select "SSE" or "HTTP" transport - Enter URL: `http://localhost:5173/mcp` **2. Authenticate:** + - Click "Connect" - Browser opens for OAuth flow - Approve access - Redirected back to Inspector **3. Test Tools:** + - Click "List Tools" - Verify tools appear - Test individual tools with parameters - View responses and errors @@ -352,11 +380,13 @@ Opens at `http://localhost:6274` ### Inspector Testing Patterns **Basic verification:** + 1. List tools → Verify expected tools available 2. Call `execute_sentry_tool` with `name="whoami"` and `arguments={}` → Verify authentication 3. Call `find_organizations` → Verify data access **Parameter testing:** + ```json // Test find_projects with parameters { @@ -366,6 +396,7 @@ Opens at `http://localhost:6274` ``` **Error testing:** + ```json // Invalid org should error gracefully { @@ -374,6 +405,7 @@ Opens at `http://localhost:6274` ``` **Complex tool testing:** + ```json // Test search_events with AI { @@ -404,11 +436,13 @@ pnpm -w run cli "list organizations" ### 2. Test Skills Permissions **In OAuth approval screen, test:** + - Default selection: all active skills (inspect, seer, triage, project-management) - Minimal custom selection (inspect only) - Write-capable custom selection (triage, project-management) **Verify tools reflect the current session grant:** + ```bash # With read-only grants: no write tools pnpm -w run cli "search Sentry tools for create project" @@ -486,11 +520,13 @@ cat ~/.sentry-mcp-tokens.json | jq .expires_at ### "Failed to connect to MCP server" **Causes:** + 1. Dev server not running 2. Wrong URL 3. Network issues **Solution:** + ```bash # Verify dev server is running curl http://localhost:5173/ @@ -505,11 +541,13 @@ pnpm dev ### "OAuth authorization failed" **Causes:** + 1. Invalid client ID/secret 2. Redirect URI mismatch 3. Expired OAuth app **Solution:** + ```bash # Verify credentials in .env cat packages/mcp-cloudflare/.env | grep SENTRY_CLIENT @@ -526,6 +564,7 @@ cat packages/mcp-cloudflare/.env | grep SENTRY_CLIENT **Cause:** Insufficient skills granted during OAuth. **Solution:** + ```bash # Force re-authorization with more skills rm ~/.sentry-mcp-tokens.json @@ -537,11 +576,13 @@ pnpm -w run cli "who am I?" ### "Tool not found" errors **Causes:** + 1. Tool is not available in the current session 2. Build issue 3. Server version mismatch **Solution:** + ```bash # Check tool list pnpm -w run cli "list tools" | jq '.tools[] | .name' @@ -559,6 +600,7 @@ pnpm -w run build && pnpm dev **Cause:** Trying to access resources outside constrained scope. **Solution:** + ```bash # Verify constraint in URL echo "Accessing: http://localhost:5173/mcp/org-slug/project-slug" @@ -571,11 +613,13 @@ pnpm -w run cli --mcp-host=http://localhost:5173/mcp \ ### Web UI not loading **Causes:** + 1. Build failed 2. Assets not compiled 3. Wrangler issue **Solution:** + ```bash # Rebuild assets cd packages/mcp-cloudflare @@ -595,6 +639,7 @@ pnpm dev **Cause:** Too many requests to Sentry API. **Solution:** + ```bash # Wait for rate limit to reset (usually 60 seconds) @@ -670,17 +715,18 @@ it changes production traffic. Key differences to verify: -| Feature | Stdio | Remote | -|---------|-------|--------| -| Authentication | Access token | OAuth, or `Sentry-Bearer` access token | -| Constraints | Via CLI flags | Via URL path | -| Transport | stdin/stdout | HTTP/SSE | -| Multi-user | No | Yes | -| Token refresh | N/A | OAuth only; direct tokens are caller-managed | -| Web UI | No | Yes | -| Performance | Faster (no network) | Network latency | +| Feature | Stdio | Remote | +| -------------- | ------------------- | -------------------------------------------- | +| Authentication | Access token | OAuth, or `Sentry-Bearer` access token | +| Constraints | Via CLI flags | Via URL path | +| Transport | stdin/stdout | HTTP/SSE | +| Multi-user | No | Yes | +| Token refresh | N/A | OAuth only; direct tokens are caller-managed | +| Web UI | No | Yes | +| Performance | Faster (no network) | Network latency | **Test both work identically:** + ```bash # Stdio pnpm start --access-token=TOKEN diff --git a/docs/testing/stdio.md b/docs/testing/stdio.md index ef0c35e29..f2fac99ac 100644 --- a/docs/testing/stdio.md +++ b/docs/testing/stdio.md @@ -7,12 +7,14 @@ Complete playbook for building, running, and testing the stdio MCP server. The stdio transport runs the MCP server as a subprocess that communicates via stdin/stdout pipes. This is the standard way IDEs and local tools integrate with MCP servers. **When to use stdio:** + - Testing with IDEs (Cursor, VSCode with MCP extension) - Self-hosted Sentry deployments - Local development without OAuth - Direct API token authentication **When to use remote instead:** + - Testing OAuth flows - Testing constraint-based access control - Testing the web chat interface @@ -27,6 +29,7 @@ The stdio transport runs the MCP server as a subprocess that communicates via st ### Required Scopes For full functionality, create a Sentry User Auth Token with: + - `org:read` - List organizations - `project:read` - Access project data - `project:write` - Create/update projects @@ -62,6 +65,7 @@ pnpm build ``` The build process: + 1. Generates tool definitions (`pnpm run generate-definitions`) 2. Compiles TypeScript to JavaScript 3. Creates both ESM and CJS outputs @@ -141,6 +145,7 @@ This opens the MCP Inspector at `http://localhost:6274` 3. Configure the command: **For development (TypeScript):** + ```json { "command": "pnpm", @@ -154,6 +159,7 @@ This opens the MCP Inspector at `http://localhost:6274` ``` **For built package:** + ```json { "command": "node", @@ -165,6 +171,7 @@ This opens the MCP Inspector at `http://localhost:6274` ``` **For self-hosted Sentry:** + ```json { "command": "npx", @@ -177,6 +184,7 @@ This opens the MCP Inspector at `http://localhost:6274` ``` **For self-hosted Sentry on plain HTTP:** + ```json { "command": "npx", @@ -195,12 +203,14 @@ This opens the MCP Inspector at `http://localhost:6274` ### 3. Test Tools Interactively **Basic workflow:** + 1. **List Tools** - Verify expected tools appear 2. **Call a tool** - Start with `execute_sentry_tool` using `name="whoami"` and `arguments={}` 3. **Test with parameters** - Try `find_organizations()` 4. **Test complex operations** - Try `search_errors(query="errors in the last hour")` **Example test sequence:** + ``` 1. execute_sentry_tool(name="whoami", arguments={}) 2. find_organizations() @@ -226,6 +236,7 @@ The CLI client supports explicit transport selection: ### Basic Usage **Test stdio transport (local):** + ```bash # Single query pnpm -w run cli --transport stdio --access-token=YOUR_TOKEN "who am I?" @@ -242,6 +253,7 @@ pnpm -w run cli --transport stdio --list-tools **Verify stdio is being used:** Look for this in the output: + ``` ● Connected to MCP server (stdio) ⎿ tools available @@ -287,6 +299,7 @@ pnpm -w run cli --transport stdio --access-token=test-fake-token-12345 "who am I ``` **Expected output:** + ``` ● Connected to MCP server (stdio) ⎿ Tools available @@ -321,10 +334,7 @@ Create or edit `.cursor/config.json`: "mcpServers": { "sentry": { "command": "npx", - "args": [ - "@sentry/mcp-server@latest", - "--access-token=YOUR_TOKEN" - ] + "args": ["@sentry/mcp-server@latest", "--access-token=YOUR_TOKEN"] } } } @@ -333,11 +343,13 @@ Create or edit `.cursor/config.json`: **2. Use Environment Variables (Recommended):** Create `.cursor/.env`: + ```bash SENTRY_ACCESS_TOKEN=your-token-here ``` Update config: + ```json { "mcpServers": { @@ -353,6 +365,7 @@ Update config: ``` **3. Test in Cursor:** + - Restart Cursor - Open command palette (Cmd/Ctrl + Shift + P) - Search for "MCP" to verify server is connected @@ -361,27 +374,27 @@ Update config: ### VSCode with MCP Extension **1. Install MCP Extension:** + - Search for "Model Context Protocol" in VSCode extensions - Install the official MCP extension **2. Configure in VSCode Settings:** Add to `.vscode/settings.json`: + ```json { "mcp.servers": { "sentry": { "command": "npx", - "args": [ - "@sentry/mcp-server@latest", - "--access-token=YOUR_TOKEN" - ] + "args": ["@sentry/mcp-server@latest", "--access-token=YOUR_TOKEN"] } } } ``` **3. Test:** + - Reload window (Cmd/Ctrl + Shift + P → "Reload Window") - Use MCP-aware AI features to access Sentry data @@ -487,12 +500,12 @@ OPENAI_API_KEY=your-key pnpm start --access-token=TOKEN # - Call search_issues(query="unresolved crashes") ``` - ### "Command not found: npx @sentry/mcp-server" **Cause:** Package not published or not in npm registry. **Solution:** + ```bash # Use local build instead cd packages/mcp-server @@ -505,6 +518,7 @@ node dist/index.js --access-token=TOKEN **Cause:** No authentication provided. **Solution:** + ```bash # Option 1: CLI flag pnpm start --access-token=YOUR_TOKEN @@ -519,6 +533,7 @@ pnpm start **Cause:** Missing `OPENAI_API_KEY`. **Solution:** + ```bash # Set the API key export OPENAI_API_KEY=your-openai-key @@ -531,11 +546,13 @@ pnpm start --access-token=TOKEN ### "Cannot connect to Sentry API" **Causes:** + 1. Invalid access token 2. Wrong host configuration 3. Network issues **Solution:** + ```bash # Test token manually curl -H "Authorization: Bearer YOUR_TOKEN" \ @@ -552,11 +569,13 @@ pnpm start --access-token=TOKEN 2>&1 | tee debug.log ### "MCP Inspector can't connect to stdio server" **Causes:** + 1. Incorrect command path 2. Missing dependencies 3. Process exits immediately **Solution:** + ```bash # Test command manually first pnpm start --access-token=TOKEN @@ -577,6 +596,7 @@ pwd # Get absolute path to project **Cause:** Built executable not marked as executable. **Solution:** + ```bash chmod +x packages/mcp-server/dist/index.js ``` @@ -586,6 +606,7 @@ chmod +x packages/mcp-server/dist/index.js **Cause:** Missing dependencies in built output. **Solution:** + ```bash # Clean and rebuild cd packages/mcp-server @@ -624,6 +645,7 @@ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | \ To verify stdio behaves the same as remote: **Test with both transports:** + ```bash # 1. Test stdio locally pnpm start --access-token=TOKEN @@ -637,6 +659,7 @@ pnpm -w run cli --mcp-host=https://mcp.sentry.dev "who am I" ``` **Key differences to expect:** + - **Authentication:** Stdio uses access tokens, remote uses OAuth - **Constraints:** Remote supports URL-based org/project constraints - **Tools:** Both should have same tool count and functionality diff --git a/openspec/changes/refine-project-management-tools/design.md b/openspec/changes/refine-project-management-tools/design.md index 298a15abc..021871cda 100644 --- a/openspec/changes/refine-project-management-tools/design.md +++ b/openspec/changes/refine-project-management-tools/design.md @@ -9,6 +9,7 @@ The MCP catalog already includes `create_project`, `update_project`, `create_tea Sentry's upstream project creation endpoint creates the project under a team and emits normal project creation side effects, including default key creation through Sentry's project post-save hook. Agents still need the DSN in the tool response because creating a Sentry project is usually followed immediately by SDK setup. Endpoint validation: + - `~/src/sentry/src/sentry/core/endpoints/project_teams.py` lists project teams through `ProjectTeamsEndpoint.get`, backed by `OffsetPaginator` and cursor `Link` headers, so MCP must page through cursors before applying team-access safety checks. - `~/src/sentry/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py` persists repository/project associations through `OrganizationCodeMappingsBulkEndpoint.post` at `/organizations/{org}/code-mappings/bulk/`; it resolves the project by slug, resolves the repository by name/provider, infers the default branch when possible, and creates or updates `RepositoryProjectPathConfig` rows. diff --git a/openspec/changes/refine-project-management-tools/specs/project-management/spec.md b/openspec/changes/refine-project-management-tools/specs/project-management/spec.md index 174c678f9..b7d49bbd0 100644 --- a/openspec/changes/refine-project-management-tools/specs/project-management/spec.md +++ b/openspec/changes/refine-project-management-tools/specs/project-management/spec.md @@ -1,125 +1,158 @@ ## ADDED Requirements ### Requirement: Project management tools are skill-gated and catalog-only + The system SHALL expose project-management tools only through the searchable catalog when the `project-management` skill and required Sentry scopes are granted. #### Scenario: Project-management skill granted + - **WHEN** a session has the `project-management` skill and the required scopes for a project-management tool - **THEN** the tool is discoverable through `search_sentry_tools` and executable through `execute_sentry_tool` #### Scenario: Project-management skill absent + - **WHEN** a session does not have the `project-management` skill - **THEN** project-management tools are not discoverable or executable #### Scenario: Direct tool surface + - **WHEN** the MCP server registers top-level tools - **THEN** project-management tools are not added to the direct `tools/list` surface ### Requirement: Project creation returns a usable DSN + The `create_project` tool SHALL create a Sentry project and return a usable `SENTRY_DSN` in the same tool response. #### Scenario: Default DSN exists after project creation + - **WHEN** Sentry creates a default client key for the new project - **THEN** `create_project` returns that existing DSN instead of creating another client key #### Scenario: Default DSN is missing after project creation + - **WHEN** no client key exists after the project is created - **THEN** `create_project` creates a `Default` client key and returns its DSN #### Scenario: Project creation response + - **WHEN** `create_project` succeeds - **THEN** the response includes the project ID, project slug, project name, and `SENTRY_DSN` ### Requirement: Project creation accepts core setup fields + The `create_project` tool SHALL accept core project setup fields and optional repository linking. #### Scenario: Caller provides core project fields + - **WHEN** a caller provides `organizationSlug`, `teamSlug`, `name`, and optional `slug` or `platform` - **THEN** the tool sends the supported project creation fields to Sentry #### Scenario: Caller wants repository linking + - **WHEN** a caller provides a `repository` - **THEN** the tool resolves the repository before creating the project - **AND** creates a root code mapping for the created project and repository after project creation #### Scenario: Requested repository does not resolve + - **WHEN** a caller provides a `repository` that cannot be found or resolves ambiguously - **THEN** the tool returns a user input error before creating the project ### Requirement: Project creation respects active constraints + The system SHALL prevent project creation from escaping the active MCP constraints. #### Scenario: Organization-scoped session + - **WHEN** a session is constrained to an organization - **THEN** `create_project` uses that organization constraint and does not accept another organization #### Scenario: Project-scoped session + - **WHEN** a session is constrained to a specific project - **THEN** `create_project` is not available or rejects execution ### Requirement: Project metadata updates are separate from team access changes + The `update_project` tool SHALL only update project metadata fields. #### Scenario: Caller updates metadata + - **WHEN** a caller provides at least one of `name`, `slug`, or `platform` - **THEN** `update_project` updates only those project metadata fields #### Scenario: Caller attempts team assignment through update_project + - **WHEN** a caller provides team assignment arguments to `update_project` - **THEN** the tool schema rejects those arguments #### Scenario: Caller renames project in project-scoped session + - **WHEN** a project-scoped session invokes `update_project` with a `slug` value - **THEN** the tool returns a user input error explaining that slug changes require an organization-scoped or unconstrained session #### Scenario: Caller provides no metadata changes + - **WHEN** a caller invokes `update_project` without `name`, `slug`, or `platform` - **THEN** the tool returns a user input error explaining that at least one metadata field is required ### Requirement: Team access grants use an explicit tool + The `add_team_to_project` tool SHALL grant a team access to an existing project. #### Scenario: Team is not assigned to project + - **WHEN** a caller invokes `add_team_to_project` for a team that is not assigned to the project - **THEN** the tool adds the team and returns the updated project team list #### Scenario: Team is already assigned to project + - **WHEN** a caller invokes `add_team_to_project` for a team that already has access - **THEN** the tool returns a successful no-op response with the current project team list ### Requirement: Team access removals are guarded + The `remove_team_from_project` tool SHALL revoke a team's project access only after validating the current project team assignments. #### Scenario: Team is assigned and not the last team + - **WHEN** a caller invokes `remove_team_from_project` for an assigned team and at least one other team remains - **THEN** the tool removes the team and returns the remaining project team list #### Scenario: Team is not assigned to project + - **WHEN** a caller invokes `remove_team_from_project` for a team that is not assigned to the project - **THEN** the tool returns a user input error and does not call the delete endpoint #### Scenario: Team is the only assigned team + - **WHEN** a caller invokes `remove_team_from_project` for the project's only assigned team - **THEN** the tool returns a user input error and does not call the delete endpoint ### Requirement: Project team access tools respect active constraints + The project team access tools SHALL operate only within the active organization and project constraints. #### Scenario: Organization constraint + - **WHEN** a session is constrained to an organization - **THEN** team access tools use that organization constraint and do not accept another organization #### Scenario: Project constraint + - **WHEN** a session is constrained to a project - **THEN** team access tools use that project constraint and do not accept another project ### Requirement: Project-management responses are user-facing markdown + Project-management tools SHALL return concise markdown responses with follow-up identifiers and no raw API payloads. #### Scenario: Project operation succeeds + - **WHEN** a project-management tool completes successfully - **THEN** the response includes the relevant organization slug, project slug, IDs needed for follow-up calls, and short response notes #### Scenario: Access-changing operation succeeds + - **WHEN** a team access tool completes successfully - **THEN** the response clearly states whether access was granted or revoked and lists the current project teams diff --git a/package.json b/package.json index 0f63c9983..b4d09a6d5 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,9 @@ "eval": "pnpm --filter '@sentry/mcp-server-evals...' --if-present run build && dotenv -e .env -e .env.local -- pnpm --filter @sentry/mcp-server-evals run eval", "eval:ci": "pnpm --filter '@sentry/mcp-server-evals...' --if-present run build && CI=true dotenv -e .env -e .env.local -- pnpm --stream -r run eval:ci", "format": "oxfmt --write", - "lint": "oxlint --report-unused-disable-directives . && pnpm run lint:ast-grep", + "lint": "oxlint --report-unused-disable-directives . && pnpm run lint:format && pnpm run lint:ast-grep", "lint:ast-grep": "ast-grep scan && ast-grep test --skip-snapshot-tests", + "lint:format": "oxfmt --check", "lint:fix": "oxlint --fix .", "pre-commit:generated": "node scripts/pre-commit-generated.mjs", "inspector": "pnpx @modelcontextprotocol/inspector@latest", diff --git a/packages/mcp-cloudflare/index.html b/packages/mcp-cloudflare/index.html index 601cb087c..d0c0a4e57 100644 --- a/packages/mcp-cloudflare/index.html +++ b/packages/mcp-cloudflare/index.html @@ -1,120 +1,141 @@ + + + + Sentry MCP + + + + + + + + + - - - - Sentry MCP - - - - - - - - - + + + + + + - - - - - - + + + + + + + - - - - - - - - - - -
    - - -
    - - - +
    + + +
    + + diff --git a/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx b/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx index d6a74ee3d..e7429d89c 100644 --- a/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx +++ b/packages/mcp-cloudflare/src/client/components/chat/chat-message.tsx @@ -128,7 +128,9 @@ const ToolPart = memo(function ToolPart({ }); // Helper to check if a part is an AI SDK 6 tool part (type starts with "tool-") -const isToolPart = (part: { type: string }): part is { +const isToolPart = (part: { + type: string; +}): part is { type: `tool-${string}`; } & ChatToolInvocation => { return part.type.startsWith("tool-") && part.type !== "tool-invocation"; @@ -136,7 +138,9 @@ const isToolPart = (part: { type: string }): part is { // Helper to check if a part is a legacy tool-invocation part (AI SDK 4/5 format) // Legacy format: { type: "tool-invocation", toolInvocation: ChatToolInvocation } -const isLegacyToolInvocation = (part: { type: string }): part is { +const isLegacyToolInvocation = (part: { + type: string; +}): part is { type: "tool-invocation"; toolInvocation: ChatToolInvocation; } => { diff --git a/packages/mcp-cloudflare/src/client/components/fragments/setup-guide.tsx b/packages/mcp-cloudflare/src/client/components/fragments/setup-guide.tsx index afdc86425..233252ca5 100644 --- a/packages/mcp-cloudflare/src/client/components/fragments/setup-guide.tsx +++ b/packages/mcp-cloudflare/src/client/components/fragments/setup-guide.tsx @@ -9,7 +9,11 @@ export default function SetupGuide({ id, title, children, -}: { id: string; title: string; children: React.ReactNode }) { +}: { + id: string; + title: string; + children: React.ReactNode; +}) { return ( {title} diff --git a/packages/mcp-cloudflare/src/client/components/ui/badge.tsx b/packages/mcp-cloudflare/src/client/components/ui/badge.tsx index 27e16ae04..7bb1404db 100644 --- a/packages/mcp-cloudflare/src/client/components/ui/badge.tsx +++ b/packages/mcp-cloudflare/src/client/components/ui/badge.tsx @@ -24,7 +24,8 @@ const badgeVariants = cva( ); export interface BadgeProps - extends React.HTMLAttributes, + extends + React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { diff --git a/packages/mcp-cloudflare/src/client/index.css b/packages/mcp-cloudflare/src/client/index.css index 8db79e1df..929cae38d 100644 --- a/packages/mcp-cloudflare/src/client/index.css +++ b/packages/mcp-cloudflare/src/client/index.css @@ -170,8 +170,9 @@ body { @apply bg-background text-foreground overflow-x-clip; min-height: 100vh; - font-family: "Rubik", -apple-system, "BlinkMacSystemFont", "Segoe UI", - "Helvetica", "Arial", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + font-family: + "Rubik", -apple-system, "BlinkMacSystemFont", "Segoe UI", "Helvetica", + "Arial", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; } @@ -213,11 +214,9 @@ background-size: var(--size, 40px) var(--size, 40px); background-position: calc(-1 * var(--size, 40px) / 2) calc(-1 * var(--size, 40px) / 2); - background-image: linear-gradient( - to right, - var(--grid-color, #fff2) 1px, - transparent 1px - ), linear-gradient(to bottom, var(--grid-color, #fff2) 1px, transparent 1px); + background-image: + linear-gradient(to right, var(--grid-color, #fff2) 1px, transparent 1px), + linear-gradient(to bottom, var(--grid-color, #fff2) 1px, transparent 1px); } .bg-squares { diff --git a/packages/mcp-cloudflare/src/homepage-content.test.ts b/packages/mcp-cloudflare/src/homepage-content.test.ts index 17e9c0bbb..39b154dad 100644 --- a/packages/mcp-cloudflare/src/homepage-content.test.ts +++ b/packages/mcp-cloudflare/src/homepage-content.test.ts @@ -38,7 +38,9 @@ describe("homepage-content", () => { }); it("escapes HTML in generated content", () => { - const html = generateHomepageFallbackHtml('https://example.com/"onload="'); + const html = generateHomepageFallbackHtml( + 'https://example.com/"onload="', + ); expect(html).toContain("""); expect(html).not.toContain('onload="'); }); diff --git a/packages/mcp-cloudflare/src/server/metrics.ts b/packages/mcp-cloudflare/src/server/metrics.ts index 7480a68c6..96e584737 100644 --- a/packages/mcp-cloudflare/src/server/metrics.ts +++ b/packages/mcp-cloudflare/src/server/metrics.ts @@ -312,8 +312,7 @@ export function recordResponseMetric( } if (options?.oauthErrorReason) { - responseAttributes[OAUTH_ERROR_REASON_ATTRIBUTE] = - options.oauthErrorReason; + responseAttributes[OAUTH_ERROR_REASON_ATTRIBUTE] = options.oauthErrorReason; } if (options?.oauthBearerShape) { diff --git a/packages/mcp-cloudflare/src/server/oauth/index.ts b/packages/mcp-cloudflare/src/server/oauth/index.ts index 9b3c416a3..ed50670c8 100644 --- a/packages/mcp-cloudflare/src/server/oauth/index.ts +++ b/packages/mcp-cloudflare/src/server/oauth/index.ts @@ -8,7 +8,4 @@ export { SENTRY_TOKEN_URL, TokenResponseSchema, } from "./constants"; -export { - getUpstreamAuthorizeUrl, - exchangeCodeForAccessToken, -} from "./helpers"; +export { getUpstreamAuthorizeUrl, exchangeCodeForAccessToken } from "./helpers"; diff --git a/packages/mcp-cloudflare/src/server/oauth/telemetry.ts b/packages/mcp-cloudflare/src/server/oauth/telemetry.ts index 9b26a896c..a2bd7114a 100644 --- a/packages/mcp-cloudflare/src/server/oauth/telemetry.ts +++ b/packages/mcp-cloudflare/src/server/oauth/telemetry.ts @@ -39,14 +39,14 @@ export type OAuthErrorTelemetry = { */ export const ACCESS_METHOD_ATTRIBUTE = "app.access.method" as const; export const OAUTH_ERROR_ATTRIBUTE = "app.access.error.code" as const; -export const OAUTH_ERROR_REASON_ATTRIBUTE = - "app.access.error.reason" as const; +export const OAUTH_ERROR_REASON_ATTRIBUTE = "app.access.error.reason" as const; export const OAUTH_REQUEST_HEADER_SHAPE_ATTRIBUTE = "app.access.request.header_shape" as const; export const OAUTH_REFRESH_OUTCOME_ATTRIBUTE = "app.access.refresh.outcome" as const; export const OAUTH_GRANT_SHAPE_ATTRIBUTE = "app.access.grant.shape" as const; -export const OAUTH_GRANT_ID_HASH_ATTRIBUTE = "app.access.grant.id_hash" as const; +export const OAUTH_GRANT_ID_HASH_ATTRIBUTE = + "app.access.grant.id_hash" as const; export const OAUTH_GRANT_AGE_BUCKET_ATTRIBUTE = "app.access.grant.age_bucket" as const; export const OAUTH_GRANT_REVOKED_REASON_ATTRIBUTE = diff --git a/packages/mcp-cloudflare/wrangler.canary.jsonc b/packages/mcp-cloudflare/wrangler.canary.jsonc index c3a4b472d..e1b6405db 100644 --- a/packages/mcp-cloudflare/wrangler.canary.jsonc +++ b/packages/mcp-cloudflare/wrangler.canary.jsonc @@ -10,16 +10,16 @@ "compatibility_flags": [ "nodejs_compat", "nodejs_compat_populate_process_env", - "global_fetch_strictly_public" + "global_fetch_strictly_public", ], "keep_vars": true, "assets": { "directory": "./dist/client", "binding": "ASSETS", - "not_found_handling": "single-page-application" + "not_found_handling": "single-page-application", }, "version_metadata": { - "binding": "CF_VERSION_METADATA" + "binding": "CF_VERSION_METADATA", }, "vars": {}, "unsafe": { @@ -30,8 +30,8 @@ "namespace_id": "2001", "simple": { "limit": 10, - "period": 60 - } + "period": 60, + }, }, { "name": "SEARCH_RATE_LIMITER", @@ -39,8 +39,8 @@ "namespace_id": "2002", "simple": { "limit": 20, - "period": 60 - } + "period": 60, + }, }, { "name": "MCP_IP_RATE_LIMITER", @@ -48,8 +48,8 @@ "namespace_id": "2003", "simple": { "limit": 300, - "period": 60 - } + "period": 60, + }, }, { "name": "MCP_USER_RATE_LIMITER", @@ -57,38 +57,38 @@ "namespace_id": "2004", "simple": { "limit": 60, - "period": 60 - } - } - ] + "period": 60, + }, + }, + ], }, "kv_namespaces": [ { "binding": "OAUTH_KV", - "id": "a3fe0d23b2d34416930e284362a88a3b" + "id": "a3fe0d23b2d34416930e284362a88a3b", }, { "binding": "MCP_CACHE", - "id": "963817ecb25c4761a2710d3aedc6478d" - } + "id": "963817ecb25c4761a2710d3aedc6478d", + }, ], "ai": { - "binding": "AI" + "binding": "AI", }, "migrations": [ { - "tag": "v1" + "tag": "v1", }, { - "tag": "v2" - } + "tag": "v2", + }, ], "observability": { "enabled": true, - "head_sampling_rate": 1 + "head_sampling_rate": 1, }, "tail_consumers": [], "dev": { - "port": 8789 - } + "port": 8789, + }, } diff --git a/packages/mcp-cloudflare/wrangler.jsonc b/packages/mcp-cloudflare/wrangler.jsonc index a2ae5d657..68e5026b7 100644 --- a/packages/mcp-cloudflare/wrangler.jsonc +++ b/packages/mcp-cloudflare/wrangler.jsonc @@ -10,7 +10,7 @@ "compatibility_flags": [ "nodejs_compat", "nodejs_compat_populate_process_env", - "global_fetch_strictly_public" + "global_fetch_strictly_public", ], // we ask people to configure environment variables in prod // Available environment variables: @@ -19,10 +19,10 @@ "assets": { "directory": "./public", "binding": "ASSETS", - "not_found_handling": "single-page-application" + "not_found_handling": "single-page-application", }, "version_metadata": { - "binding": "CF_VERSION_METADATA" + "binding": "CF_VERSION_METADATA", }, "vars": {}, "unsafe": { @@ -33,8 +33,8 @@ "namespace_id": "1001", "simple": { "limit": 10, - "period": 60 - } + "period": 60, + }, }, { "name": "SEARCH_RATE_LIMITER", @@ -42,8 +42,8 @@ "namespace_id": "1002", "simple": { "limit": 20, - "period": 60 - } + "period": 60, + }, }, { "name": "MCP_IP_RATE_LIMITER", @@ -51,8 +51,8 @@ "namespace_id": "1003", "simple": { "limit": 300, - "period": 60 - } + "period": 60, + }, }, { "name": "MCP_USER_RATE_LIMITER", @@ -60,41 +60,41 @@ "namespace_id": "1004", "simple": { "limit": 60, - "period": 60 - } - } - ] + "period": 60, + }, + }, + ], }, "kv_namespaces": [ { "binding": "OAUTH_KV", - "id": "8dd5e9bafe1945298e2d5ca3b408a553" + "id": "8dd5e9bafe1945298e2d5ca3b408a553", }, { "binding": "MCP_CACHE", - "id": "01b5b45e9f3c4edaa9ef357b4f9949db" - } + "id": "01b5b45e9f3c4edaa9ef357b4f9949db", + }, ], "ai": { - "binding": "AI" + "binding": "AI", }, "migrations": [ { - "tag": "v1" + "tag": "v1", }, { - "tag": "v2" - } + "tag": "v2", + }, ], "observability": { "enabled": true, - "head_sampling_rate": 1 + "head_sampling_rate": 1, }, "tail_consumers": [ // super noisy - disable until it can be improve // { "service": "sentry-mcp-tail" } ], "dev": { - "port": 8788 - } + "port": 8788, + }, } diff --git a/packages/mcp-cloudflare/wrangler.test.jsonc b/packages/mcp-cloudflare/wrangler.test.jsonc index cab442cd6..b972909b7 100644 --- a/packages/mcp-cloudflare/wrangler.test.jsonc +++ b/packages/mcp-cloudflare/wrangler.test.jsonc @@ -7,7 +7,7 @@ "compatibility_flags": [ "nodejs_compat", "nodejs_compat_populate_process_env", - "global_fetch_strictly_public" + "global_fetch_strictly_public", ], "vars": { "COOKIE_SECRET": "test-cookie-secret-32-characters", @@ -16,16 +16,16 @@ "SENTRY_HOST": "sentry.io", "OPENROUTER_API_KEY": "test-openrouter-key", "OPENROUTER_MODEL": "openai/gpt-5.6-luna", - "EMBEDDED_AGENT_PROVIDER": "openrouter" + "EMBEDDED_AGENT_PROVIDER": "openrouter", }, "kv_namespaces": [ { "binding": "OAUTH_KV", - "id": "test-oauth-kv" - } + "id": "test-oauth-kv", + }, ], // Disable observability to avoid OTel issues in tests "observability": { - "enabled": false - } + "enabled": false, + }, } diff --git a/packages/mcp-core/README.md b/packages/mcp-core/README.md index 6d095f96a..734fee570 100644 --- a/packages/mcp-core/README.md +++ b/packages/mcp-core/README.md @@ -7,6 +7,7 @@ This package is primarily for running the `stdio` MCP server. If you do not know **Note:** Some tools require additional configuration: + - **AI-powered search tools** (the dataset search tools such as `search_errors`/`search_traces`/`search_logs`, and `search_issues`): These tools use a configured LLM provider to translate natural language queries into Sentry's query syntax. Set one provider key, such as `OPENAI_API_KEY` or `OPENROUTER_API_KEY`. Without a provider key, these specific tools will be unavailable, but all other tools will function normally. ## Authorization @@ -101,9 +102,11 @@ If `SENTRY_HOST` is not provided, the CLI automatically targets the Sentry SaaS To utilize the `stdio` transport, create a User Auth Token in Sentry with these scopes: **Minimum (read-only)**: + - `org:read`, `project:read`, `team:read`, `event:read` **Additional (for write operations)**: + - `event:write` - Required for `triage` skill - `project:write`, `team:write` - Required for `project-management` skill diff --git a/packages/mcp-core/src/internal/agents/tools/data/CLAUDE.md b/packages/mcp-core/src/internal/agents/tools/data/CLAUDE.md index a8437264f..85121bba0 100644 --- a/packages/mcp-core/src/internal/agents/tools/data/CLAUDE.md +++ b/packages/mcp-core/src/internal/agents/tools/data/CLAUDE.md @@ -81,7 +81,7 @@ The `otel-semantics-lookup.ts` tool reads these JSON files to provide semantic g data/ ├── CLAUDE.md # This documentation ├── gen_ai.json # Generative AI attributes -├── db.json # Database attributes +├── db.json # Database attributes ├── http.json # HTTP attributes ├── rpc.json # RPC attributes ├── messaging.json # Messaging attributes @@ -95,4 +95,4 @@ data/ - **Custom files**: Update manually as needed for non-OpenTelemetry attributes - **Validation**: Ensure all files follow the expected JSON schema format -The embedded AI agent uses these definitions to provide accurate semantic guidance when users query for things like "agent calls" or "tool calls" (maps to gen_ai.*) vs MCP protocol fields (maps to mcp.*). +The embedded AI agent uses these definitions to provide accurate semantic guidance when users query for things like "agent calls" or "tool calls" (maps to gen_ai._) vs MCP protocol fields (maps to mcp._). diff --git a/packages/mcp-core/src/internal/agents/tools/data/app.json b/packages/mcp-core/src/internal/agents/tools/data/app.json index 7e31b23ef..d173e692e 100644 --- a/packages/mcp-core/src/internal/agents/tools/data/app.json +++ b/packages/mcp-core/src/internal/agents/tools/data/app.json @@ -7,12 +7,7 @@ "description": "Scrubber-safe OAuth error code returned by Sentry MCP or the OAuth provider.", "type": "string", "note": "Emitted under app.access.* so default Sentry data scrubbing does not redact values via sensitive substrings (auth/oauth, token, bearer, credentials). invalid_token responses are bucketed as invalid_access.", - "examples": [ - "invalid_access", - "invalid_grant", - "invalid_client", - "other" - ] + "examples": ["invalid_access", "invalid_grant", "invalid_client", "other"] }, "app.access.error.reason": { "description": "Low-cardinality scrubber-safe bucket for an OAuth error description.", @@ -28,19 +23,12 @@ "app.access.grant.age_bucket": { "description": "Bounded MCP grant age at refresh or revocation.", "type": "string", - "examples": [ - "lt_1h", - "1d_7d", - "gte_30d", - "unknown" - ] + "examples": ["lt_1h", "1d_7d", "gte_30d", "unknown"] }, "app.access.grant.id_hash": { "description": "Deterministic non-secret fingerprint of a Sentry MCP OAuth grant ID used to correlate grant lifecycle logs.", "type": "string", - "examples": [ - "a1b2c3d4" - ] + "examples": ["a1b2c3d4"] }, "app.access.grant.revoked_reason": { "description": "Reason a Sentry MCP OAuth wrapper grant was revoked.", @@ -54,36 +42,22 @@ "app.access.grant.shape": { "description": "Shape of the Sentry MCP OAuth grant used during refresh.", "type": "string", - "examples": [ - "refreshable" - ] + "examples": ["refreshable"] }, "app.access.method": { "description": "How the MCP request obtained upstream Sentry access.", "type": "string", - "examples": [ - "mcp_grant", - "sentry_access" - ] + "examples": ["mcp_grant", "sentry_access"] }, "app.access.probe.reason": { "description": "Sentry MCP bucket for an indeterminate upstream OAuth access probe.", "type": "string", - "examples": [ - "rate_limit", - "server_error", - "unknown" - ] + "examples": ["rate_limit", "server_error", "unknown"] }, "app.access.probe.status_code": { "description": "HTTP status returned by the upstream Sentry access-validity probe.", "type": "string", - "examples": [ - "200", - "401", - "429", - "500" - ] + "examples": ["200", "401", "429", "500"] }, "app.access.refresh.outcome": { "description": "Outcome bucket for a Sentry MCP OAuth refresh/token-exchange path.", @@ -110,212 +84,135 @@ "app.access.upstream.expires_in_bucket": { "description": "Bounded remaining time before the original upstream Sentry expiry at refresh or revocation.", "type": "string", - "examples": [ - "expired", - "lt_1h", - "1d_7d", - "unknown" - ] + "examples": ["expired", "lt_1h", "1d_7d", "unknown"] }, "app.client.family": { "description": "Sentry MCP low-cardinality bucket for the MCP client family.", "type": "string", "note": "Resolved from the MCP client's User-Agent on direct MCP requests, or from the registered client name on browser-mediated OAuth endpoints.", - "examples": [ - "claude-code", - "cursor", - "codex", - "unknown" - ] + "examples": ["claude-code", "cursor", "codex", "unknown"] }, "app.consent.skill": { "description": "Sentry MCP skill granted during OAuth approval.", "type": "string", - "examples": [ - "inspect", - "docs", - "project-management" - ] + "examples": ["inspect", "docs", "project-management"] }, "app.consent.skill..granted": { "description": "Whether a normalized Sentry MCP skill was granted for a specific MCP request.", "type": "boolean", "note": "The dynamic segment uses the skill id with hyphens replaced by underscores.", - "examples": [ - "app.consent.skill.project_management.granted" - ] + "examples": ["app.consent.skill.project_management.granted"] }, "app.constraint.organization_slug": { "description": "Sentry MCP organization slug constraint applied to the MCP session.", "type": "string", - "examples": [ - "my-org" - ] + "examples": ["my-org"] }, "app.constraint.project_slug": { "description": "Sentry MCP project slug constraint applied to the MCP session.", "type": "string", - "examples": [ - "backend" - ] + "examples": ["backend"] }, "app.installation.id": { "description": "A unique identifier representing the installation of an application on a specific device\n", "type": "string", "note": "Its value SHOULD persist across launches of the same application installation, including through application upgrades.\nIt SHOULD change if the application is uninstalled or if all applications of the vendor are uninstalled.\nAdditionally, users might be able to reset this value (e.g. by clearing application data).\nIf an app is installed multiple times on the same device (e.g. in different accounts on Android), each `app.installation.id` SHOULD have a different value.\nIf multiple OpenTelemetry SDKs are used within the same application, they SHOULD use the same value for `app.installation.id`.\nHardware IDs (e.g. serial number, IMEI, MAC address) MUST NOT be used as the `app.installation.id`.\n\nFor iOS, this value SHOULD be equal to the [vendor identifier](https://developer.apple.com/documentation/uikit/uidevice/identifierforvendor).\n\nFor Android, examples of `app.installation.id` implementations include:\n\n- [Firebase Installation ID](https://firebase.google.com/docs/projects/manage-installations).\n- A globally unique UUID which is persisted across sessions in your application.\n- [App set ID](https://developer.android.com/identity/app-set-id).\n- [`Settings.getString(Settings.Secure.ANDROID_ID)`](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID).\n\nMore information about Android identifier best practices can be found [here](https://developer.android.com/training/articles/user-data-ids).\n", "stability": "development", - "examples": [ - "2ab2916d-a51f-4ac8-80ee-45ac31a28092" - ] + "examples": ["2ab2916d-a51f-4ac8-80ee-45ac31a28092"] }, "app.rate_limit.scope": { "description": "Scope of a Sentry MCP local rate-limit response.", "type": "string", - "examples": [ - "ip", - "user", - "sentry_access" - ], + "examples": ["ip", "user", "sentry_access"], "note": "sentry_access is the scrubber-safe bucket for direct Sentry access-header rate limits." }, "app.referrer.family": { "description": "Sentry MCP low-cardinality bucket for the external referrer host.", "type": "string", - "examples": [ - "sentry-docs", - "github", - "unknown" - ] + "examples": ["sentry-docs", "github", "unknown"] }, "app.request.duration_ms": { "description": "Sentry MCP request duration in milliseconds.", "type": "number", - "examples": [ - "42", - "1250" - ] + "examples": ["42", "1250"] }, "app.resource.type": { "description": "Sentry MCP resource type resolved by get_sentry_resource.", "type": "string", - "examples": [ - "issue", - "event", - "trace", - "replay" - ] + "examples": ["issue", "event", "trace", "replay"] }, "app.response.reason": { "description": "Sentry MCP local response reason bucket.", "type": "string", - "examples": [ - "local_rate_limit" - ] + "examples": ["local_rate_limit"] }, "app.response.status_class": { "description": "HTTP response status family for Sentry MCP response metrics.", "type": "string", - "examples": [ - "2xx", - "4xx", - "5xx" - ] + "examples": ["2xx", "4xx", "5xx"] }, "app.route.group": { "description": "Sentry MCP coarse route family.", "type": "string", - "examples": [ - "mcp", - "oauth", - "chat", - "search" - ] + "examples": ["mcp", "oauth", "chat", "search"] }, "app.screen.coordinate.x": { "description": "The x (horizontal) coordinate of a screen coordinate, in screen pixels.", "type": "number", "stability": "development", - "examples": [ - "0", - "131" - ] + "examples": ["0", "131"] }, "app.screen.coordinate.y": { "description": "The y (vertical) component of a screen coordinate, in screen pixels.\n", "type": "number", "stability": "development", - "examples": [ - "12", - "99" - ] + "examples": ["12", "99"] }, "app.server.mode.experimental": { "description": "Whether Sentry MCP experimental mode is enabled for the request or stdio session.", "type": "boolean", "note": "Metrics and tags record this as the low-cardinality string values `true` or `false`; spans record it as a boolean.", - "examples": [ - true, - false - ] + "examples": [true, false] }, "app.server.version": { "description": "Sentry MCP server package version.", "type": "string", - "examples": [ - "0.35.0" - ] + "examples": ["0.35.0"] }, "app.transport": { "description": "Sentry MCP product transport label.", "type": "string", - "examples": [ - "http", - "stdio" - ] + "examples": ["http", "stdio"] }, "app.upstream.host": { "description": "Configured upstream Sentry host for Sentry MCP.", "type": "string", - "examples": [ - "sentry.io" - ] + "examples": ["sentry.io"] }, "app.url.full": { "description": "Configured Sentry MCP URL for stdio telemetry.", "type": "string", "note": "Do not include credentials or access tokens in this URL.", - "examples": [ - "https://mcp.sentry.dev/mcp" - ] + "examples": ["https://mcp.sentry.dev/mcp"] }, "app.utm_source": { "description": "Sanitized in-product utm_source query parameter for Sentry MCP attribution.", "type": "string", - "examples": [ - "docs", - "product" - ] + "examples": ["docs", "product"] }, "app.widget.id": { "description": "An identifier that uniquely differentiates this widget from other widgets in the same application.\n", "type": "string", "note": "A widget is an application component, typically an on-screen visual GUI element.\n", "stability": "development", - "examples": [ - "f9bc787d-ff05-48ad-90e1-fca1d46130b3", - "submit_order_1829" - ] + "examples": ["f9bc787d-ff05-48ad-90e1-fca1d46130b3", "submit_order_1829"] }, "app.widget.name": { "description": "The name of an application widget.", "type": "string", "note": "A widget is an application component, typically an on-screen visual GUI element.\n", "stability": "development", - "examples": [ - "submit", - "attack", - "Clear Cart" - ] + "examples": ["submit", "attack", "Clear Cart"] } } } diff --git a/packages/mcp-core/src/internal/error-handling.test.ts b/packages/mcp-core/src/internal/error-handling.test.ts index 1d9505180..6648af8eb 100644 --- a/packages/mcp-core/src/internal/error-handling.test.ts +++ b/packages/mcp-core/src/internal/error-handling.test.ts @@ -1,8 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { - formatErrorForUser, - isExpectedToolError, -} from "./error-handling"; +import { formatErrorForUser, isExpectedToolError } from "./error-handling"; import { AgentExecutionError, UserInputError, @@ -85,7 +82,9 @@ describe("formatErrorForUser", () => { it("returns detailed message for stdio transport", async () => { const result = await formatErrorForUser(error, { transport: "stdio" }); - expect(result).toContain("Workspace monthly budget of $15000.00 exceeded"); + expect(result).toContain( + "Workspace monthly budget of $15000.00 exceeded", + ); expect(result).toContain("**AI Provider Error**"); expect(result).toContain("Other non-AI tools should still work"); expect(result).not.toContain("Feature Unavailable"); @@ -101,7 +100,9 @@ describe("formatErrorForUser", () => { it("returns graceful availability message for http transport without creating an issue", async () => { const result = await formatErrorForUser(error, { transport: "http" }); expect(result).toContain("**Feature Unavailable**"); - expect(result).toContain("AI-powered features are temporarily unavailable"); + expect(result).toContain( + "AI-powered features are temporarily unavailable", + ); expect(result).toContain("do not require AI should still work"); expect(result).not.toContain( "Workspace monthly budget of $15000.00 exceeded", @@ -118,7 +119,9 @@ describe("formatErrorForUser", () => { it("returns detailed message when transport is undefined", async () => { const result = await formatErrorForUser(error); - expect(result).toContain("Workspace monthly budget of $15000.00 exceeded"); + expect(result).toContain( + "Workspace monthly budget of $15000.00 exceeded", + ); expect(logIssue).not.toHaveBeenCalled(); expect(logWarn).toHaveBeenCalled(); }); @@ -145,7 +148,9 @@ describe("formatErrorForUser", () => { it("returns graceful availability message for http transport without creating an issue", async () => { const result = await formatErrorForUser(error, { transport: "http" }); expect(result).toContain("**Feature Unavailable**"); - expect(result).toContain("AI-powered features are temporarily unavailable"); + expect(result).toContain( + "AI-powered features are temporarily unavailable", + ); expect(result).not.toContain("Invalid API key provided"); expect(logIssue).not.toHaveBeenCalled(); expect(logWarn).toHaveBeenCalled(); @@ -170,7 +175,9 @@ describe("formatErrorForUser", () => { it("returns graceful availability message without creating an issue", async () => { const result = await formatErrorForUser(error, { transport: "http" }); - expect(result).toContain("AI-powered features are temporarily unavailable"); + expect(result).toContain( + "AI-powered features are temporarily unavailable", + ); expect(result).not.toContain("Internal server error"); expect(logIssue).not.toHaveBeenCalled(); expect(logWarn).toHaveBeenCalled(); @@ -206,7 +213,9 @@ describe("formatErrorForUser", () => { it("returns graceful availability message without creating an issue", async () => { const result = await formatErrorForUser(error, { transport: "http" }); - expect(result).toContain("AI-powered features are temporarily unavailable"); + expect(result).toContain( + "AI-powered features are temporarily unavailable", + ); expect(logIssue).not.toHaveBeenCalled(); expect(logWarn).toHaveBeenCalled(); }); diff --git a/packages/mcp-core/src/internal/issue-helpers.test.ts b/packages/mcp-core/src/internal/issue-helpers.test.ts index 8899b033d..caa272c0e 100644 --- a/packages/mcp-core/src/internal/issue-helpers.test.ts +++ b/packages/mcp-core/src/internal/issue-helpers.test.ts @@ -7,9 +7,8 @@ import { describe("extractIssueId", () => { it("should extract issue ID from a full Sentry URL", () => { - expect( - extractIssueId("https://sentry.sentry.io/issues/1234"), - ).toMatchInlineSnapshot(` + expect(extractIssueId("https://sentry.sentry.io/issues/1234")) + .toMatchInlineSnapshot(` { "issueId": "1234", "organizationSlug": "sentry", @@ -18,9 +17,8 @@ describe("extractIssueId", () => { }); it("should extract issue ID from a Sentry URL with organization in path", () => { - expect( - extractIssueId("https://sentry.io/sentry/issues/123"), - ).toMatchInlineSnapshot(` + expect(extractIssueId("https://sentry.io/sentry/issues/123")) + .toMatchInlineSnapshot(` { "issueId": "123", "organizationSlug": "sentry", @@ -29,9 +27,8 @@ describe("extractIssueId", () => { }); it("should extract issue ID and org slug from URL with organizations path", () => { - expect( - extractIssueId("https://sentry.io/organizations/my-org/issues/123"), - ).toMatchInlineSnapshot(` + expect(extractIssueId("https://sentry.io/organizations/my-org/issues/123")) + .toMatchInlineSnapshot(` { "issueId": "123", "organizationSlug": "my-org", @@ -47,9 +44,8 @@ describe("extractIssueId", () => { }); it("should extract issue ID and org slug from self-hosted Sentry with subdomain", () => { - expect( - extractIssueId("https://sentry.mycompany.com/issues/123"), - ).toMatchInlineSnapshot(` + expect(extractIssueId("https://sentry.mycompany.com/issues/123")) + .toMatchInlineSnapshot(` { "issueId": "123", "organizationSlug": "sentry", @@ -58,9 +54,8 @@ describe("extractIssueId", () => { }); it("should extract issue ID and org slug from self-hosted Sentry with organization path", () => { - expect( - extractIssueId("https://mycompany.com/my-team/issues/123"), - ).toMatchInlineSnapshot(` + expect(extractIssueId("https://mycompany.com/my-team/issues/123")) + .toMatchInlineSnapshot(` { "issueId": "123", "organizationSlug": "my-team", @@ -83,9 +78,8 @@ describe("extractIssueId", () => { }); it("should throw error for non-numeric issue ID in URL", () => { - expect( - extractIssueId("https://sentry.sentry.io/issues/abc"), - ).toMatchInlineSnapshot(` + expect(extractIssueId("https://sentry.sentry.io/issues/abc")) + .toMatchInlineSnapshot(` { "issueId": "abc", "organizationSlug": "sentry", diff --git a/packages/mcp-core/src/tools/catalog/get-alert-rule.ts b/packages/mcp-core/src/tools/catalog/get-alert-rule.ts index 039527f68..44772918a 100644 --- a/packages/mcp-core/src/tools/catalog/get-alert-rule.ts +++ b/packages/mcp-core/src/tools/catalog/get-alert-rule.ts @@ -130,9 +130,11 @@ export default defineTool({ ruleName: params.ruleIdOrName, }); matches.push( - ...issueRules.map( - (rule): AlertRuleMatch => ({ kind: "issue", rule, projectSlug }), - ), + ...issueRules.map((rule): AlertRuleMatch => ({ + kind: "issue", + rule, + projectSlug, + })), ); const metricMonitors = await findExactMetricMonitorMatches(apiService, { @@ -141,13 +143,11 @@ export default defineTool({ name: params.ruleIdOrName, }); matches.push( - ...metricMonitors.map( - (rule): AlertRuleMatch => ({ - kind: "metric", - rule, - projectSlug, - }), - ), + ...metricMonitors.map((rule): AlertRuleMatch => ({ + kind: "metric", + rule, + projectSlug, + })), ); if (matches.length === 0) { diff --git a/packages/mcp-core/src/tools/support/search-issues/README.md b/packages/mcp-core/src/tools/support/search-issues/README.md index 21829a44b..4dd476333 100644 --- a/packages/mcp-core/src/tools/support/search-issues/README.md +++ b/packages/mcp-core/src/tools/support/search-issues/README.md @@ -44,6 +44,6 @@ search_issues({ organizationSlug: "my-org", query: "critical bugs from last week", limit: 25, - includeExplanation: true -}) + includeExplanation: true, +}); ``` diff --git a/packages/mcp-server-mocks/src/fixtures.ts b/packages/mcp-server-mocks/src/fixtures.ts index 2f7f7b843..b050341e3 100644 --- a/packages/mcp-server-mocks/src/fixtures.ts +++ b/packages/mcp-server-mocks/src/fixtures.ts @@ -21,36 +21,16 @@ * ``` */ -import defaultEventFixtureJson from "./fixtures/default-event.json" with { - type: "json", -}; -import genericEventFixtureJson from "./fixtures/generic-event.json" with { - type: "json", -}; -import unknownEventFixtureJson from "./fixtures/unknown-event.json" with { - type: "json", -}; -import performanceIssueFixtureJson from "./fixtures/performance-issue.json" with { - type: "json", -}; -import regressedIssueFixtureJson from "./fixtures/regressed-issue.json" with { - type: "json", -}; -import unsupportedIssueFixtureJson from "./fixtures/unsupported-issue.json" with { - type: "json", -}; -import performanceEventFixtureJson from "./fixtures/performance-event.json" with { - type: "json", -}; -import cspIssueFixtureJson from "./fixtures/csp-issue.json" with { - type: "json", -}; -import cspEventFixtureJson from "./fixtures/csp-event.json" with { - type: "json", -}; -import feedbackIssueFixtureJson from "./fixtures/feedback-issue.json" with { - type: "json", -}; +import defaultEventFixtureJson from "./fixtures/default-event.json" with { type: "json" }; +import genericEventFixtureJson from "./fixtures/generic-event.json" with { type: "json" }; +import unknownEventFixtureJson from "./fixtures/unknown-event.json" with { type: "json" }; +import performanceIssueFixtureJson from "./fixtures/performance-issue.json" with { type: "json" }; +import regressedIssueFixtureJson from "./fixtures/regressed-issue.json" with { type: "json" }; +import unsupportedIssueFixtureJson from "./fixtures/unsupported-issue.json" with { type: "json" }; +import performanceEventFixtureJson from "./fixtures/performance-event.json" with { type: "json" }; +import cspIssueFixtureJson from "./fixtures/csp-issue.json" with { type: "json" }; +import cspEventFixtureJson from "./fixtures/csp-event.json" with { type: "json" }; +import feedbackIssueFixtureJson from "./fixtures/feedback-issue.json" with { type: "json" }; // Type helper for deep partial overrides type DeepPartial = T extends object diff --git a/packages/mcp-server-mocks/src/payloads.ts b/packages/mcp-server-mocks/src/payloads.ts index 5078d0fda..177194c4e 100644 --- a/packages/mcp-server-mocks/src/payloads.ts +++ b/packages/mcp-server-mocks/src/payloads.ts @@ -6,92 +6,40 @@ */ // Import JSON fixtures -import autofixStateFixture from "./fixtures/autofix-state.json" with { - type: "json", -}; -import autofixStateExplorerFixture from "./fixtures/autofix-state-explorer.json" with { - type: "json", -}; +import autofixStateFixture from "./fixtures/autofix-state.json" with { type: "json" }; +import autofixStateExplorerFixture from "./fixtures/autofix-state-explorer.json" with { type: "json" }; import clientKeyFixture from "./fixtures/client-key.json" with { type: "json" }; -import eventAttachmentsFixture from "./fixtures/event-attachments.json" with { - type: "json", -}; +import eventAttachmentsFixture from "./fixtures/event-attachments.json" with { type: "json" }; import eventFixture from "./fixtures/event.json" with { type: "json" }; -import eventsErrorsEmptyFixture from "./fixtures/events-errors-empty.json" with { - type: "json", -}; -import eventsErrorsFixture from "./fixtures/events-errors.json" with { - type: "json", -}; -import eventsSpansEmptyFixture from "./fixtures/events-spans-empty.json" with { - type: "json", -}; -import eventsSpansFixture from "./fixtures/events-spans.json" with { - type: "json", -}; -import flamegraphFixture from "./fixtures/flamegraph.json" with { - type: "json", -}; -import transactionProfileV1Fixture from "./fixtures/transaction-profile-v1.json" with { - type: "json", -}; -import transactionProfileV1MissingFunctionFixture from "./fixtures/transaction-profile-v1-missing-function.json" with { - type: "json", -}; +import eventsErrorsEmptyFixture from "./fixtures/events-errors-empty.json" with { type: "json" }; +import eventsErrorsFixture from "./fixtures/events-errors.json" with { type: "json" }; +import eventsSpansEmptyFixture from "./fixtures/events-spans-empty.json" with { type: "json" }; +import eventsSpansFixture from "./fixtures/events-spans.json" with { type: "json" }; +import flamegraphFixture from "./fixtures/flamegraph.json" with { type: "json" }; +import transactionProfileV1Fixture from "./fixtures/transaction-profile-v1.json" with { type: "json" }; +import transactionProfileV1MissingFunctionFixture from "./fixtures/transaction-profile-v1-missing-function.json" with { type: "json" }; import issueFixture from "./fixtures/issue.json" with { type: "json" }; -import issueNullCulpritFixture from "./fixtures/issue-null-culprit.json" with { - type: "json", -}; -import issueTagValuesFixture from "./fixtures/issue-tag-values.json" with { - type: "json", -}; -import organizationFixture from "./fixtures/organization.json" with { - type: "json", -}; -import performanceEventFixture from "./fixtures/performance-event.json" with { - type: "json", -}; +import issueNullCulpritFixture from "./fixtures/issue-null-culprit.json" with { type: "json" }; +import issueTagValuesFixture from "./fixtures/issue-tag-values.json" with { type: "json" }; +import organizationFixture from "./fixtures/organization.json" with { type: "json" }; +import performanceEventFixture from "./fixtures/performance-event.json" with { type: "json" }; import projectFixture from "./fixtures/project.json" with { type: "json" }; import releaseFixture from "./fixtures/release.json" with { type: "json" }; -import replayDetailsFixture from "./fixtures/replay-details.json" with { - type: "json", -}; -import replayRecordingSegmentsFixture from "./fixtures/replay-recording-segments.json" with { - type: "json", -}; +import replayDetailsFixture from "./fixtures/replay-details.json" with { type: "json" }; +import replayRecordingSegmentsFixture from "./fixtures/replay-recording-segments.json" with { type: "json" }; import tagsFixture from "./fixtures/tags.json" with { type: "json" }; import teamFixture from "./fixtures/team.json" with { type: "json" }; -import traceEventFixture from "./fixtures/trace-event.json" with { - type: "json", -}; -import traceItemsAttributesLogsNumberFixture from "./fixtures/trace-items-attributes-logs-number.json" with { - type: "json", -}; -import traceItemsAttributesLogsStringFixture from "./fixtures/trace-items-attributes-logs-string.json" with { - type: "json", -}; -import traceItemsAttributesSpansNumberFixture from "./fixtures/trace-items-attributes-spans-number.json" with { - type: "json", -}; -import traceItemsAttributesSpansStringFixture from "./fixtures/trace-items-attributes-spans-string.json" with { - type: "json", -}; -import traceItemsAttributesSpansNumberWithContextFixture from "./fixtures/trace-items-attributes-spans-number-with-context.json" with { - type: "json", -}; -import traceItemsAttributesSpansStringWithContextFixture from "./fixtures/trace-items-attributes-spans-string-with-context.json" with { - type: "json", -}; -import traceItemsAttributesFixture from "./fixtures/trace-items-attributes.json" with { - type: "json", -}; -import traceMetaWithNullsFixture from "./fixtures/trace-meta-with-nulls.json" with { - type: "json", -}; +import traceEventFixture from "./fixtures/trace-event.json" with { type: "json" }; +import traceItemsAttributesLogsNumberFixture from "./fixtures/trace-items-attributes-logs-number.json" with { type: "json" }; +import traceItemsAttributesLogsStringFixture from "./fixtures/trace-items-attributes-logs-string.json" with { type: "json" }; +import traceItemsAttributesSpansNumberFixture from "./fixtures/trace-items-attributes-spans-number.json" with { type: "json" }; +import traceItemsAttributesSpansStringFixture from "./fixtures/trace-items-attributes-spans-string.json" with { type: "json" }; +import traceItemsAttributesSpansNumberWithContextFixture from "./fixtures/trace-items-attributes-spans-number-with-context.json" with { type: "json" }; +import traceItemsAttributesSpansStringWithContextFixture from "./fixtures/trace-items-attributes-spans-string-with-context.json" with { type: "json" }; +import traceItemsAttributesFixture from "./fixtures/trace-items-attributes.json" with { type: "json" }; +import traceMetaWithNullsFixture from "./fixtures/trace-meta-with-nulls.json" with { type: "json" }; import traceMetaFixture from "./fixtures/trace-meta.json" with { type: "json" }; -import traceMixedFixture from "./fixtures/trace-mixed.json" with { - type: "json", -}; +import traceMixedFixture from "./fixtures/trace-mixed.json" with { type: "json" }; import traceFixture from "./fixtures/trace.json" with { type: "json" }; import userFixture from "./fixtures/user.json" with { type: "json" }; diff --git a/packages/mcp-server-mocks/src/utils.ts b/packages/mcp-server-mocks/src/utils.ts index 0a397e634..74fc4674f 100644 --- a/packages/mcp-server-mocks/src/utils.ts +++ b/packages/mcp-server-mocks/src/utils.ts @@ -10,9 +10,7 @@ export function setupMockServer(handlers: Array = []): SetupServer { * Start the MSW server with common configuration for Sentry MCP tests * This helper ensures consistent configuration across all test suites */ -export function startMockServer(options?: { - ignoreOpenAI?: boolean; -}): void { +export function startMockServer(options?: { ignoreOpenAI?: boolean }): void { const { ignoreOpenAI = true } = options || {}; mswServer.listen({ diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index d207bf96d..cd8a1d608 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -124,12 +124,13 @@ export async function runMcpServer( // Resolve access token before starting the transport. // For sentry.io without a token, this blocks on device code flow — // the client won't connect until the user has authenticated. - const cfg = await (options.resolveAccessToken - ? options.resolveAccessToken(partialCfg).then((accessToken) => ({ - ...partialCfg, - accessToken, - })) - : resolveAccessToken(partialCfg) + const cfg = await ( + options.resolveAccessToken + ? options.resolveAccessToken(partialCfg).then((accessToken) => ({ + ...partialCfg, + accessToken, + })) + : resolveAccessToken(partialCfg) ).catch((err) => { die(err); }); diff --git a/scripts/oxc-config.test.mjs b/scripts/oxc-config.test.mjs index 877cc3f67..6d8ed0804 100644 --- a/scripts/oxc-config.test.mjs +++ b/scripts/oxc-config.test.mjs @@ -58,3 +58,15 @@ test("editor and staged files use Oxc for root, MCP, and CLI", () => { assert.deepEqual(extensions.recommendations, ["oxc.oxc-vscode"]); assert.deepEqual(Object.keys(packageJson["lint-staged"]), ["*"]); }); + +test("CI lint checks formatting for commits that skip the pre-commit hook", () => { + const { scripts } = readRootJson("package.json"); + const workflow = readFileSync( + new URL("../.github/workflows/test.yml", import.meta.url), + "utf8", + ); + + assert.equal(scripts["lint:format"], "oxfmt --check"); + assert.match(scripts.lint, /pnpm run lint:format/); + assert.match(workflow, /^\s*- run: pnpm run lint$/m); +});