This is page 8 of 15. Use http://codebase.md/getsentry/sentry-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .claude
│ ├── agents
│ │ └── claude-optimizer.md
│ ├── commands
│ │ ├── gh-pr.md
│ │ └── gh-review.md
│ └── settings.json
├── .craft.yml
├── .cursor
│ ├── mcp.json
│ └── rules
├── .env.example
├── .github
│ └── workflows
│ ├── deploy.yml
│ ├── eval.yml
│ ├── merge-jobs.yml
│ ├── release.yml
│ ├── smoke-tests.yml
│ └── test.yml
├── .gitignore
├── .mcp.json
├── .vscode
│ ├── extensions.json
│ ├── mcp.json
│ └── settings.json
├── AGENTS.md
├── bin
│ └── bump-version.sh
├── biome.json
├── CLAUDE.md
├── codecov.yml
├── core
├── docs
│ ├── adding-tools.mdc
│ ├── api-patterns.mdc
│ ├── architecture.mdc
│ ├── cloudflare
│ │ ├── architecture.md
│ │ ├── constraint-do-analysis.md
│ │ ├── deployment.md
│ │ ├── mcpagent-architecture.md
│ │ ├── oauth-architecture.md
│ │ └── overview.md
│ ├── coding-guidelines.mdc
│ ├── common-patterns.mdc
│ ├── cursor.mdc
│ ├── deployment.mdc
│ ├── error-handling.mdc
│ ├── github-actions.mdc
│ ├── llms
│ │ ├── document-scopes.mdc
│ │ ├── documentation-style-guide.mdc
│ │ └── README.md
│ ├── logging.mdc
│ ├── monitoring.mdc
│ ├── permissions-and-scopes.md
│ ├── pr-management.mdc
│ ├── quality-checks.mdc
│ ├── README.md
│ ├── search-events-api-patterns.md
│ ├── security.mdc
│ ├── specs
│ │ ├── README.md
│ │ ├── search-events.md
│ │ └── subpath-constraints.md
│ └── testing.mdc
├── LICENSE.md
├── Makefile
├── package.json
├── packages
│ ├── mcp-cloudflare
│ │ ├── .env.example
│ │ ├── components.json
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── favicon.ico
│ │ │ ├── flow-transparent.png
│ │ │ └── flow.jpg
│ │ ├── src
│ │ │ ├── client
│ │ │ │ ├── app.tsx
│ │ │ │ ├── components
│ │ │ │ │ ├── chat
│ │ │ │ │ │ ├── auth-form.tsx
│ │ │ │ │ │ ├── chat-input.tsx
│ │ │ │ │ │ ├── chat-message.tsx
│ │ │ │ │ │ ├── chat-messages.tsx
│ │ │ │ │ │ ├── chat-ui.tsx
│ │ │ │ │ │ ├── chat.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── tool-invocation.tsx
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── fragments
│ │ │ │ │ │ ├── remote-setup.tsx
│ │ │ │ │ │ ├── setup-guide.tsx
│ │ │ │ │ │ └── stdio-setup.tsx
│ │ │ │ │ └── ui
│ │ │ │ │ ├── accordion.tsx
│ │ │ │ │ ├── backdrop.tsx
│ │ │ │ │ ├── base.tsx
│ │ │ │ │ ├── button.tsx
│ │ │ │ │ ├── code-snippet.tsx
│ │ │ │ │ ├── header.tsx
│ │ │ │ │ ├── icon.tsx
│ │ │ │ │ ├── icons
│ │ │ │ │ │ └── sentry.tsx
│ │ │ │ │ ├── interactive-markdown.tsx
│ │ │ │ │ ├── json-schema-params.tsx
│ │ │ │ │ ├── markdown.tsx
│ │ │ │ │ ├── note.tsx
│ │ │ │ │ ├── prose.tsx
│ │ │ │ │ ├── section.tsx
│ │ │ │ │ ├── slash-command-actions.tsx
│ │ │ │ │ ├── slash-command-text.tsx
│ │ │ │ │ ├── sliding-panel.tsx
│ │ │ │ │ ├── template-vars.tsx
│ │ │ │ │ ├── tool-actions.tsx
│ │ │ │ │ └── typewriter.tsx
│ │ │ │ ├── contexts
│ │ │ │ │ └── auth-context.tsx
│ │ │ │ ├── hooks
│ │ │ │ │ ├── use-mcp-metadata.ts
│ │ │ │ │ ├── use-persisted-chat.ts
│ │ │ │ │ ├── use-scroll-lock.ts
│ │ │ │ │ └── use-streaming-simulation.ts
│ │ │ │ ├── index.css
│ │ │ │ ├── instrument.ts
│ │ │ │ ├── lib
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── main.tsx
│ │ │ │ ├── pages
│ │ │ │ │ └── home.tsx
│ │ │ │ ├── utils
│ │ │ │ │ ├── chat-error-handler.ts
│ │ │ │ │ └── index.ts
│ │ │ │ └── vite-env.d.ts
│ │ │ ├── constants.ts
│ │ │ ├── server
│ │ │ │ ├── app.test.ts
│ │ │ │ ├── app.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── lib
│ │ │ │ │ ├── approval-dialog.test.ts
│ │ │ │ │ ├── approval-dialog.ts
│ │ │ │ │ ├── constraint-utils.test.ts
│ │ │ │ │ ├── constraint-utils.ts
│ │ │ │ │ ├── html-utils.ts
│ │ │ │ │ ├── mcp-agent.ts
│ │ │ │ │ ├── slug-validation.test.ts
│ │ │ │ │ └── slug-validation.ts
│ │ │ │ ├── logging.ts
│ │ │ │ ├── oauth
│ │ │ │ │ ├── authorize.test.ts
│ │ │ │ │ ├── callback.test.ts
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── helpers.test.ts
│ │ │ │ │ ├── helpers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── routes
│ │ │ │ │ │ ├── authorize.ts
│ │ │ │ │ │ ├── callback.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── state.ts
│ │ │ │ ├── routes
│ │ │ │ │ ├── chat-oauth.ts
│ │ │ │ │ ├── chat.ts
│ │ │ │ │ ├── mcp.ts
│ │ │ │ │ ├── metadata.ts
│ │ │ │ │ ├── search.test.ts
│ │ │ │ │ └── search.ts
│ │ │ │ ├── sentry.config.ts
│ │ │ │ ├── types
│ │ │ │ │ └── chat.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── auth-errors.ts
│ │ │ └── test-setup.ts
│ │ ├── tsconfig.client.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ ├── tsconfig.server.json
│ │ ├── vite.config.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ ├── wrangler.canary.jsonc
│ │ └── wrangler.jsonc
│ ├── mcp-server
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── scripts
│ │ │ ├── generate-definitions.ts
│ │ │ └── generate-otel-namespaces.ts
│ │ ├── src
│ │ │ ├── api-client
│ │ │ │ ├── client.test.ts
│ │ │ │ ├── client.ts
│ │ │ │ ├── errors.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── schema.ts
│ │ │ │ └── types.ts
│ │ │ ├── cli
│ │ │ │ ├── parse.test.ts
│ │ │ │ ├── parse.ts
│ │ │ │ ├── resolve.test.ts
│ │ │ │ ├── resolve.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── usage.ts
│ │ │ ├── constants.ts
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── index.ts
│ │ │ ├── internal
│ │ │ │ ├── agents
│ │ │ │ │ ├── callEmbeddedAgent.ts
│ │ │ │ │ ├── openai-provider.ts
│ │ │ │ │ └── tools
│ │ │ │ │ ├── data
│ │ │ │ │ │ ├── __namespaces.json
│ │ │ │ │ │ ├── android.json
│ │ │ │ │ │ ├── app.json
│ │ │ │ │ │ ├── artifact.json
│ │ │ │ │ │ ├── aspnetcore.json
│ │ │ │ │ │ ├── aws.json
│ │ │ │ │ │ ├── azure.json
│ │ │ │ │ │ ├── browser.json
│ │ │ │ │ │ ├── cassandra.json
│ │ │ │ │ │ ├── cicd.json
│ │ │ │ │ │ ├── CLAUDE.md
│ │ │ │ │ │ ├── client.json
│ │ │ │ │ │ ├── cloud.json
│ │ │ │ │ │ ├── cloudevents.json
│ │ │ │ │ │ ├── cloudfoundry.json
│ │ │ │ │ │ ├── code.json
│ │ │ │ │ │ ├── container.json
│ │ │ │ │ │ ├── cpu.json
│ │ │ │ │ │ ├── cpython.json
│ │ │ │ │ │ ├── database.json
│ │ │ │ │ │ ├── db.json
│ │ │ │ │ │ ├── deployment.json
│ │ │ │ │ │ ├── destination.json
│ │ │ │ │ │ ├── device.json
│ │ │ │ │ │ ├── disk.json
│ │ │ │ │ │ ├── dns.json
│ │ │ │ │ │ ├── dotnet.json
│ │ │ │ │ │ ├── elasticsearch.json
│ │ │ │ │ │ ├── enduser.json
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ ├── faas.json
│ │ │ │ │ │ ├── feature_flags.json
│ │ │ │ │ │ ├── file.json
│ │ │ │ │ │ ├── gcp.json
│ │ │ │ │ │ ├── gen_ai.json
│ │ │ │ │ │ ├── geo.json
│ │ │ │ │ │ ├── go.json
│ │ │ │ │ │ ├── graphql.json
│ │ │ │ │ │ ├── hardware.json
│ │ │ │ │ │ ├── heroku.json
│ │ │ │ │ │ ├── host.json
│ │ │ │ │ │ ├── http.json
│ │ │ │ │ │ ├── ios.json
│ │ │ │ │ │ ├── jvm.json
│ │ │ │ │ │ ├── k8s.json
│ │ │ │ │ │ ├── linux.json
│ │ │ │ │ │ ├── log.json
│ │ │ │ │ │ ├── mcp.json
│ │ │ │ │ │ ├── messaging.json
│ │ │ │ │ │ ├── network.json
│ │ │ │ │ │ ├── nodejs.json
│ │ │ │ │ │ ├── oci.json
│ │ │ │ │ │ ├── opentracing.json
│ │ │ │ │ │ ├── os.json
│ │ │ │ │ │ ├── otel.json
│ │ │ │ │ │ ├── peer.json
│ │ │ │ │ │ ├── process.json
│ │ │ │ │ │ ├── profile.json
│ │ │ │ │ │ ├── rpc.json
│ │ │ │ │ │ ├── server.json
│ │ │ │ │ │ ├── service.json
│ │ │ │ │ │ ├── session.json
│ │ │ │ │ │ ├── signalr.json
│ │ │ │ │ │ ├── source.json
│ │ │ │ │ │ ├── system.json
│ │ │ │ │ │ ├── telemetry.json
│ │ │ │ │ │ ├── test.json
│ │ │ │ │ │ ├── thread.json
│ │ │ │ │ │ ├── tls.json
│ │ │ │ │ │ ├── url.json
│ │ │ │ │ │ ├── user.json
│ │ │ │ │ │ ├── v8js.json
│ │ │ │ │ │ ├── vcs.json
│ │ │ │ │ │ ├── webengine.json
│ │ │ │ │ │ └── zos.json
│ │ │ │ │ ├── dataset-fields.test.ts
│ │ │ │ │ ├── dataset-fields.ts
│ │ │ │ │ ├── otel-semantics.test.ts
│ │ │ │ │ ├── otel-semantics.ts
│ │ │ │ │ ├── utils.ts
│ │ │ │ │ ├── whoami.test.ts
│ │ │ │ │ └── whoami.ts
│ │ │ │ ├── constraint-helpers.test.ts
│ │ │ │ ├── constraint-helpers.ts
│ │ │ │ ├── error-handling.ts
│ │ │ │ ├── fetch-utils.test.ts
│ │ │ │ ├── fetch-utils.ts
│ │ │ │ ├── formatting.test.ts
│ │ │ │ ├── formatting.ts
│ │ │ │ ├── issue-helpers.test.ts
│ │ │ │ ├── issue-helpers.ts
│ │ │ │ ├── test-fixtures.ts
│ │ │ │ └── tool-helpers
│ │ │ │ ├── api.test.ts
│ │ │ │ ├── api.ts
│ │ │ │ ├── define.ts
│ │ │ │ ├── enhance-error.ts
│ │ │ │ ├── formatting.ts
│ │ │ │ ├── issue.ts
│ │ │ │ ├── seer.test.ts
│ │ │ │ ├── seer.ts
│ │ │ │ ├── validate-region-url.test.ts
│ │ │ │ └── validate-region-url.ts
│ │ │ ├── permissions.parseScopes.test.ts
│ │ │ ├── permissions.ts
│ │ │ ├── schema.ts
│ │ │ ├── server.ts
│ │ │ ├── telem
│ │ │ │ ├── index.ts
│ │ │ │ ├── logging.ts
│ │ │ │ ├── sentry.test.ts
│ │ │ │ └── sentry.ts
│ │ │ ├── test-setup.ts
│ │ │ ├── test-utils
│ │ │ │ └── context.ts
│ │ │ ├── toolDefinitions.ts
│ │ │ ├── tools
│ │ │ │ ├── analyze-issue-with-seer.test.ts
│ │ │ │ ├── analyze-issue-with-seer.ts
│ │ │ │ ├── create-dsn.test.ts
│ │ │ │ ├── create-dsn.ts
│ │ │ │ ├── create-project.test.ts
│ │ │ │ ├── create-project.ts
│ │ │ │ ├── create-team.test.ts
│ │ │ │ ├── create-team.ts
│ │ │ │ ├── find-dsns.test.ts
│ │ │ │ ├── find-dsns.ts
│ │ │ │ ├── find-organizations.test.ts
│ │ │ │ ├── find-organizations.ts
│ │ │ │ ├── find-projects.test.ts
│ │ │ │ ├── find-projects.ts
│ │ │ │ ├── find-releases.test.ts
│ │ │ │ ├── find-releases.ts
│ │ │ │ ├── find-teams.test.ts
│ │ │ │ ├── find-teams.ts
│ │ │ │ ├── get-doc.test.ts
│ │ │ │ ├── get-doc.ts
│ │ │ │ ├── get-event-attachment.test.ts
│ │ │ │ ├── get-event-attachment.ts
│ │ │ │ ├── get-issue-details.test.ts
│ │ │ │ ├── get-issue-details.ts
│ │ │ │ ├── get-trace-details.test.ts
│ │ │ │ ├── get-trace-details.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── search-docs.test.ts
│ │ │ │ ├── search-docs.ts
│ │ │ │ ├── search-events
│ │ │ │ │ ├── agent.ts
│ │ │ │ │ ├── CLAUDE.md
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── formatters.ts
│ │ │ │ │ ├── handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── utils.test.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── search-events.test.ts
│ │ │ │ ├── search-issues
│ │ │ │ │ ├── agent.ts
│ │ │ │ │ ├── CLAUDE.md
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── formatters.ts
│ │ │ │ │ ├── handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── README.md
│ │ │ │ ├── tools.test.ts
│ │ │ │ ├── types.ts
│ │ │ │ ├── update-issue.test.ts
│ │ │ │ ├── update-issue.ts
│ │ │ │ ├── update-project.test.ts
│ │ │ │ ├── update-project.ts
│ │ │ │ ├── whoami.test.ts
│ │ │ │ └── whoami.ts
│ │ │ ├── transports
│ │ │ │ └── stdio.ts
│ │ │ ├── types.ts
│ │ │ ├── utils
│ │ │ │ ├── slug-validation.test.ts
│ │ │ │ ├── slug-validation.ts
│ │ │ │ ├── url-utils.test.ts
│ │ │ │ └── url-utils.ts
│ │ │ └── version.ts
│ │ ├── tsconfig.json
│ │ ├── tsdown.config.ts
│ │ └── vitest.config.ts
│ ├── mcp-server-evals
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── bin
│ │ │ │ └── start-mock-stdio.ts
│ │ │ ├── evals
│ │ │ │ ├── autofix.eval.ts
│ │ │ │ ├── create-dsn.eval.ts
│ │ │ │ ├── create-project.eval.ts
│ │ │ │ ├── create-team.eval.ts
│ │ │ │ ├── get-issue.eval.ts
│ │ │ │ ├── get-trace-details.eval.ts
│ │ │ │ ├── list-dsns.eval.ts
│ │ │ │ ├── list-issues.eval.ts
│ │ │ │ ├── list-organizations.eval.ts
│ │ │ │ ├── list-projects.eval.ts
│ │ │ │ ├── list-releases.eval.ts
│ │ │ │ ├── list-tags.eval.ts
│ │ │ │ ├── list-teams.eval.ts
│ │ │ │ ├── search-docs.eval.ts
│ │ │ │ ├── search-events-agent.eval.ts
│ │ │ │ ├── search-events.eval.ts
│ │ │ │ ├── search-issues-agent.eval.ts
│ │ │ │ ├── search-issues.eval.ts
│ │ │ │ ├── update-issue.eval.ts
│ │ │ │ ├── update-project.eval.ts
│ │ │ │ └── utils
│ │ │ │ ├── fixtures.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runner.ts
│ │ │ │ ├── structuredOutputScorer.ts
│ │ │ │ └── toolPredictionScorer.ts
│ │ │ └── setup-env.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── mcp-server-mocks
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── fixtures
│ │ │ │ ├── autofix-state.json
│ │ │ │ ├── event-attachments.json
│ │ │ │ ├── event.json
│ │ │ │ ├── issue.json
│ │ │ │ ├── performance-event.json
│ │ │ │ ├── project.json
│ │ │ │ ├── tags.json
│ │ │ │ ├── team.json
│ │ │ │ ├── trace-event.json
│ │ │ │ ├── trace-items-attributes-logs-number.json
│ │ │ │ ├── trace-items-attributes-logs-string.json
│ │ │ │ ├── trace-items-attributes-spans-number.json
│ │ │ │ ├── trace-items-attributes-spans-string.json
│ │ │ │ ├── trace-items-attributes.json
│ │ │ │ ├── trace-meta-with-nulls.json
│ │ │ │ ├── trace-meta.json
│ │ │ │ ├── trace-mixed.json
│ │ │ │ └── trace.json
│ │ │ ├── index.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ └── tsdown.config.ts
│ ├── mcp-server-tsconfig
│ │ ├── package.json
│ │ ├── tsconfig.base.json
│ │ └── tsconfig.vite.json
│ ├── mcp-test-client
│ │ ├── .env.test
│ │ ├── .gitignore
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── agent.ts
│ │ │ ├── auth
│ │ │ │ ├── config.ts
│ │ │ │ └── oauth.ts
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── logger.test.ts
│ │ │ ├── logger.ts
│ │ │ ├── mcp-test-client-remote.ts
│ │ │ ├── mcp-test-client.ts
│ │ │ ├── types.ts
│ │ │ └── version.ts
│ │ ├── tsconfig.json
│ │ ├── tsdown.config.ts
│ │ └── vitest.config.ts
│ └── smoke-tests
│ ├── package.json
│ ├── src
│ │ └── smoke.test.ts
│ └── vitest.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── scripts
│ └── check-doc-links.mjs
├── turbo.json
└── vitest.workspace.ts
```
# Files
--------------------------------------------------------------------------------
/packages/mcp-server/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * MCP Server Configuration and Request Handling Infrastructure.
3 | *
4 | * This module orchestrates tool execution and telemetry collection
5 | * in a unified server interface for LLMs.
6 | *
7 | * **Configuration Example:**
8 | * ```typescript
9 | * const server = new McpServer();
10 | * const context: ServerContext = {
11 | * accessToken: "your-sentry-token",
12 | * host: "sentry.io",
13 | * userId: "user-123",
14 | * clientId: "mcp-client"
15 | * };
16 | *
17 | * await configureServer({ server, context });
18 | * ```
19 | */
20 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21 | import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
22 | import type {
23 | ServerRequest,
24 | ServerNotification,
25 | } from "@modelcontextprotocol/sdk/types.js";
26 | import tools from "./tools/index";
27 | import type { ServerContext } from "./types";
28 | import { setTag, setUser, startNewTrace, startSpan } from "@sentry/core";
29 | import { getLogger, logIssue, type LogIssueOptions } from "./telem/logging";
30 | import { formatErrorForUser } from "./internal/error-handling";
31 | import { LIB_VERSION } from "./version";
32 | import { DEFAULT_SCOPES, MCP_SERVER_NAME } from "./constants";
33 | import { isToolAllowed, type Scope } from "./permissions";
34 | import {
35 | getConstraintKeysToFilter,
36 | getConstraintParametersToInject,
37 | } from "./internal/constraint-helpers";
38 |
39 | const toolLogger = getLogger(["server", "tools"]);
40 |
41 | /**
42 | * Extracts MCP request parameters for OpenTelemetry attributes.
43 | *
44 | * @example Parameter Transformation
45 | * ```typescript
46 | * const input = { organizationSlug: "my-org", query: "is:unresolved" };
47 | * const output = extractMcpParameters(input);
48 | * // { "mcp.request.argument.organizationSlug": "\"my-org\"", "mcp.request.argument.query": "\"is:unresolved\"" }
49 | * ```
50 | */
51 | function extractMcpParameters(args: Record<string, unknown>) {
52 | return Object.fromEntries(
53 | Object.entries(args).map(([key, value]) => {
54 | return [`mcp.request.argument.${key}`, JSON.stringify(value)];
55 | }),
56 | );
57 | }
58 |
59 | /**
60 | * Configures an MCP server with all tools and telemetry.
61 | *
62 | * Transforms a bare MCP server instance into a fully-featured Sentry integration
63 | * with comprehensive observability, error handling, and handler registration.
64 | *
65 | * @example Basic Configuration
66 | * ```typescript
67 | * const server = new McpServer();
68 | * const context = {
69 | * accessToken: process.env.SENTRY_TOKEN,
70 | * host: "sentry.io",
71 | * userId: "user-123",
72 | * clientId: "cursor-ide"
73 | * };
74 | *
75 | * await configureServer({ server, context });
76 | * ```
77 | */
78 | export async function configureServer({
79 | server,
80 | context,
81 | onToolComplete,
82 | }: {
83 | server: McpServer;
84 | context: ServerContext;
85 | onToolComplete?: () => void;
86 | }) {
87 | // Get granted scopes with default to read-only scopes
88 | // Normalize to a mutable Set regardless of input being Set or ReadonlySet
89 | const grantedScopes: Set<Scope> = context.grantedScopes
90 | ? new Set<Scope>(context.grantedScopes)
91 | : new Set<Scope>(DEFAULT_SCOPES);
92 |
93 | server.server.onerror = (error) => {
94 | const transportLogOptions: LogIssueOptions = {
95 | loggerScope: ["server", "transport"] as const,
96 | contexts: {
97 | transport: {
98 | phase: "server.onerror",
99 | },
100 | },
101 | };
102 |
103 | logIssue(error, transportLogOptions);
104 | };
105 |
106 | for (const [toolKey, tool] of Object.entries(tools)) {
107 | // Check if this tool is allowed based on granted scopes
108 | if (!isToolAllowed(tool.requiredScopes, grantedScopes)) {
109 | toolLogger.debug(
110 | "Skipping tool {tool} - missing required scopes",
111 | () => ({
112 | tool: tool.name,
113 | requiredScopes: Array.isArray(tool.requiredScopes)
114 | ? tool.requiredScopes
115 | : tool.requiredScopes
116 | ? Array.from(tool.requiredScopes)
117 | : [],
118 | grantedScopes: [...grantedScopes],
119 | }),
120 | );
121 | continue;
122 | }
123 |
124 | // Determine which constraint parameters should be filtered from the schema
125 | // because they will be injected at runtime
126 | const toolConstraintKeys = getConstraintKeysToFilter(
127 | context.constraints,
128 | tool.inputSchema,
129 | );
130 |
131 | // Create modified schema by removing constraint parameters that will be injected
132 | const modifiedInputSchema = Object.fromEntries(
133 | Object.entries(tool.inputSchema).filter(
134 | ([key, _]) => !toolConstraintKeys.includes(key),
135 | ),
136 | );
137 |
138 | server.tool(
139 | tool.name,
140 | tool.description,
141 | modifiedInputSchema,
142 | tool.annotations,
143 | async (
144 | params: any,
145 | extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
146 | ) => {
147 | try {
148 | return await startNewTrace(async () => {
149 | return await startSpan(
150 | {
151 | name: `tools/call ${tool.name}`,
152 | attributes: {
153 | "mcp.tool.name": tool.name,
154 | "mcp.server.name": MCP_SERVER_NAME,
155 | "mcp.server.version": LIB_VERSION,
156 | ...(context.constraints.organizationSlug && {
157 | "sentry-mcp.constraint-organization":
158 | context.constraints.organizationSlug,
159 | }),
160 | ...(context.constraints.projectSlug && {
161 | "sentry-mcp.constraint-project":
162 | context.constraints.projectSlug,
163 | }),
164 | ...extractMcpParameters(params || {}),
165 | },
166 | },
167 | async (span) => {
168 | if (context.userId) {
169 | setUser({
170 | id: context.userId,
171 | });
172 | }
173 | if (context.clientId) {
174 | setTag("client.id", context.clientId);
175 | }
176 |
177 | try {
178 | // Double-check scopes at runtime (defense in depth)
179 | if (!isToolAllowed(tool.requiredScopes, grantedScopes)) {
180 | throw new Error(
181 | `Tool '${tool.name}' is not allowed - missing required scopes`,
182 | );
183 | }
184 |
185 | // Apply constraints as parameters, handling aliases (e.g., projectSlug → projectSlugOrId)
186 | const applicableConstraints = getConstraintParametersToInject(
187 | context.constraints,
188 | tool.inputSchema,
189 | );
190 |
191 | const paramsWithConstraints = {
192 | ...params,
193 | ...applicableConstraints,
194 | };
195 |
196 | const output = await tool.handler(
197 | paramsWithConstraints,
198 | context,
199 | );
200 | span.setStatus({
201 | code: 1, // ok
202 | });
203 | // if the tool returns a string, assume it's a message
204 | if (typeof output === "string") {
205 | return {
206 | content: [
207 | {
208 | type: "text" as const,
209 | text: output,
210 | },
211 | ],
212 | };
213 | }
214 | // if the tool returns a list, assume it's a content list
215 | if (Array.isArray(output)) {
216 | return {
217 | content: output,
218 | };
219 | }
220 | throw new Error(`Invalid tool output: ${output}`);
221 | } catch (error) {
222 | span.setStatus({
223 | code: 2, // error
224 | });
225 |
226 | // CRITICAL: Tool errors MUST be returned as formatted text responses,
227 | // NOT thrown as exceptions. This ensures consistent error handling
228 | // and prevents the MCP client from receiving raw error objects.
229 | //
230 | // The logAndFormatError function provides user-friendly error messages
231 | // with appropriate formatting for different error types:
232 | // - UserInputError: Clear guidance for fixing input problems
233 | // - ConfigurationError: Clear guidance for fixing configuration issues
234 | // - ApiError: HTTP status context with helpful messaging
235 | // - System errors: Sentry event IDs for debugging
236 | //
237 | // DO NOT change this to throw error - it breaks error handling!
238 | return {
239 | content: [
240 | {
241 | type: "text" as const,
242 | text: await formatErrorForUser(error),
243 | },
244 | ],
245 | isError: true,
246 | };
247 | }
248 | },
249 | );
250 | });
251 | } finally {
252 | onToolComplete?.();
253 | }
254 | },
255 | );
256 | }
257 | }
258 |
```
--------------------------------------------------------------------------------
/packages/mcp-test-client/src/logger.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2 | import {
3 | logError,
4 | logSuccess,
5 | logInfo,
6 | logUser,
7 | logTool,
8 | logToolResult,
9 | logStreamStart,
10 | logStreamWrite,
11 | logStreamEnd,
12 | } from "./logger.js";
13 |
14 | describe("Logger", () => {
15 | let processStdoutWriteSpy: ReturnType<typeof vi.spyOn>;
16 | let capturedOutput: string;
17 |
18 | beforeEach(() => {
19 | capturedOutput = "";
20 | processStdoutWriteSpy = vi
21 | .spyOn(process.stdout, "write")
22 | .mockImplementation((chunk) => {
23 | capturedOutput += chunk;
24 | return true;
25 | });
26 | });
27 |
28 | afterEach(() => {
29 | vi.restoreAllMocks();
30 | });
31 |
32 | describe("single log functions", () => {
33 | it("logError without detail", () => {
34 | logError("Test error");
35 | expect(capturedOutput).toMatchInlineSnapshot(`
36 | "
37 | ● Test error
38 | "
39 | `);
40 | });
41 |
42 | it("logError with detail", () => {
43 | logError("Test error", "Error details");
44 | expect(capturedOutput).toMatchInlineSnapshot(`
45 | "
46 | ● Test error
47 | ⎿ Error details
48 | "
49 | `);
50 | });
51 |
52 | it("logError with Error object", () => {
53 | const error = new Error("Something went wrong");
54 | logError("Test error", error);
55 | expect(capturedOutput).toMatchInlineSnapshot(`
56 | "
57 | ● Test error
58 | ⎿ Something went wrong
59 | "
60 | `);
61 | });
62 |
63 | it("logSuccess without detail", () => {
64 | logSuccess("Test success");
65 | expect(capturedOutput).toMatchInlineSnapshot(`
66 | "
67 | ● Test success
68 | "
69 | `);
70 | });
71 |
72 | it("logSuccess with detail", () => {
73 | logSuccess("Test success", "Success details");
74 | expect(capturedOutput).toMatchInlineSnapshot(`
75 | "
76 | ● Test success
77 | ⎿ Success details
78 | "
79 | `);
80 | });
81 |
82 | it("logInfo without detail", () => {
83 | logInfo("Test info");
84 | expect(capturedOutput).toMatchInlineSnapshot(`
85 | "
86 | ● Test info
87 | "
88 | `);
89 | });
90 |
91 | it("logInfo with detail", () => {
92 | logInfo("Test info", "Info details");
93 | expect(capturedOutput).toMatchInlineSnapshot(`
94 | "
95 | ● Test info
96 | ⎿ Info details
97 | "
98 | `);
99 | });
100 |
101 | it("logUser", () => {
102 | logUser("User message");
103 | expect(capturedOutput).toMatchInlineSnapshot(`
104 | "
105 | > User message
106 | "
107 | `);
108 | });
109 |
110 | it("logTool without params", () => {
111 | logTool("test_tool");
112 | expect(capturedOutput).toMatchInlineSnapshot(`
113 | "
114 | ● test_tool()
115 | "
116 | `);
117 | });
118 |
119 | it("logTool with params", () => {
120 | logTool("test_tool", { param1: "value1", param2: 42 });
121 | expect(capturedOutput).toMatchInlineSnapshot(`
122 | "
123 | ● test_tool(param1: value1, param2: 42)
124 | "
125 | `);
126 | });
127 |
128 | it("logTool with complex params", () => {
129 | logTool("test_tool", { obj: { nested: true }, arr: [1, 2, 3] });
130 | expect(capturedOutput).toMatchInlineSnapshot(`
131 | "
132 | ● test_tool(obj: {"nested":true}, arr: [1,2,3])
133 | "
134 | `);
135 | });
136 |
137 | it("logToolResult", () => {
138 | logToolResult("Tool result");
139 | expect(capturedOutput).toMatchInlineSnapshot(`
140 | " ⎿ Tool result
141 | "
142 | `);
143 | });
144 | });
145 |
146 | describe("streaming logs", () => {
147 | it("complete stream flow", () => {
148 | logStreamStart();
149 | logStreamWrite("Hello world");
150 | logStreamWrite(" with continuation");
151 | logStreamWrite("\nAnd a new line");
152 | logStreamEnd();
153 |
154 | expect(capturedOutput).toMatchInlineSnapshot(`
155 | "
156 | ● Hello world with continuation
157 | And a new line
158 | "
159 | `);
160 | });
161 |
162 | it("should not start stream twice", () => {
163 | logStreamStart();
164 | logStreamStart();
165 | logStreamWrite("content");
166 | logStreamEnd();
167 |
168 | expect(capturedOutput).toMatchInlineSnapshot(`
169 | "
170 | ● content
171 | "
172 | `);
173 | });
174 | });
175 |
176 | describe("multiple consecutive calls", () => {
177 | it("mixed log types without extra newlines", () => {
178 | logInfo("First message");
179 | logSuccess("Second message", "with detail");
180 | logError("Third message");
181 | logUser("Fourth message");
182 |
183 | expect(capturedOutput).toMatchInlineSnapshot(`
184 | "
185 | ● First message
186 |
187 | ● Second message
188 | ⎿ with detail
189 |
190 | ● Third message
191 |
192 | > Fourth message
193 | "
194 | `);
195 | });
196 |
197 | it("tool call sequence", () => {
198 | logTool("find_issues", { query: "is:unresolved" });
199 | logToolResult("Found 3 issues");
200 | logTool("get_issue_details", { issueId: "PROJ-123" });
201 | logToolResult("Issue details retrieved");
202 |
203 | expect(capturedOutput).toMatchInlineSnapshot(`
204 | "
205 | ● find_issues(query: is:unresolved)
206 | ⎿ Found 3 issues
207 |
208 | ● get_issue_details(issueId: PROJ-123)
209 | ⎿ Issue details retrieved
210 | "
211 | `);
212 | });
213 | });
214 |
215 | describe("real-world scenarios", () => {
216 | it("authentication flow", () => {
217 | logInfo("Authenticated with Sentry", "using stored token");
218 | expect(capturedOutput).toMatchInlineSnapshot(`
219 | "
220 | ● Authenticated with Sentry
221 | ⎿ using stored token
222 | "
223 | `);
224 | });
225 |
226 | it("interactive mode start", () => {
227 | logInfo("Interactive mode", "type 'exit', 'quit', or Ctrl+D to end");
228 | expect(capturedOutput).toMatchInlineSnapshot(`
229 | "
230 | ● Interactive mode
231 | ⎿ type 'exit', 'quit', or Ctrl+D to end
232 | "
233 | `);
234 | });
235 |
236 | it("MCP connection", () => {
237 | logSuccess("Connected to MCP server (stdio)", "5 tools available");
238 | expect(capturedOutput).toMatchInlineSnapshot(`
239 | "
240 | ● Connected to MCP server (stdio)
241 | ⎿ 5 tools available
242 | "
243 | `);
244 | });
245 |
246 | it("fatal error with event ID", () => {
247 | logError("Fatal error", "Network connection failed. Event ID: abc123");
248 | expect(capturedOutput).toMatchInlineSnapshot(`
249 | "
250 | ● Fatal error
251 | ⎿ Network connection failed. Event ID: abc123
252 | "
253 | `);
254 | });
255 |
256 | it("complete session simulation", () => {
257 | // Authentication
258 | logInfo("Authenticated with Sentry", "using stored token");
259 | // Connection
260 | logSuccess("Connected to MCP server (stdio)", "5 tools available");
261 | // Interactive mode
262 | logInfo("Interactive mode", "type 'exit', 'quit', or Ctrl+D to end");
263 | // User query
264 | logUser(
265 | "List all my projects and show me details about the most recent issue",
266 | );
267 |
268 | // AI starts responding (streaming)
269 | logStreamStart();
270 | logStreamWrite(
271 | "I'll help you find your projects and get details about the most recent issue. Let me start by fetching your projects.",
272 | );
273 | logStreamEnd();
274 |
275 | // Tool execution mid-response
276 | logTool("find_projects", { organizationSlug: "my-org" });
277 | logToolResult("Found 3 projects: frontend-app, backend-api, mobile-app");
278 |
279 | // AI continues response
280 | logStreamStart();
281 | logStreamWrite(
282 | "Great! I found 3 projects. Now let me find the most recent issue across all projects.",
283 | );
284 | logStreamEnd();
285 |
286 | // Another tool call mid-response
287 | logTool("find_issues", {
288 | query: "is:unresolved",
289 | sortBy: "last_seen",
290 | limit: 1,
291 | });
292 | logToolResult("Found issue FRONTEND-456: TypeError in checkout flow");
293 |
294 | // AI final response
295 | logStreamStart();
296 | logStreamWrite("Here's what I found:\n\n**Your Projects:**");
297 | logStreamWrite("\n1. frontend-app");
298 | logStreamWrite("\n2. backend-api");
299 | logStreamWrite("\n3. mobile-app");
300 | logStreamWrite("\n\n**Most Recent Issue:**");
301 | logStreamWrite("\n- FRONTEND-456: TypeError in checkout flow");
302 | logStreamWrite(
303 | "\n- This is an unresolved issue in your frontend-app project",
304 | );
305 | logStreamEnd();
306 |
307 | // Exit
308 | logInfo("Goodbye!");
309 |
310 | expect(capturedOutput).toMatchInlineSnapshot(`
311 | "
312 | ● Authenticated with Sentry
313 | ⎿ using stored token
314 |
315 | ● Connected to MCP server (stdio)
316 | ⎿ 5 tools available
317 |
318 | ● Interactive mode
319 | ⎿ type 'exit', 'quit', or Ctrl+D to end
320 |
321 | > List all my projects and show me details about the most recent issue
322 |
323 | ● I'll help you find your projects and get details about the most recent issue. Let me start by fetching your projects.
324 |
325 | ● find_projects(organizationSlug: my-org)
326 | ⎿ Found 3 projects: frontend-app, backend-api, mobile-app
327 |
328 | ● Great! I found 3 projects. Now let me find the most recent issue across all projects.
329 |
330 | ● find_issues(query: is:unresolved, sortBy: last_seen, limit: 1)
331 | ⎿ Found issue FRONTEND-456: TypeError in checkout flow
332 |
333 | ● Here's what I found:
334 |
335 | **Your Projects:**
336 | 1. frontend-app
337 | 2. backend-api
338 | 3. mobile-app
339 |
340 | **Most Recent Issue:**
341 | - FRONTEND-456: TypeError in checkout flow
342 | - This is an unresolved issue in your frontend-app project
343 |
344 | ● Goodbye!
345 | "
346 | `);
347 | });
348 | });
349 | });
350 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/tls.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "namespace": "tls",
3 | "description": "This document defines semantic convention attributes in the TLS namespace.",
4 | "attributes": {
5 | "tls.cipher": {
6 | "description": "String indicating the [cipher](https://datatracker.ietf.org/doc/html/rfc5246#appendix-A.5) used during the current connection.\n",
7 | "type": "string",
8 | "note": "The values allowed for `tls.cipher` MUST be one of the `Descriptions` of the [registered TLS Cipher Suits](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#table-tls-parameters-4).\n",
9 | "stability": "development",
10 | "examples": [
11 | "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
12 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256"
13 | ]
14 | },
15 | "tls.client.certificate": {
16 | "description": "PEM-encoded stand-alone certificate offered by the client. This is usually mutually-exclusive of `client.certificate_chain` since this value also exists in that list.\n",
17 | "type": "string",
18 | "stability": "development",
19 | "examples": ["MII..."]
20 | },
21 | "tls.client.certificate_chain": {
22 | "description": "Array of PEM-encoded certificates that make up the certificate chain offered by the client. This is usually mutually-exclusive of `client.certificate` since that value should be the first certificate in the chain.\n",
23 | "type": "string",
24 | "stability": "development",
25 | "examples": ["[\"MII...\",\"MI...\"]"]
26 | },
27 | "tls.client.hash.md5": {
28 | "description": "Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash.\n",
29 | "type": "string",
30 | "stability": "development",
31 | "examples": ["0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC"]
32 | },
33 | "tls.client.hash.sha1": {
34 | "description": "Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash.\n",
35 | "type": "string",
36 | "stability": "development",
37 | "examples": ["9E393D93138888D288266C2D915214D1D1CCEB2A"]
38 | },
39 | "tls.client.hash.sha256": {
40 | "description": "Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash.\n",
41 | "type": "string",
42 | "stability": "development",
43 | "examples": [
44 | "0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0"
45 | ]
46 | },
47 | "tls.client.issuer": {
48 | "description": "Distinguished name of [subject](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6) of the issuer of the x.509 certificate presented by the client.",
49 | "type": "string",
50 | "stability": "development",
51 | "examples": [
52 | "CN=Example Root CA, OU=Infrastructure Team, DC=example, DC=com"
53 | ]
54 | },
55 | "tls.client.ja3": {
56 | "description": "A hash that identifies clients based on how they perform an SSL/TLS handshake.",
57 | "type": "string",
58 | "stability": "development",
59 | "examples": ["d4e5b18d6b55c71272893221c96ba240"]
60 | },
61 | "tls.client.not_after": {
62 | "description": "Date/Time indicating when client certificate is no longer considered valid.",
63 | "type": "string",
64 | "stability": "development",
65 | "examples": ["2021-01-01T00:00:00.000Z"]
66 | },
67 | "tls.client.not_before": {
68 | "description": "Date/Time indicating when client certificate is first considered valid.",
69 | "type": "string",
70 | "stability": "development",
71 | "examples": ["1970-01-01T00:00:00.000Z"]
72 | },
73 | "tls.client.subject": {
74 | "description": "Distinguished name of subject of the x.509 certificate presented by the client.",
75 | "type": "string",
76 | "stability": "development",
77 | "examples": ["CN=myclient, OU=Documentation Team, DC=example, DC=com"]
78 | },
79 | "tls.client.supported_ciphers": {
80 | "description": "Array of ciphers offered by the client during the client hello.",
81 | "type": "string",
82 | "stability": "development",
83 | "examples": [
84 | "[\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\",\"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\"]"
85 | ]
86 | },
87 | "tls.curve": {
88 | "description": "String indicating the curve used for the given cipher, when applicable",
89 | "type": "string",
90 | "stability": "development",
91 | "examples": ["secp256r1"]
92 | },
93 | "tls.established": {
94 | "description": "Boolean flag indicating if the TLS negotiation was successful and transitioned to an encrypted tunnel.",
95 | "type": "boolean",
96 | "stability": "development",
97 | "examples": ["true"]
98 | },
99 | "tls.next_protocol": {
100 | "description": "String indicating the protocol being tunneled. Per the values in the [IANA registry](https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids), this string should be lower case.\n",
101 | "type": "string",
102 | "stability": "development",
103 | "examples": ["http/1.1"]
104 | },
105 | "tls.protocol.name": {
106 | "description": "Normalized lowercase protocol name parsed from original string of the negotiated [SSL/TLS protocol version](https://docs.openssl.org/1.1.1/man3/SSL_get_version/#return-values)\n",
107 | "type": "string",
108 | "stability": "development",
109 | "examples": ["ssl", "tls"]
110 | },
111 | "tls.protocol.version": {
112 | "description": "Numeric part of the version parsed from the original string of the negotiated [SSL/TLS protocol version](https://docs.openssl.org/1.1.1/man3/SSL_get_version/#return-values)\n",
113 | "type": "string",
114 | "stability": "development",
115 | "examples": ["1.2", "3"]
116 | },
117 | "tls.resumed": {
118 | "description": "Boolean flag indicating if this TLS connection was resumed from an existing TLS negotiation.",
119 | "type": "boolean",
120 | "stability": "development",
121 | "examples": ["true"]
122 | },
123 | "tls.server.certificate": {
124 | "description": "PEM-encoded stand-alone certificate offered by the server. This is usually mutually-exclusive of `server.certificate_chain` since this value also exists in that list.\n",
125 | "type": "string",
126 | "stability": "development",
127 | "examples": ["MII..."]
128 | },
129 | "tls.server.certificate_chain": {
130 | "description": "Array of PEM-encoded certificates that make up the certificate chain offered by the server. This is usually mutually-exclusive of `server.certificate` since that value should be the first certificate in the chain.\n",
131 | "type": "string",
132 | "stability": "development",
133 | "examples": ["[\"MII...\",\"MI...\"]"]
134 | },
135 | "tls.server.hash.md5": {
136 | "description": "Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash.\n",
137 | "type": "string",
138 | "stability": "development",
139 | "examples": ["0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC"]
140 | },
141 | "tls.server.hash.sha1": {
142 | "description": "Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash.\n",
143 | "type": "string",
144 | "stability": "development",
145 | "examples": ["9E393D93138888D288266C2D915214D1D1CCEB2A"]
146 | },
147 | "tls.server.hash.sha256": {
148 | "description": "Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash.\n",
149 | "type": "string",
150 | "stability": "development",
151 | "examples": [
152 | "0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0"
153 | ]
154 | },
155 | "tls.server.issuer": {
156 | "description": "Distinguished name of [subject](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6) of the issuer of the x.509 certificate presented by the client.",
157 | "type": "string",
158 | "stability": "development",
159 | "examples": [
160 | "CN=Example Root CA, OU=Infrastructure Team, DC=example, DC=com"
161 | ]
162 | },
163 | "tls.server.ja3s": {
164 | "description": "A hash that identifies servers based on how they perform an SSL/TLS handshake.",
165 | "type": "string",
166 | "stability": "development",
167 | "examples": ["d4e5b18d6b55c71272893221c96ba240"]
168 | },
169 | "tls.server.not_after": {
170 | "description": "Date/Time indicating when server certificate is no longer considered valid.",
171 | "type": "string",
172 | "stability": "development",
173 | "examples": ["2021-01-01T00:00:00.000Z"]
174 | },
175 | "tls.server.not_before": {
176 | "description": "Date/Time indicating when server certificate is first considered valid.",
177 | "type": "string",
178 | "stability": "development",
179 | "examples": ["1970-01-01T00:00:00.000Z"]
180 | },
181 | "tls.server.subject": {
182 | "description": "Distinguished name of subject of the x.509 certificate presented by the server.",
183 | "type": "string",
184 | "stability": "development",
185 | "examples": ["CN=myserver, OU=Documentation Team, DC=example, DC=com"]
186 | }
187 | }
188 | }
189 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/dataset-fields.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach } from "vitest";
2 | import { http, HttpResponse } from "msw";
3 | import { mswServer } from "@sentry/mcp-server-mocks";
4 | import {
5 | discoverDatasetFields,
6 | getFieldExamples,
7 | getCommonPatterns,
8 | } from "./dataset-fields";
9 | import { SentryApiService } from "../../../api-client";
10 |
11 | // Test the core logic functions directly without AI SDK complexity
12 |
13 | describe("dataset-fields agent tool", () => {
14 | let apiService: SentryApiService;
15 |
16 | beforeEach(() => {
17 | vi.clearAllMocks();
18 | apiService = new SentryApiService({
19 | accessToken: "test-token",
20 | });
21 | });
22 |
23 | describe("discoverDatasetFields", () => {
24 | it("should discover fields for search_issues dataset", async () => {
25 | // Mock the tags API response for issues
26 | mswServer.use(
27 | http.get(
28 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/tags/",
29 | ({ request }) => {
30 | const url = new URL(request.url);
31 | expect(url.searchParams.get("dataset")).toBe("search_issues");
32 | expect(url.searchParams.get("project")).toBe("4509062593708032");
33 | expect(url.searchParams.get("statsPeriod")).toBe("14d");
34 |
35 | return HttpResponse.json([
36 | {
37 | key: "level",
38 | name: "Level",
39 | totalValues: 5,
40 | topValues: [
41 | { key: "error", name: "error", value: "error", count: 42 },
42 | ],
43 | },
44 | {
45 | key: "is",
46 | name: "Status",
47 | totalValues: 3,
48 | topValues: [
49 | {
50 | key: "unresolved",
51 | name: "unresolved",
52 | value: "unresolved",
53 | count: 15,
54 | },
55 | ],
56 | },
57 | {
58 | key: "sentry:user", // Should be filtered out
59 | name: "User (Internal)",
60 | totalValues: 10,
61 | },
62 | ]);
63 | },
64 | ),
65 | );
66 |
67 | const result = await discoverDatasetFields(
68 | apiService,
69 | "sentry-mcp-evals",
70 | "search_issues",
71 | { projectId: "4509062593708032" },
72 | );
73 |
74 | expect(result.dataset).toBe("search_issues");
75 | expect(result.fields).toHaveLength(2); // sentry:user should be filtered out
76 | expect(result.fields[0].key).toBe("level");
77 | expect(result.fields[0].name).toBe("Level");
78 | expect(result.fields[0].totalValues).toBe(5);
79 | expect(result.fields[1].key).toBe("is");
80 | expect(result.commonPatterns).toContainEqual({
81 | pattern: "is:unresolved",
82 | description: "Open issues",
83 | });
84 | });
85 |
86 | it("should discover fields for events dataset (examples always included)", async () => {
87 | // Mock the tags API response for events
88 | mswServer.use(
89 | http.get(
90 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/tags/",
91 | ({ request }) => {
92 | const url = new URL(request.url);
93 | expect(url.searchParams.get("dataset")).toBe("events");
94 |
95 | return HttpResponse.json([
96 | {
97 | key: "http.method",
98 | name: "HTTP Method",
99 | totalValues: 4,
100 | },
101 | {
102 | key: "environment",
103 | name: "Environment",
104 | totalValues: 3,
105 | },
106 | ]);
107 | },
108 | ),
109 | );
110 |
111 | const result = await discoverDatasetFields(
112 | apiService,
113 | "sentry-mcp-evals",
114 | "events",
115 | { projectId: "4509062593708032" },
116 | );
117 |
118 | expect(result.dataset).toBe("events");
119 | expect(result.fields).toHaveLength(2);
120 | expect(result.fields[0].key).toBe("http.method");
121 | expect(result.fields[0].examples).toEqual([
122 | "GET",
123 | "POST",
124 | "PUT",
125 | "DELETE",
126 | ]);
127 | expect(result.fields[1].key).toBe("environment");
128 | expect(result.fields[1].examples).toEqual([
129 | "production",
130 | "staging",
131 | "development",
132 | ]);
133 | expect(result.commonPatterns).toContainEqual({
134 | pattern: "level:error",
135 | description: "Error events",
136 | });
137 | });
138 |
139 | it("should handle API errors gracefully", async () => {
140 | // Mock API error
141 | mswServer.use(
142 | http.get(
143 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/tags/",
144 | () =>
145 | HttpResponse.json(
146 | { detail: "Organization not found" },
147 | { status: 404 },
148 | ),
149 | ),
150 | );
151 |
152 | await expect(
153 | discoverDatasetFields(apiService, "sentry-mcp-evals", "errors"),
154 | ).rejects.toThrow();
155 | });
156 |
157 | it("should provide appropriate examples for each dataset type", async () => {
158 | mswServer.use(
159 | http.get(
160 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/tags/",
161 | () =>
162 | HttpResponse.json([
163 | { key: "assignedOrSuggested", name: "Assigned", totalValues: 5 },
164 | { key: "is", name: "Status", totalValues: 3 },
165 | ]),
166 | ),
167 | );
168 |
169 | const issuesResult = await discoverDatasetFields(
170 | apiService,
171 | "sentry-mcp-evals",
172 | "search_issues",
173 | );
174 |
175 | expect(issuesResult.fields[0].examples).toEqual([
176 | "[email protected]",
177 | "team-slug",
178 | "me",
179 | ]);
180 | expect(issuesResult.fields[1].examples).toEqual([
181 | "unresolved",
182 | "resolved",
183 | "ignored",
184 | ]);
185 |
186 | // Test events examples
187 |
188 | mswServer.use(
189 | http.get(
190 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/tags/",
191 | () =>
192 | HttpResponse.json([
193 | { key: "http.method", name: "HTTP Method", totalValues: 4 },
194 | { key: "db.system", name: "Database System", totalValues: 3 },
195 | ]),
196 | ),
197 | );
198 |
199 | const eventsResult = await discoverDatasetFields(
200 | apiService,
201 | "sentry-mcp-evals",
202 | "events",
203 | );
204 |
205 | expect(eventsResult.fields[0].examples).toEqual([
206 | "GET",
207 | "POST",
208 | "PUT",
209 | "DELETE",
210 | ]);
211 | expect(eventsResult.fields[1].examples).toEqual([
212 | "postgresql",
213 | "mysql",
214 | "redis",
215 | ]);
216 | });
217 |
218 | it("should provide correct common patterns for different datasets", async () => {
219 | // Mock minimal API response
220 | mswServer.use(
221 | http.get(
222 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/tags/",
223 | () => HttpResponse.json([]),
224 | ),
225 | );
226 |
227 | // Test patterns are returned correctly for each dataset type
228 | const issuesResult = await discoverDatasetFields(
229 | apiService,
230 | "sentry-mcp-evals",
231 | "search_issues",
232 | );
233 | expect(issuesResult.commonPatterns).toEqual(
234 | expect.arrayContaining([
235 | { pattern: "is:unresolved", description: "Open issues" },
236 | {
237 | pattern: "firstSeen:-24h",
238 | description: "New issues from last 24 hours",
239 | },
240 | ]),
241 | );
242 |
243 | const eventsResult = await discoverDatasetFields(
244 | apiService,
245 | "sentry-mcp-evals",
246 | "events",
247 | );
248 | expect(eventsResult.commonPatterns).toEqual(
249 | expect.arrayContaining([
250 | { pattern: "level:error", description: "Error events" },
251 | { pattern: "has:http.method", description: "HTTP requests" },
252 | ]),
253 | );
254 | });
255 | });
256 |
257 | describe("getFieldExamples", () => {
258 | it("should return examples for search_issues fields", () => {
259 | expect(getFieldExamples("assignedOrSuggested", "search_issues")).toEqual([
260 | "[email protected]",
261 | "team-slug",
262 | "me",
263 | ]);
264 | expect(getFieldExamples("is", "search_issues")).toEqual([
265 | "unresolved",
266 | "resolved",
267 | "ignored",
268 | ]);
269 | });
270 |
271 | it("should return examples for events fields", () => {
272 | expect(getFieldExamples("http.method", "events")).toEqual([
273 | "GET",
274 | "POST",
275 | "PUT",
276 | "DELETE",
277 | ]);
278 | expect(getFieldExamples("db.system", "events")).toEqual([
279 | "postgresql",
280 | "mysql",
281 | "redis",
282 | ]);
283 | });
284 |
285 | it("should return common examples for unknown fields", () => {
286 | expect(getFieldExamples("level", "search_issues")).toEqual([
287 | "error",
288 | "warning",
289 | "info",
290 | "debug",
291 | "fatal",
292 | ]);
293 | expect(getFieldExamples("unknown", "search_issues")).toBeUndefined();
294 | });
295 | });
296 |
297 | describe("getCommonPatterns", () => {
298 | it("should return patterns for search_issues", () => {
299 | const patterns = getCommonPatterns("search_issues");
300 | expect(patterns).toContainEqual({
301 | pattern: "is:unresolved",
302 | description: "Open issues",
303 | });
304 | expect(patterns).toContainEqual({
305 | pattern: "firstSeen:-24h",
306 | description: "New issues from last 24 hours",
307 | });
308 | });
309 |
310 | it("should return patterns for events", () => {
311 | const patterns = getCommonPatterns("events");
312 | expect(patterns).toContainEqual({
313 | pattern: "level:error",
314 | description: "Error events",
315 | });
316 | expect(patterns).toContainEqual({
317 | pattern: "has:http.method",
318 | description: "HTTP requests",
319 | });
320 | });
321 |
322 | it("should return empty array for unknown datasets", () => {
323 | const patterns = getCommonPatterns("unknown");
324 | expect(patterns).toEqual([]);
325 | });
326 | });
327 | });
328 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/search-events/handler.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { setTag } from "@sentry/core";
3 | import { defineTool } from "../../internal/tool-helpers/define";
4 | import { apiServiceFromContext } from "../../internal/tool-helpers/api";
5 | import type { ServerContext } from "../../types";
6 | import {
7 | ParamOrganizationSlug,
8 | ParamRegionUrl,
9 | ParamProjectSlug,
10 | } from "../../schema";
11 | import { searchEventsAgent } from "./agent";
12 | import {
13 | formatErrorResults,
14 | formatLogResults,
15 | formatSpanResults,
16 | } from "./formatters";
17 | import { RECOMMENDED_FIELDS } from "./config";
18 | import { UserInputError } from "../../errors";
19 |
20 | export default defineTool({
21 | name: "search_events",
22 | requiredScopes: ["event:read"],
23 | description: [
24 | "Search for events AND perform counts/aggregations - the ONLY tool for statistics and counts.",
25 | "",
26 | "Supports TWO query types:",
27 | "1. AGGREGATIONS (counts, sums, averages): 'how many errors', 'count of issues', 'total tokens'",
28 | "2. Individual events with timestamps: 'show me error logs from last hour'",
29 | "",
30 | "🔢 USE THIS FOR ALL COUNTS/STATISTICS:",
31 | "- 'how many errors today' → returns count",
32 | "- 'count of database failures' → returns count",
33 | "- 'total number of issues' → returns count",
34 | "- 'average response time' → returns avg()",
35 | "- 'sum of tokens used' → returns sum()",
36 | "",
37 | "📋 ALSO USE FOR INDIVIDUAL EVENTS:",
38 | "- 'error logs from last hour' → returns event list",
39 | "- 'database errors with timestamps' → returns event list",
40 | "- 'trace spans for slow API calls' → returns span list",
41 | "",
42 | "Dataset Selection (AI automatically chooses):",
43 | "- errors: Exception/crash events",
44 | "- logs: Log entries",
45 | "- spans: Performance data, AI/LLM calls, token usage",
46 | "",
47 | "❌ DO NOT USE for grouped issue lists → use search_issues",
48 | "",
49 | "<examples>",
50 | "search_events(organizationSlug='my-org', naturalLanguageQuery='how many errors today')",
51 | "search_events(organizationSlug='my-org', naturalLanguageQuery='count of database failures this week')",
52 | "search_events(organizationSlug='my-org', naturalLanguageQuery='total tokens used by model')",
53 | "search_events(organizationSlug='my-org', naturalLanguageQuery='error logs from the last hour')",
54 | "</examples>",
55 | "",
56 | "<hints>",
57 | "- If the user passes a parameter in the form of name/otherName, it's likely in the format of <organizationSlug>/<projectSlug>.",
58 | "- Parse org/project notation directly without calling find_organizations or find_projects.",
59 | "</hints>",
60 | ].join("\n"),
61 | inputSchema: {
62 | organizationSlug: ParamOrganizationSlug,
63 | naturalLanguageQuery: z
64 | .string()
65 | .trim()
66 | .min(1)
67 | .describe("Natural language description of what you want to search for"),
68 | projectSlug: ParamProjectSlug.optional(),
69 | regionUrl: ParamRegionUrl.optional(),
70 | limit: z
71 | .number()
72 | .min(1)
73 | .max(100)
74 | .optional()
75 | .default(10)
76 | .describe("Maximum number of results to return"),
77 | includeExplanation: z
78 | .boolean()
79 | .optional()
80 | .default(false)
81 | .describe("Include explanation of how the query was translated"),
82 | },
83 | annotations: {
84 | readOnlyHint: true,
85 | openWorldHint: true,
86 | },
87 | async handler(params, context: ServerContext) {
88 | const apiService = apiServiceFromContext(context, {
89 | regionUrl: params.regionUrl,
90 | });
91 | const organizationSlug = params.organizationSlug;
92 |
93 | setTag("organization.slug", organizationSlug);
94 | if (params.projectSlug) setTag("project.slug", params.projectSlug);
95 |
96 | // The agent will determine the dataset based on the query content
97 |
98 | // Convert project slug to ID if needed - we need this for attribute fetching
99 | let projectId: string | undefined;
100 | if (params.projectSlug) {
101 | const project = await apiService.getProject({
102 | organizationSlug,
103 | projectSlugOrId: params.projectSlug!,
104 | });
105 | projectId = String(project.id);
106 | }
107 |
108 | // Translate the natural language query using Search Events Agent
109 | // The agent will determine the dataset and fetch the appropriate attributes
110 | const agentResult = await searchEventsAgent({
111 | query: params.naturalLanguageQuery,
112 | organizationSlug,
113 | apiService,
114 | projectId,
115 | });
116 |
117 | const parsed = agentResult.result;
118 |
119 | // Get the dataset chosen by the agent (should be defined when no error)
120 | const dataset = parsed.dataset!;
121 |
122 | // Get recommended fields for this dataset (for fallback when no fields are provided)
123 | const recommendedFields = RECOMMENDED_FIELDS[dataset];
124 |
125 | // Validate that sort parameter was provided
126 | if (!parsed.sort) {
127 | throw new UserInputError(
128 | `Search Events Agent response missing required 'sort' parameter. Received: ${JSON.stringify(parsed, null, 2)}. The agent must specify how to sort results (e.g., '-timestamp' for newest first, '-count()' for highest count).`,
129 | );
130 | }
131 |
132 | // Use empty string as default if no query is provided
133 | // This allows fetching all recent events when no specific filter is needed
134 | const sentryQuery = parsed.query || "";
135 | const requestedFields = parsed.fields || [];
136 |
137 | // Determine if this is an aggregate query by checking if any field contains a function
138 | const isAggregateQuery = requestedFields.some(
139 | (field) => field.includes("(") && field.includes(")"),
140 | );
141 |
142 | // For aggregate queries, we should only use the fields provided by the AI
143 | // For non-aggregate queries, we can use recommended fields as fallback
144 | let fields: string[];
145 |
146 | if (isAggregateQuery) {
147 | // For aggregate queries, fields must be provided and should only include
148 | // aggregate functions and groupBy fields
149 | if (!requestedFields || requestedFields.length === 0) {
150 | throw new UserInputError(
151 | `AI response missing required 'fields' for aggregate query. The AI must specify which fields to return. For aggregate queries, include only the aggregate functions (like count(), avg()) and groupBy fields.`,
152 | );
153 | }
154 | fields = requestedFields;
155 | } else {
156 | // For non-aggregate queries, use AI-provided fields or fall back to recommended fields
157 | fields =
158 | requestedFields && requestedFields.length > 0
159 | ? requestedFields
160 | : recommendedFields.basic;
161 | }
162 |
163 | // Use the AI-provided sort parameter
164 | const sortParam = parsed.sort;
165 |
166 | // Extract time range parameters from parsed response
167 | const timeParams: { statsPeriod?: string; start?: string; end?: string } =
168 | {};
169 | if (parsed.timeRange) {
170 | if ("statsPeriod" in parsed.timeRange) {
171 | timeParams.statsPeriod = parsed.timeRange.statsPeriod;
172 | } else if ("start" in parsed.timeRange && "end" in parsed.timeRange) {
173 | timeParams.start = parsed.timeRange.start;
174 | timeParams.end = parsed.timeRange.end;
175 | }
176 | } else {
177 | // Default time window if not specified
178 | timeParams.statsPeriod = "14d";
179 | }
180 |
181 | const eventsResponse = await apiService.searchEvents({
182 | organizationSlug,
183 | query: sentryQuery,
184 | fields,
185 | limit: params.limit,
186 | projectId, // API requires numeric project ID, not slug
187 | dataset: dataset === "logs" ? "ourlogs" : dataset,
188 | sort: sortParam,
189 | ...timeParams, // Spread the time parameters
190 | });
191 |
192 | // Generate the Sentry explorer URL with structured aggregate information
193 | // Derive aggregate functions and groupBy fields from the fields array
194 | const aggregateFunctions = fields.filter(
195 | (field) => field.includes("(") && field.includes(")"),
196 | );
197 | const groupByFields = fields.filter(
198 | (field) => !field.includes("(") && !field.includes(")"),
199 | );
200 |
201 | const explorerUrl = apiService.getEventsExplorerUrl(
202 | organizationSlug,
203 | sentryQuery,
204 | projectId, // Pass the numeric project ID for URL generation
205 | dataset, // dataset is already correct for URL generation (logs, spans, errors)
206 | fields, // Pass fields to detect if it's an aggregate query
207 | sortParam, // Pass sort parameter for URL generation
208 | aggregateFunctions,
209 | groupByFields,
210 | timeParams.statsPeriod,
211 | timeParams.start,
212 | timeParams.end,
213 | );
214 |
215 | // Type-safe access to event data with proper validation
216 | function isValidResponse(
217 | response: unknown,
218 | ): response is { data?: unknown[] } {
219 | return typeof response === "object" && response !== null;
220 | }
221 |
222 | function isValidEventArray(
223 | data: unknown,
224 | ): data is Record<string, unknown>[] {
225 | return (
226 | Array.isArray(data) &&
227 | data.every((item) => typeof item === "object" && item !== null)
228 | );
229 | }
230 |
231 | if (!isValidResponse(eventsResponse)) {
232 | throw new Error("Invalid response format from Sentry API");
233 | }
234 |
235 | const eventData = eventsResponse.data;
236 | if (!isValidEventArray(eventData)) {
237 | throw new Error("Invalid event data format from Sentry API");
238 | }
239 |
240 | // Format results based on dataset
241 | const formatParams = {
242 | eventData,
243 | naturalLanguageQuery: params.naturalLanguageQuery,
244 | includeExplanation: params.includeExplanation,
245 | apiService,
246 | organizationSlug,
247 | explorerUrl,
248 | sentryQuery,
249 | fields,
250 | explanation: parsed.explanation,
251 | };
252 |
253 | switch (dataset) {
254 | case "errors":
255 | return formatErrorResults(formatParams);
256 | case "logs":
257 | return formatLogResults(formatParams);
258 | case "spans":
259 | return formatSpanResults(formatParams);
260 | }
261 | },
262 | });
263 |
```
--------------------------------------------------------------------------------
/packages/mcp-server-mocks/src/fixtures/event.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "id": "7ca573c0f4814912aaa9bdc77d1a7d51",
3 | "groupID": "6507376925",
4 | "eventID": "7ca573c0f4814912aaa9bdc77d1a7d51",
5 | "projectID": "4509062593708032",
6 | "size": 5891,
7 | "entries": [
8 | {
9 | "data": {
10 | "values": [
11 | {
12 | "type": "Error",
13 | "value": "Tool list_organizations is already registered",
14 | "mechanism": {
15 | "type": "cloudflare",
16 | "handled": false
17 | },
18 | "threadId": null,
19 | "module": null,
20 | "stacktrace": {
21 | "frames": [
22 | {
23 | "filename": "index.js",
24 | "absPath": "/index.js",
25 | "module": "index",
26 | "package": null,
27 | "platform": null,
28 | "instructionAddr": null,
29 | "symbolAddr": null,
30 | "function": null,
31 | "rawFunction": null,
32 | "symbol": null,
33 | "context": [],
34 | "lineNo": 7809,
35 | "colNo": 27,
36 | "inApp": true,
37 | "trust": null,
38 | "errors": null,
39 | "lock": null,
40 | "sourceLink": null,
41 | "vars": null
42 | },
43 | {
44 | "filename": "index.js",
45 | "absPath": "/index.js",
46 | "module": "index",
47 | "package": null,
48 | "platform": null,
49 | "instructionAddr": null,
50 | "symbolAddr": null,
51 | "function": "OAuthProviderImpl.fetch",
52 | "rawFunction": null,
53 | "symbol": null,
54 | "context": [],
55 | "lineNo": 8029,
56 | "colNo": 24,
57 | "inApp": true,
58 | "trust": null,
59 | "errors": null,
60 | "lock": null,
61 | "sourceLink": null,
62 | "vars": null
63 | },
64 | {
65 | "filename": "index.js",
66 | "absPath": "/index.js",
67 | "module": "index",
68 | "package": null,
69 | "platform": null,
70 | "instructionAddr": null,
71 | "symbolAddr": null,
72 | "function": "Object.fetch",
73 | "rawFunction": null,
74 | "symbol": null,
75 | "context": [],
76 | "lineNo": 19631,
77 | "colNo": 28,
78 | "inApp": true,
79 | "trust": null,
80 | "errors": null,
81 | "lock": null,
82 | "sourceLink": null,
83 | "vars": null
84 | }
85 | ],
86 | "framesOmitted": null,
87 | "registers": null,
88 | "hasSystemFrames": true
89 | },
90 | "rawStacktrace": {
91 | "frames": [
92 | {
93 | "filename": "index.js",
94 | "absPath": "/index.js",
95 | "module": "index",
96 | "package": null,
97 | "platform": null,
98 | "instructionAddr": null,
99 | "symbolAddr": null,
100 | "function": null,
101 | "rawFunction": null,
102 | "symbol": null,
103 | "context": [],
104 | "lineNo": 7809,
105 | "colNo": 27,
106 | "inApp": true,
107 | "trust": null,
108 | "errors": null,
109 | "lock": null,
110 | "sourceLink": null,
111 | "vars": null
112 | },
113 | {
114 | "filename": "index.js",
115 | "absPath": "/index.js",
116 | "module": "index",
117 | "package": null,
118 | "platform": null,
119 | "instructionAddr": null,
120 | "symbolAddr": null,
121 | "function": "OAuthProviderImpl.fetch",
122 | "rawFunction": null,
123 | "symbol": null,
124 | "context": [],
125 | "lineNo": 8029,
126 | "colNo": 24,
127 | "inApp": true,
128 | "trust": null,
129 | "errors": null,
130 | "lock": null,
131 | "sourceLink": null,
132 | "vars": null
133 | },
134 | {
135 | "filename": "index.js",
136 | "absPath": "/index.js",
137 | "module": "index",
138 | "package": null,
139 | "platform": null,
140 | "instructionAddr": null,
141 | "symbolAddr": null,
142 | "function": "Object.fetch",
143 | "rawFunction": null,
144 | "symbol": null,
145 | "context": [],
146 | "lineNo": 19631,
147 | "colNo": 28,
148 | "inApp": true,
149 | "trust": null,
150 | "errors": null,
151 | "lock": null,
152 | "sourceLink": null,
153 | "vars": null
154 | }
155 | ],
156 | "framesOmitted": null,
157 | "registers": null,
158 | "hasSystemFrames": true
159 | }
160 | }
161 | ],
162 | "hasSystemFrames": true,
163 | "excOmitted": null
164 | },
165 | "type": "exception"
166 | },
167 | {
168 | "data": {
169 | "apiTarget": null,
170 | "method": "GET",
171 | "url": "https://mcp.sentry.dev/sse",
172 | "query": [],
173 | "fragment": null,
174 | "data": null,
175 | "headers": [
176 | ["Accept", "text/event-stream"],
177 | ["Accept-Encoding", "gzip, br"],
178 | ["Accept-Language", "*"],
179 | ["Authorization", "[Filtered]"],
180 | ["Cache-Control", "no-cache"],
181 | ["Cf-Ipcountry", "GB"],
182 | ["Cf-Ray", "92d4c7266c8f48c9"],
183 | ["Cf-Visitor", "{\"scheme\":\"https\"}"],
184 | ["Connection", "Keep-Alive"],
185 | ["Host", "mcp.sentry.dev"],
186 | ["Pragma", "no-cache"],
187 | ["Sec-Fetch-Mode", "cors"],
188 | ["User-Agent", "node"],
189 | ["X-Forwarded-Proto", "https"]
190 | ],
191 | "cookies": [],
192 | "env": null,
193 | "inferredContentType": null
194 | },
195 | "type": "request"
196 | }
197 | ],
198 | "dist": null,
199 | "message": "",
200 | "title": "Error: Tool list_organizations is already registered",
201 | "location": "index.js",
202 | "user": {
203 | "id": null,
204 | "email": null,
205 | "username": null,
206 | "ip_address": "2a06:98c0:3600::103",
207 | "name": null,
208 | "geo": {
209 | "country_code": "US",
210 | "region": "United States"
211 | },
212 | "data": null
213 | },
214 | "contexts": {
215 | "cloud_resource": {
216 | "cloud.provider": "cloudflare",
217 | "type": "default"
218 | },
219 | "culture": {
220 | "timezone": "Europe/London",
221 | "type": "default"
222 | },
223 | "runtime": {
224 | "name": "cloudflare",
225 | "type": "runtime"
226 | },
227 | "trace": {
228 | "trace_id": "3032af8bcdfe4423b937fc5c041d5d82",
229 | "span_id": "953da703d2a6f4c7",
230 | "status": "unknown",
231 | "client_sample_rate": 1,
232 | "sampled": true,
233 | "type": "trace"
234 | }
235 | },
236 | "sdk": {
237 | "name": "sentry.javascript.cloudflare",
238 | "version": "9.12.0"
239 | },
240 | "context": {},
241 | "packages": {},
242 | "type": "error",
243 | "metadata": {
244 | "filename": "index.js",
245 | "function": "Object.fetch",
246 | "in_app_frame_mix": "in-app-only",
247 | "type": "Error",
248 | "value": "Tool list_organizations is already registered"
249 | },
250 | "tags": [
251 | {
252 | "key": "environment",
253 | "value": "development"
254 | },
255 | {
256 | "key": "handled",
257 | "value": "no"
258 | },
259 | {
260 | "key": "level",
261 | "value": "error"
262 | },
263 | {
264 | "key": "mechanism",
265 | "value": "cloudflare"
266 | },
267 | {
268 | "key": "runtime.name",
269 | "value": "cloudflare"
270 | },
271 | {
272 | "key": "url",
273 | "value": "https://mcp.sentry.dev/sse"
274 | }
275 | ],
276 | "platform": "javascript",
277 | "dateReceived": "2025-04-08T21:15:04.700878Z",
278 | "errors": [
279 | {
280 | "type": "js_no_source",
281 | "message": "Source code was not found",
282 | "data": {
283 | "symbolicator_type": "missing_source",
284 | "url": "/index.js"
285 | }
286 | }
287 | ],
288 | "occurrence": null,
289 | "_meta": {
290 | "entries": {
291 | "1": {
292 | "data": {
293 | "": null,
294 | "apiTarget": null,
295 | "method": null,
296 | "url": null,
297 | "query": null,
298 | "data": null,
299 | "headers": {
300 | "3": {
301 | "1": {
302 | "": {
303 | "rem": [["@password:filter", "s", 0, 10]],
304 | "len": 64,
305 | "chunks": [
306 | {
307 | "type": "redaction",
308 | "text": "[Filtered]",
309 | "rule_id": "@password:filter",
310 | "remark": "s"
311 | }
312 | ]
313 | }
314 | }
315 | }
316 | },
317 | "cookies": null,
318 | "env": null
319 | }
320 | }
321 | },
322 | "message": null,
323 | "user": null,
324 | "contexts": null,
325 | "sdk": null,
326 | "context": null,
327 | "packages": null,
328 | "tags": {}
329 | },
330 | "crashFile": null,
331 | "culprit": "Object.fetch(index)",
332 | "dateCreated": "2025-04-08T21:15:04Z",
333 | "fingerprints": ["60d1c667b173018c004e399b29dc927d"],
334 | "groupingConfig": {
335 | "enhancements": "KLUv_SAYwQAAkwKRs25ld3N0eWxlOjIwMjMtMDEtMTGQ",
336 | "id": "newstyle:2023-01-11"
337 | },
338 | "release": null,
339 | "userReport": null,
340 | "sdkUpdates": [],
341 | "resolvedWith": [null],
342 | "nextEventID": null,
343 | "previousEventID": "b7ed18493f4f4817a217b03839d4c017"
344 | }
345 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/process.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "namespace": "process",
3 | "description": "An operating system process.\n",
4 | "attributes": {
5 | "process.pid": {
6 | "description": "Process identifier (PID).\n",
7 | "type": "number",
8 | "stability": "development",
9 | "examples": ["1234"]
10 | },
11 | "process.parent_pid": {
12 | "description": "Parent Process identifier (PPID).\n",
13 | "type": "number",
14 | "stability": "development",
15 | "examples": ["111"]
16 | },
17 | "process.vpid": {
18 | "description": "Virtual process identifier.\n",
19 | "type": "number",
20 | "note": "The process ID within a PID namespace. This is not necessarily unique across all processes on the host but it is unique within the process namespace that the process exists within.\n",
21 | "stability": "development",
22 | "examples": ["12"]
23 | },
24 | "process.session_leader.pid": {
25 | "description": "The PID of the process's session leader. This is also the session ID (SID) of the process.\n",
26 | "type": "number",
27 | "stability": "development",
28 | "examples": ["14"]
29 | },
30 | "process.group_leader.pid": {
31 | "description": "The PID of the process's group leader. This is also the process group ID (PGID) of the process.\n",
32 | "type": "number",
33 | "stability": "development",
34 | "examples": ["23"]
35 | },
36 | "process.executable.build_id.gnu": {
37 | "description": "The GNU build ID as found in the `.note.gnu.build-id` ELF section (hex string).\n",
38 | "type": "string",
39 | "stability": "development",
40 | "examples": ["c89b11207f6479603b0d49bf291c092c2b719293"]
41 | },
42 | "process.executable.build_id.go": {
43 | "description": "The Go build ID as retrieved by `go tool buildid <go executable>`.\n",
44 | "type": "string",
45 | "stability": "development",
46 | "examples": [
47 | "foh3mEXu7BLZjsN9pOwG/kATcXlYVCDEFouRMQed_/WwRFB1hPo9LBkekthSPG/x8hMC8emW2cCjXD0_1aY"
48 | ]
49 | },
50 | "process.executable.build_id.htlhash": {
51 | "description": "Profiling specific build ID for executables. See the OTel specification for Profiles for more information.\n",
52 | "type": "string",
53 | "stability": "development",
54 | "examples": ["600DCAFE4A110000F2BF38C493F5FB92"]
55 | },
56 | "process.executable.name": {
57 | "description": "The name of the process executable. On Linux based systems, this SHOULD be set to the base name of the target of `/proc/[pid]/exe`. On Windows, this SHOULD be set to the base name of `GetProcessImageFileNameW`.\n",
58 | "type": "string",
59 | "stability": "development",
60 | "examples": ["otelcol"]
61 | },
62 | "process.executable.path": {
63 | "description": "The full path to the process executable. On Linux based systems, can be set to the target of `proc/[pid]/exe`. On Windows, can be set to the result of `GetProcessImageFileNameW`.\n",
64 | "type": "string",
65 | "stability": "development",
66 | "examples": ["/usr/bin/cmd/otelcol"]
67 | },
68 | "process.command": {
69 | "description": "The command used to launch the process (i.e. the command name). On Linux based systems, can be set to the zeroth string in `proc/[pid]/cmdline`. On Windows, can be set to the first parameter extracted from `GetCommandLineW`.\n",
70 | "type": "string",
71 | "stability": "development",
72 | "examples": ["cmd/otelcol"]
73 | },
74 | "process.command_line": {
75 | "description": "The full command used to launch the process as a single string representing the full command. On Windows, can be set to the result of `GetCommandLineW`. Do not set this if you have to assemble it just for monitoring; use `process.command_args` instead. SHOULD NOT be collected by default unless there is sanitization that excludes sensitive data.\n",
76 | "type": "string",
77 | "stability": "development",
78 | "examples": ["C:\\cmd\\otecol --config=\"my directory\\config.yaml\""]
79 | },
80 | "process.command_args": {
81 | "description": "All the command arguments (including the command/executable itself) as received by the process. On Linux-based systems (and some other Unixoid systems supporting procfs), can be set according to the list of null-delimited strings extracted from `proc/[pid]/cmdline`. For libc-based executables, this would be the full argv vector passed to `main`. SHOULD NOT be collected by default unless there is sanitization that excludes sensitive data.\n",
82 | "type": "string",
83 | "stability": "development",
84 | "examples": ["[\"cmd/otecol\",\"--config=config.yaml\"]"]
85 | },
86 | "process.args_count": {
87 | "description": "Length of the process.command_args array\n",
88 | "type": "number",
89 | "note": "This field can be useful for querying or performing bucket analysis on how many arguments were provided to start a process. More arguments may be an indication of suspicious activity.\n",
90 | "stability": "development",
91 | "examples": ["4"]
92 | },
93 | "process.owner": {
94 | "description": "The username of the user that owns the process.\n",
95 | "type": "string",
96 | "stability": "development",
97 | "examples": ["root"]
98 | },
99 | "process.user.id": {
100 | "description": "The effective user ID (EUID) of the process.\n",
101 | "type": "number",
102 | "stability": "development",
103 | "examples": ["1001"]
104 | },
105 | "process.user.name": {
106 | "description": "The username of the effective user of the process.\n",
107 | "type": "string",
108 | "stability": "development",
109 | "examples": ["root"]
110 | },
111 | "process.real_user.id": {
112 | "description": "The real user ID (RUID) of the process.\n",
113 | "type": "number",
114 | "stability": "development",
115 | "examples": ["1000"]
116 | },
117 | "process.real_user.name": {
118 | "description": "The username of the real user of the process.\n",
119 | "type": "string",
120 | "stability": "development",
121 | "examples": ["operator"]
122 | },
123 | "process.saved_user.id": {
124 | "description": "The saved user ID (SUID) of the process.\n",
125 | "type": "number",
126 | "stability": "development",
127 | "examples": ["1002"]
128 | },
129 | "process.saved_user.name": {
130 | "description": "The username of the saved user.\n",
131 | "type": "string",
132 | "stability": "development",
133 | "examples": ["operator"]
134 | },
135 | "process.runtime.name": {
136 | "description": "The name of the runtime of this process.\n",
137 | "type": "string",
138 | "stability": "development",
139 | "examples": ["OpenJDK Runtime Environment"]
140 | },
141 | "process.runtime.version": {
142 | "description": "The version of the runtime of this process, as returned by the runtime without modification.\n",
143 | "type": "string",
144 | "stability": "development",
145 | "examples": ["14.0.2"]
146 | },
147 | "process.runtime.description": {
148 | "description": "An additional description about the runtime of the process, for example a specific vendor customization of the runtime environment.\n",
149 | "type": "string",
150 | "stability": "development",
151 | "examples": ["Eclipse OpenJ9 Eclipse OpenJ9 VM openj9-0.21.0"]
152 | },
153 | "process.title": {
154 | "description": "Process title (proctitle)\n",
155 | "type": "string",
156 | "note": "In many Unix-like systems, process title (proctitle), is the string that represents the name or command line of a running process, displayed by system monitoring tools like ps, top, and htop.\n",
157 | "stability": "development",
158 | "examples": ["cat /etc/hostname", "xfce4-session", "bash"]
159 | },
160 | "process.creation.time": {
161 | "description": "The date and time the process was created, in ISO 8601 format.\n",
162 | "type": "string",
163 | "stability": "development",
164 | "examples": ["2023-11-21T09:25:34.853Z"]
165 | },
166 | "process.exit.time": {
167 | "description": "The date and time the process exited, in ISO 8601 format.\n",
168 | "type": "string",
169 | "stability": "development",
170 | "examples": ["2023-11-21T09:26:12.315Z"]
171 | },
172 | "process.exit.code": {
173 | "description": "The exit code of the process.\n",
174 | "type": "number",
175 | "stability": "development",
176 | "examples": ["127"]
177 | },
178 | "process.interactive": {
179 | "description": "Whether the process is connected to an interactive shell.\n",
180 | "type": "boolean",
181 | "stability": "development"
182 | },
183 | "process.working_directory": {
184 | "description": "The working directory of the process.\n",
185 | "type": "string",
186 | "stability": "development",
187 | "examples": ["/root"]
188 | },
189 | "process.context_switch_type": {
190 | "description": "Specifies whether the context switches for this data point were voluntary or involuntary.",
191 | "type": "string",
192 | "stability": "development",
193 | "examples": ["voluntary", "involuntary"]
194 | },
195 | "process.paging.fault_type": {
196 | "description": "The type of page fault for this data point. Type `major` is for major/hard page faults, and `minor` is for minor/soft page faults.\n",
197 | "type": "string",
198 | "stability": "development",
199 | "examples": ["major", "minor"]
200 | },
201 | "process.environment_variable": {
202 | "description": "Process environment variables, `<key>` being the environment variable name, the value being the environment variable value.\n",
203 | "type": "string",
204 | "note": "Examples:\n\n- an environment variable `USER` with value `\"ubuntu\"` SHOULD be recorded\nas the `process.environment_variable.USER` attribute with value `\"ubuntu\"`.\n\n- an environment variable `PATH` with value `\"/usr/local/bin:/usr/bin\"`\nSHOULD be recorded as the `process.environment_variable.PATH` attribute\nwith value `\"/usr/local/bin:/usr/bin\"`.\n",
205 | "stability": "development",
206 | "examples": ["ubuntu", "/usr/local/bin:/usr/bin"]
207 | }
208 | }
209 | }
210 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/database.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "namespace": "database",
3 | "description": "This group defines the attributes used to describe telemetry in the context of databases.\n",
4 | "attributes": {
5 | "db.collection.name": {
6 | "description": "The name of a collection (table, container) within the database.",
7 | "type": "string",
8 | "note": "It is RECOMMENDED to capture the value as provided by the application\nwithout attempting to do any case normalization.\n\nThe collection name SHOULD NOT be extracted from `db.query.text`,\nwhen the database system supports query text with multiple collections\nin non-batch operations.\n\nFor batch operations, if the individual operations are known to have the same\ncollection name then that collection name SHOULD be used.\n",
9 | "stability": "stable",
10 | "examples": ["public.users", "customers"]
11 | },
12 | "db.namespace": {
13 | "description": "The name of the database, fully qualified within the server address and port.\n",
14 | "type": "string",
15 | "note": "If a database system has multiple namespace components, they SHOULD be concatenated from the most general to the most specific namespace component, using `|` as a separator between the components. Any missing components (and their associated separators) SHOULD be omitted.\nSemantic conventions for individual database systems SHOULD document what `db.namespace` means in the context of that system.\nIt is RECOMMENDED to capture the value as provided by the application without attempting to do any case normalization.\n",
16 | "stability": "stable",
17 | "examples": ["customers", "test.users"]
18 | },
19 | "db.operation.name": {
20 | "description": "The name of the operation or command being executed.\n",
21 | "type": "string",
22 | "note": "It is RECOMMENDED to capture the value as provided by the application\nwithout attempting to do any case normalization.\n\nThe operation name SHOULD NOT be extracted from `db.query.text`,\nwhen the database system supports query text with multiple operations\nin non-batch operations.\n\nIf spaces can occur in the operation name, multiple consecutive spaces\nSHOULD be normalized to a single space.\n\nFor batch operations, if the individual operations are known to have the same operation name\nthen that operation name SHOULD be used prepended by `BATCH `,\notherwise `db.operation.name` SHOULD be `BATCH` or some other database\nsystem specific term if more applicable.\n",
23 | "stability": "stable",
24 | "examples": ["findAndModify", "HMSET", "SELECT"]
25 | },
26 | "db.query.text": {
27 | "description": "The database query being executed.\n",
28 | "type": "string",
29 | "note": "For sanitization see [Sanitization of `db.query.text`](/docs/database/database-spans.md#sanitization-of-dbquerytext).\nFor batch operations, if the individual operations are known to have the same query text then that query text SHOULD be used, otherwise all of the individual query texts SHOULD be concatenated with separator `; ` or some other database system specific separator if more applicable.\nParameterized query text SHOULD NOT be sanitized. Even though parameterized query text can potentially have sensitive data, by using a parameterized query the user is giving a strong signal that any sensitive data will be passed as parameter values, and the benefit to observability of capturing the static part of the query text by default outweighs the risk.\n",
30 | "stability": "stable",
31 | "examples": [
32 | "SELECT * FROM wuser_table where username = ?",
33 | "SET mykey ?"
34 | ]
35 | },
36 | "db.query.parameter": {
37 | "description": "A database query parameter, with `<key>` being the parameter name, and the attribute value being a string representation of the parameter value.\n",
38 | "type": "string",
39 | "note": "If a query parameter has no name and instead is referenced only by index,\nthen `<key>` SHOULD be the 0-based index.\n\n`db.query.parameter.<key>` SHOULD match\nup with the parameterized placeholders present in `db.query.text`.\n\n`db.query.parameter.<key>` SHOULD NOT be captured on batch operations.\n\nExamples:\n\n- For a query `SELECT * FROM users where username = %s` with the parameter `\"jdoe\"`,\n the attribute `db.query.parameter.0` SHOULD be set to `\"jdoe\"`.\n\n- For a query `\"SELECT * FROM users WHERE username = %(username)s;` with parameter\n `username = \"jdoe\"`, the attribute `db.query.parameter.username` SHOULD be set to `\"jdoe\"`.\n",
40 | "stability": "development",
41 | "examples": ["someval", "55"]
42 | },
43 | "db.query.summary": {
44 | "description": "Low cardinality summary of a database query.\n",
45 | "type": "string",
46 | "note": "The query summary describes a class of database queries and is useful\nas a grouping key, especially when analyzing telemetry for database\ncalls involving complex queries.\n\nSummary may be available to the instrumentation through\ninstrumentation hooks or other means. If it is not available, instrumentations\nthat support query parsing SHOULD generate a summary following\n[Generating query summary](/docs/database/database-spans.md#generating-a-summary-of-the-query)\nsection.\n",
47 | "stability": "stable",
48 | "examples": [
49 | "SELECT wuser_table",
50 | "INSERT shipping_details SELECT orders",
51 | "get user by id"
52 | ]
53 | },
54 | "db.stored_procedure.name": {
55 | "description": "The name of a stored procedure within the database.",
56 | "type": "string",
57 | "note": "It is RECOMMENDED to capture the value as provided by the application\nwithout attempting to do any case normalization.\n\nFor batch operations, if the individual operations are known to have the same\nstored procedure name then that stored procedure name SHOULD be used.\n",
58 | "stability": "stable",
59 | "examples": ["GetCustomer"]
60 | },
61 | "db.operation.parameter": {
62 | "description": "A database operation parameter, with `<key>` being the parameter name, and the attribute value being a string representation of the parameter value.\n",
63 | "type": "string",
64 | "note": "For example, a client-side maximum number of rows to read from the database\nMAY be recorded as the `db.operation.parameter.max_rows` attribute.\n\n`db.query.text` parameters SHOULD be captured using `db.query.parameter.<key>`\ninstead of `db.operation.parameter.<key>`.\n",
65 | "stability": "development",
66 | "examples": ["someval", "55"]
67 | },
68 | "db.operation.batch.size": {
69 | "description": "The number of queries included in a batch operation.",
70 | "type": "number",
71 | "note": "Operations are only considered batches when they contain two or more operations, and so `db.operation.batch.size` SHOULD never be `1`.\n",
72 | "stability": "stable",
73 | "examples": ["2", "3", "4"]
74 | },
75 | "db.response.status_code": {
76 | "description": "Database response status code.",
77 | "type": "string",
78 | "note": "The status code returned by the database. Usually it represents an error code, but may also represent partial success, warning, or differentiate between various types of successful outcomes.\nSemantic conventions for individual database systems SHOULD document what `db.response.status_code` means in the context of that system.\n",
79 | "stability": "stable",
80 | "examples": ["102", "ORA-17002", "08P01", "404"]
81 | },
82 | "db.response.returned_rows": {
83 | "description": "Number of rows returned by the operation.",
84 | "type": "number",
85 | "stability": "development",
86 | "examples": ["10", "30", "1000"]
87 | },
88 | "db.system.name": {
89 | "description": "The database management system (DBMS) product as identified by the client instrumentation.",
90 | "type": "string",
91 | "note": "The actual DBMS may differ from the one identified by the client. For example, when using PostgreSQL client libraries to connect to a CockroachDB, the `db.system.name` is set to `postgresql` based on the instrumentation's best knowledge.\n",
92 | "stability": "stable",
93 | "examples": [
94 | "other_sql",
95 | "softwareag.adabas",
96 | "actian.ingres",
97 | "aws.dynamodb",
98 | "aws.redshift",
99 | "azure.cosmosdb",
100 | "intersystems.cache",
101 | "cassandra",
102 | "clickhouse",
103 | "cockroachdb",
104 | "couchbase",
105 | "couchdb",
106 | "derby",
107 | "elasticsearch",
108 | "firebirdsql",
109 | "gcp.spanner",
110 | "geode",
111 | "h2database",
112 | "hbase",
113 | "hive",
114 | "hsqldb",
115 | "ibm.db2",
116 | "ibm.informix",
117 | "ibm.netezza",
118 | "influxdb",
119 | "instantdb",
120 | "mariadb",
121 | "memcached",
122 | "mongodb",
123 | "microsoft.sql_server",
124 | "mysql",
125 | "neo4j",
126 | "opensearch",
127 | "oracle.db",
128 | "postgresql",
129 | "redis",
130 | "sap.hana",
131 | "sap.maxdb",
132 | "sqlite",
133 | "teradata",
134 | "trino"
135 | ]
136 | },
137 | "db.client.connection.state": {
138 | "description": "The state of a connection in the pool",
139 | "type": "string",
140 | "stability": "development",
141 | "examples": ["idle", "used"]
142 | },
143 | "db.client.connection.pool.name": {
144 | "description": "The name of the connection pool; unique within the instrumented application. In case the connection pool implementation doesn't provide a name, instrumentation SHOULD use a combination of parameters that would make the name unique, for example, combining attributes `server.address`, `server.port`, and `db.namespace`, formatted as `server.address:server.port/db.namespace`. Instrumentations that generate connection pool name following different patterns SHOULD document it.\n",
145 | "type": "string",
146 | "stability": "development",
147 | "examples": ["myDataSource"]
148 | }
149 | }
150 | }
151 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/api-client/errors.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * API Error Class Hierarchy
3 | *
4 | * This module defines a hierarchical error system for the Sentry API client
5 | * that automatically categorizes errors based on HTTP status codes.
6 | *
7 | * Key principles:
8 | * - 4xx errors (ApiClientError) are user input errors, not reported to Sentry
9 | * - 5xx errors (ApiServerError) are system errors, reported to Sentry
10 | * - Specific error types provide additional context and helper methods
11 | * - The hierarchy enables type-safe error handling without manual status checks
12 | */
13 |
14 | /**
15 | * Base class for all API errors.
16 | * Contains common properties for all API error types.
17 | * Status is optional since some errors (like client-side validation) don't have HTTP status codes.
18 | */
19 | export class ApiError extends Error {
20 | constructor(
21 | message: string,
22 | public readonly status?: number,
23 | public readonly detail?: string,
24 | public readonly responseBody?: unknown,
25 | ) {
26 | super(message);
27 | this.name = "ApiError";
28 | // Ensure proper prototype chain for instanceof checks
29 | Object.setPrototypeOf(this, ApiError.prototype);
30 | }
31 | }
32 |
33 | /**
34 | * Client errors (4xx) - User input errors that should NOT be reported to Sentry.
35 | * These typically indicate issues with the request that the user can fix.
36 | * Also includes client-side validation errors that don't have HTTP status codes.
37 | */
38 | export class ApiClientError extends ApiError {
39 | constructor(
40 | message: string,
41 | status?: number,
42 | detail?: string,
43 | responseBody?: unknown,
44 | ) {
45 | super(message, status, detail, responseBody);
46 | this.name = "ApiClientError";
47 | Object.setPrototypeOf(this, ApiClientError.prototype);
48 | }
49 |
50 | /**
51 | * Convert to a user-friendly formatted message.
52 | * Returns the standard format: "API error (status): message" or just "API error: message" if no status.
53 | * For 404 errors, adds helpful context about checking parameters.
54 | */
55 | toUserMessage(): string {
56 | const baseMessage = this.status
57 | ? `API error (${this.status}): ${this.message}`
58 | : `API error: ${this.message}`;
59 |
60 | // Add helpful context for 404 errors, especially generic ones
61 | if (this.status === 404) {
62 | // Check if the message is generic
63 | const genericMessages = [
64 | "not found",
65 | "the requested resource does not exist",
66 | "resource does not exist",
67 | "resource not found",
68 | ];
69 |
70 | const isGeneric = genericMessages.some(
71 | (msg) =>
72 | this.message.toLowerCase() === msg ||
73 | this.message.toLowerCase().includes("requested resource"),
74 | );
75 |
76 | if (isGeneric) {
77 | return `${baseMessage}. Please verify that the organization, project, or resource ID is correct and that you have access to it.`;
78 | }
79 | // For specific messages like "Project not found", just add a hint
80 | return `${baseMessage}. Please verify the parameters are correct.`;
81 | }
82 |
83 | return baseMessage;
84 | }
85 |
86 | /**
87 | * Check if this is a permission/authorization error (403)
88 | */
89 | isPermissionError(): boolean {
90 | return this.status === 403;
91 | }
92 |
93 | /**
94 | * Check if this is a not found error (404)
95 | */
96 | isNotFoundError(): boolean {
97 | return this.status === 404;
98 | }
99 |
100 | /**
101 | * Check if this is a validation error (400 or 422)
102 | */
103 | isValidationError(): boolean {
104 | return this.status === 400 || this.status === 422;
105 | }
106 |
107 | /**
108 | * Check if this is an authentication error (401)
109 | */
110 | isAuthenticationError(): boolean {
111 | return this.status === 401;
112 | }
113 |
114 | /**
115 | * Check if this is a rate limit error (429)
116 | */
117 | isRateLimitError(): boolean {
118 | return this.status === 429;
119 | }
120 | }
121 |
122 | /**
123 | * Permission denied error (403).
124 | * Includes special handling for multi-project access errors.
125 | */
126 | export class ApiPermissionError extends ApiClientError {
127 | constructor(message: string, detail?: string, responseBody?: unknown) {
128 | super(message, 403, detail, responseBody);
129 | this.name = "ApiPermissionError";
130 | Object.setPrototypeOf(this, ApiPermissionError.prototype);
131 | }
132 |
133 | /**
134 | * Check if this is the specific multi-project access error
135 | */
136 | isMultiProjectAccessError(): boolean {
137 | return (
138 | this.message.includes("multiple projects") ||
139 | this.message.includes("multi project") ||
140 | this.message.includes("multi-project") ||
141 | this.message.includes(
142 | "You do not have access to query across multiple projects",
143 | )
144 | );
145 | }
146 | }
147 |
148 | /**
149 | * Resource not found error (404).
150 | * Can include additional context about what resource was not found.
151 | */
152 | export class ApiNotFoundError extends ApiClientError {
153 | constructor(
154 | message: string,
155 | detail?: string,
156 | responseBody?: unknown,
157 | public readonly resourceType?: string,
158 | public readonly resourceId?: string,
159 | ) {
160 | super(message, 404, detail, responseBody);
161 | this.name = "ApiNotFoundError";
162 | Object.setPrototypeOf(this, ApiNotFoundError.prototype);
163 | }
164 | }
165 |
166 | /**
167 | * Validation error (400 or 422 for API responses, or no status for client-side validation).
168 | * Indicates the request was malformed or contained invalid data.
169 | */
170 | export class ApiValidationError extends ApiClientError {
171 | constructor(
172 | message: string,
173 | status?: 400 | 422,
174 | detail?: string,
175 | responseBody?: unknown,
176 | public readonly validationErrors?: Record<string, string[]>,
177 | ) {
178 | super(message, status, detail, responseBody);
179 | this.name = "ApiValidationError";
180 | Object.setPrototypeOf(this, ApiValidationError.prototype);
181 | }
182 | }
183 |
184 | /**
185 | * Authentication error (401).
186 | * Indicates the request lacks valid authentication credentials.
187 | */
188 | export class ApiAuthenticationError extends ApiClientError {
189 | constructor(message: string, detail?: string, responseBody?: unknown) {
190 | super(message, 401, detail, responseBody);
191 | this.name = "ApiAuthenticationError";
192 | Object.setPrototypeOf(this, ApiAuthenticationError.prototype);
193 | }
194 | }
195 |
196 | /**
197 | * Rate limit error (429).
198 | * Includes retry-after information when available.
199 | */
200 | export class ApiRateLimitError extends ApiClientError {
201 | constructor(
202 | message: string,
203 | detail?: string,
204 | responseBody?: unknown,
205 | public readonly retryAfter?: number,
206 | ) {
207 | super(message, 429, detail, responseBody);
208 | this.name = "ApiRateLimitError";
209 | Object.setPrototypeOf(this, ApiRateLimitError.prototype);
210 | }
211 | }
212 |
213 | /**
214 | * Server errors (5xx) - System errors that SHOULD be reported to Sentry.
215 | * These indicate problems with the server that are not the user's fault.
216 | */
217 | export class ApiServerError extends ApiError {
218 | constructor(
219 | message: string,
220 | status: number,
221 | detail?: string,
222 | responseBody?: unknown,
223 | ) {
224 | super(message, status, detail, responseBody);
225 | this.name = "ApiServerError";
226 | Object.setPrototypeOf(this, ApiServerError.prototype);
227 | }
228 |
229 | /**
230 | * Check if this is a gateway/proxy error (502, 503, 504)
231 | */
232 | isGatewayError(): boolean {
233 | return this.status === 502 || this.status === 503 || this.status === 504;
234 | }
235 |
236 | /**
237 | * Check if this is an internal server error (500)
238 | */
239 | isInternalError(): boolean {
240 | return this.status === 500;
241 | }
242 | }
243 |
244 | /**
245 | * Factory function to create the appropriate error type based on HTTP status code.
246 | * This centralizes the logic for determining which error class to instantiate.
247 | * This is only used for actual API responses, so status is required.
248 | */
249 | export function createApiError(
250 | message: string,
251 | status: number,
252 | detail?: string,
253 | responseBody?: unknown,
254 | ): ApiError {
255 | // Apply message improvements for known error patterns
256 | let improvedMessage = message;
257 |
258 | // Handle the multi-project access error that comes in various forms
259 | if (
260 | message.includes(
261 | "You do not have the multi project stream feature enabled",
262 | ) ||
263 | message.includes("You cannot view events from multiple projects")
264 | ) {
265 | improvedMessage =
266 | "You do not have access to query across multiple projects. Please select a project for your query.";
267 | return new ApiPermissionError(improvedMessage, detail, responseBody);
268 | }
269 |
270 | // Create specific error types based on status code
271 | switch (status) {
272 | case 401:
273 | return new ApiAuthenticationError(message, detail, responseBody);
274 |
275 | case 403:
276 | return new ApiPermissionError(message, detail, responseBody);
277 |
278 | case 404:
279 | // TODO: Could extract resource type/ID from the request context
280 | return new ApiNotFoundError(message, detail, responseBody);
281 |
282 | case 400:
283 | case 422: {
284 | // Try to extract validation errors if present
285 | let validationErrors: Record<string, string[]> | undefined;
286 | if (
287 | responseBody &&
288 | typeof responseBody === "object" &&
289 | "errors" in responseBody
290 | ) {
291 | validationErrors = responseBody.errors as Record<string, string[]>;
292 | }
293 | return new ApiValidationError(
294 | message,
295 | status as 400 | 422,
296 | detail,
297 | responseBody,
298 | validationErrors,
299 | );
300 | }
301 |
302 | case 429: {
303 | // Try to extract retry-after header if available
304 | let retryAfter: number | undefined;
305 | if (responseBody && typeof responseBody === "object") {
306 | if ("retry_after" in responseBody) {
307 | retryAfter = Number(responseBody.retry_after);
308 | } else if ("retryAfter" in responseBody) {
309 | retryAfter = Number(responseBody.retryAfter);
310 | }
311 | }
312 | return new ApiRateLimitError(message, detail, responseBody, retryAfter);
313 | }
314 |
315 | default:
316 | // Generic categorization for other status codes
317 | if (status >= 400 && status < 500) {
318 | return new ApiClientError(message, status, detail, responseBody);
319 | }
320 | if (status >= 500 && status < 600) {
321 | return new ApiServerError(message, status, detail, responseBody);
322 | }
323 | // Fallback for unusual status codes
324 | return new ApiError(message, status, detail, responseBody);
325 | }
326 | }
327 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/test-fixtures.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { Event } from "../api-client/types";
2 | import type { z } from "zod";
3 | import type {
4 | FrameInterface,
5 | ExceptionInterface,
6 | ThreadEntrySchema,
7 | } from "../api-client/schema";
8 |
9 | // Type aliases for cleaner code
10 | type Frame = z.infer<typeof FrameInterface>;
11 | type ExceptionValue = z.infer<typeof ExceptionInterface>;
12 | type Thread = z.infer<typeof ThreadEntrySchema>;
13 | type StackTrace = { frames: Frame[] };
14 |
15 | /**
16 | * Test fixture factories for creating Event objects with minimal boilerplate.
17 | * These factories provide sensible defaults while allowing customization.
18 | */
19 |
20 | // Frame factory with common defaults
21 | export function createFrame(overrides: Partial<Frame> = {}): Frame {
22 | return {
23 | filename: "/app/main.js",
24 | function: "main",
25 | lineNo: 42,
26 | ...overrides,
27 | };
28 | }
29 |
30 | // Platform-specific frame factories
31 | export const frameFactories = {
32 | python: (overrides: Partial<Frame> = {}) =>
33 | createFrame({
34 | filename: "/app/main.py",
35 | function: "process_data",
36 | ...overrides,
37 | }),
38 |
39 | java: (overrides: Partial<Frame> = {}) =>
40 | createFrame({
41 | filename: "Example.java",
42 | module: "com.example.Example",
43 | function: "doSomething",
44 | ...overrides,
45 | }),
46 |
47 | javascript: (overrides: Partial<Frame> = {}) =>
48 | createFrame({
49 | filename: "/app/main.js",
50 | function: "handleRequest",
51 | colNo: 15,
52 | ...overrides,
53 | }),
54 |
55 | ruby: (overrides: Partial<Frame> = {}) =>
56 | createFrame({
57 | filename: "/app/main.rb",
58 | function: "process",
59 | ...overrides,
60 | }),
61 |
62 | php: (overrides: Partial<Frame> = {}) =>
63 | createFrame({
64 | filename: "/app/main.php",
65 | function: "handleRequest",
66 | ...overrides,
67 | }),
68 | };
69 |
70 | // StackTrace factory
71 | export function createStackTrace(frames: Frame[]): StackTrace {
72 | return { frames };
73 | }
74 |
75 | // Exception value factory
76 | export function createExceptionValue(
77 | overrides: Partial<ExceptionValue> = {},
78 | ): ExceptionValue {
79 | return {
80 | type: "Error",
81 | value: "Something went wrong",
82 | stacktrace: createStackTrace([createFrame()]),
83 | ...overrides,
84 | };
85 | }
86 |
87 | // Thread factory
88 | export function createThread(overrides: Partial<Thread> = {}): Thread {
89 | return {
90 | id: 1,
91 | name: "main",
92 | crashed: true,
93 | stacktrace: createStackTrace([createFrame()]),
94 | ...overrides,
95 | };
96 | }
97 |
98 | // Base type that includes all possible fields from ErrorEvent and TransactionEvent
99 | // This allows the builder to mutate fields without type casts
100 | type MutableEvent = {
101 | id: string;
102 | title: string;
103 | message: string | null;
104 | platform: string | null;
105 | type: "error" | "transaction";
106 | entries: Array<{
107 | type: string;
108 | data: any;
109 | }>;
110 | contexts?: Record<string, any>;
111 | tags?: Array<{ key: string; value: string }>;
112 | _meta?: unknown;
113 | dateReceived?: string;
114 | // ErrorEvent specific fields
115 | culprit?: string | null;
116 | dateCreated?: string;
117 | // TransactionEvent specific fields
118 | occurrence?: {
119 | id?: string;
120 | projectId?: number;
121 | eventId?: string;
122 | fingerprint?: string[];
123 | issueTitle: string;
124 | subtitle?: string;
125 | resourceId?: string | null;
126 | evidenceData?: Record<string, any>;
127 | evidenceDisplay?: Array<{
128 | name: string;
129 | value: string;
130 | important?: boolean;
131 | }>;
132 | type?: number;
133 | detectionTime?: number;
134 | level?: string;
135 | culprit?: string | null;
136 | priority?: number;
137 | assignee?: string | null;
138 | // Allow extra fields for test flexibility (like issueType)
139 | [key: string]: unknown;
140 | };
141 | };
142 |
143 | // Event factory with builder pattern
144 | export class EventBuilder {
145 | private event: MutableEvent;
146 |
147 | constructor(platform = "javascript") {
148 | this.event = {
149 | id: "test123",
150 | title: "Test Event",
151 | message: null,
152 | platform,
153 | type: "error",
154 | entries: [],
155 | contexts: {},
156 | culprit: null,
157 | dateCreated: new Date().toISOString(),
158 | };
159 | }
160 |
161 | withId(id: string): this {
162 | this.event.id = id;
163 | return this;
164 | }
165 |
166 | withPlatform(platform: string): this {
167 | this.event.platform = platform;
168 | return this;
169 | }
170 |
171 | withException(exception: ExceptionValue): this {
172 | this.event.entries.push({
173 | type: "exception",
174 | data: {
175 | values: [exception],
176 | },
177 | });
178 | return this;
179 | }
180 |
181 | withChainedExceptions(exceptions: ExceptionValue[]): this {
182 | this.event.entries.push({
183 | type: "exception",
184 | data: {
185 | values: exceptions,
186 | },
187 | });
188 | return this;
189 | }
190 |
191 | withThread(thread: Thread): this {
192 | const existingThread = this.event.entries.find((e) => e.type === "threads");
193 | if (
194 | existingThread?.data &&
195 | typeof existingThread.data === "object" &&
196 | "values" in existingThread.data &&
197 | Array.isArray(existingThread.data.values)
198 | ) {
199 | existingThread.data.values.push(thread);
200 | } else {
201 | this.event.entries.push({
202 | type: "threads",
203 | data: {
204 | values: [thread],
205 | },
206 | });
207 | }
208 | return this;
209 | }
210 |
211 | withMessage(message: string): this {
212 | this.event.entries.push({
213 | type: "message",
214 | data: {
215 | formatted: message,
216 | },
217 | });
218 | return this;
219 | }
220 |
221 | withTitle(title: string): this {
222 | this.event.title = title;
223 | return this;
224 | }
225 |
226 | withType(type: "error" | "transaction"): this {
227 | this.event.type = type;
228 | return this;
229 | }
230 |
231 | withContexts(contexts: Record<string, any>): this {
232 | this.event.contexts = contexts;
233 | return this;
234 | }
235 |
236 | withOccurrence(occurrence: {
237 | id?: string;
238 | projectId?: number;
239 | eventId?: string;
240 | fingerprint?: string[];
241 | issueTitle: string;
242 | subtitle?: string;
243 | resourceId?: string | null;
244 | evidenceData?: Record<string, any>;
245 | evidenceDisplay?: Array<{
246 | name: string;
247 | value: string;
248 | important?: boolean;
249 | }>;
250 | type?: number;
251 | detectionTime?: number;
252 | level?: string;
253 | culprit?: string | null;
254 | priority?: number;
255 | assignee?: string | null;
256 | // Allow extra fields for test flexibility (like issueType)
257 | [key: string]: unknown;
258 | }): this {
259 | this.event.occurrence = occurrence;
260 | return this;
261 | }
262 |
263 | withEntry(entry: { type: string; data: any }): this {
264 | this.event.entries.push(entry);
265 | return this;
266 | }
267 |
268 | build(): Event {
269 | // Cast is safe here because we ensure MutableEvent has all fields needed
270 | // for either ErrorEvent or TransactionEvent based on the type field
271 | return { ...this.event } as Event;
272 | }
273 | }
274 |
275 | // Convenience factories for common test scenarios
276 | export const testEvents = {
277 | // Simple Python exception
278 | pythonException: (errorMessage = "Invalid value") =>
279 | new EventBuilder("python")
280 | .withException(
281 | createExceptionValue({
282 | type: "ValueError",
283 | value: errorMessage,
284 | stacktrace: createStackTrace([
285 | frameFactories.python({ lineNo: 42 }),
286 | frameFactories.python({
287 | filename: "/app/utils.py",
288 | function: "validate",
289 | lineNo: 15,
290 | }),
291 | ]),
292 | }),
293 | )
294 | .build(),
295 |
296 | // Java thread error
297 | javaThreadError: (message = "Test error") =>
298 | new EventBuilder("java")
299 | .withTitle("Test Error")
300 | .withType("error")
301 | .withMessage(message)
302 | .withThread(
303 | createThread({
304 | id: 187,
305 | name: "CONTRACT_WORKER",
306 | state: "RUNNABLE",
307 | stacktrace: createStackTrace([
308 | frameFactories.java({
309 | filename: "Thread.java",
310 | module: "java.lang.Thread",
311 | function: "run",
312 | lineNo: 833,
313 | }),
314 | frameFactories.java({
315 | filename: "AeronServer.java",
316 | module: "com.citics.eqd.mq.aeron.AeronServer",
317 | function: "lambda$start$3",
318 | lineNo: 110,
319 | }),
320 | ]),
321 | }),
322 | )
323 | .build(),
324 |
325 | // Enhanced frame with context and variables
326 | enhancedFrame: (platform = "python") => {
327 | const frame = frameFactories[platform as keyof typeof frameFactories]({
328 | inApp: true,
329 | context: [
330 | [40, ' raise ValueError("User not found")'],
331 | [41, " "],
332 | [42, " balance = user.account.balance"],
333 | [43, " if balance < amount:"],
334 | [44, " raise InsufficientFundsError()"],
335 | ],
336 | vars: {
337 | amount: 150.0,
338 | user_id: "usr_123456",
339 | user: null,
340 | },
341 | });
342 |
343 | return new EventBuilder(platform)
344 | .withException(
345 | createExceptionValue({
346 | type: "ValueError",
347 | value: "Something went wrong",
348 | stacktrace: createStackTrace([frame]),
349 | }),
350 | )
351 | .build();
352 | },
353 | };
354 |
355 | // Helper to create frames with context lines
356 | export function createFrameWithContext(
357 | frame: Partial<Frame>,
358 | contextLines: Array<[number, string]>,
359 | vars?: Record<string, any>,
360 | ): Frame {
361 | return createFrame({
362 | ...frame,
363 | inApp: true,
364 | context: contextLines,
365 | vars,
366 | });
367 | }
368 |
369 | // Advanced test fixtures for specific scenarios
370 | export const advancedFixtures = {
371 | // Create a minimal event with just an error message
372 | minimalError: (message: string, platform = "javascript") =>
373 | new EventBuilder(platform)
374 | .withException(createExceptionValue({ value: message }))
375 | .build(),
376 |
377 | // Create an event with multiple exceptions (chained errors)
378 | chainedExceptions: (platform = "javascript") =>
379 | new EventBuilder(platform)
380 | .withException(
381 | createExceptionValue({
382 | type: "Error",
383 | value: "High level error",
384 | stacktrace: createStackTrace([createFrame({ lineNo: 100 })]),
385 | }),
386 | )
387 | .withException(
388 | createExceptionValue({
389 | type: "CausedBy",
390 | value: "Low level error",
391 | stacktrace: createStackTrace([createFrame({ lineNo: 50 })]),
392 | }),
393 | )
394 | .build(),
395 |
396 | // Create event with specific context data
397 | withContextData: (contexts: Record<string, any>) => {
398 | return new EventBuilder().withContexts(contexts).build();
399 | },
400 | };
401 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/analyze-issue-with-seer.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { setTag } from "@sentry/core";
3 | import { defineTool } from "../internal/tool-helpers/define";
4 | import { apiServiceFromContext } from "../internal/tool-helpers/api";
5 | import { parseIssueParams } from "../internal/tool-helpers/issue";
6 | import {
7 | getStatusDisplayName,
8 | isTerminalStatus,
9 | getHumanInterventionGuidance,
10 | getOutputForAutofixStep,
11 | SEER_POLLING_INTERVAL,
12 | SEER_TIMEOUT,
13 | SEER_MAX_RETRIES,
14 | SEER_INITIAL_RETRY_DELAY,
15 | } from "../internal/tool-helpers/seer";
16 | import { retryWithBackoff } from "../internal/fetch-utils";
17 | import type { ServerContext } from "../types";
18 | import { ApiError, ApiServerError } from "../api-client/index";
19 | import {
20 | ParamOrganizationSlug,
21 | ParamRegionUrl,
22 | ParamIssueShortId,
23 | ParamIssueUrl,
24 | } from "../schema";
25 |
26 | export default defineTool({
27 | name: "analyze_issue_with_seer",
28 | requiredScopes: ["seer"],
29 | description: [
30 | "Use Seer to analyze production errors and get detailed root cause analysis with specific code fixes.",
31 | "",
32 | "Use this tool when you need:",
33 | "- Detailed AI-powered root cause analysis",
34 | "- Specific code fixes and implementation guidance",
35 | "- Step-by-step troubleshooting for complex issues",
36 | "- Understanding why an error is happening in production",
37 | "",
38 | "What this tool provides:",
39 | "- Root cause analysis with code-level explanations",
40 | "- Specific file locations and line numbers where errors occur",
41 | "- Concrete code fixes you can apply",
42 | "- Step-by-step implementation guidance",
43 | "",
44 | "This tool automatically:",
45 | "1. Checks if analysis already exists (instant results)",
46 | "2. Starts new AI analysis if needed (~2-5 minutes)",
47 | "3. Returns complete fix recommendations",
48 | "",
49 | "<examples>",
50 | '### User: "What\'s causing this error? https://my-org.sentry.io/issues/PROJECT-1Z43"',
51 | "",
52 | "```",
53 | "analyze_issue_with_seer(issueUrl='https://my-org.sentry.io/issues/PROJECT-1Z43')",
54 | "```",
55 | "",
56 | '### User: "Can you help me understand why this is failing in production?"',
57 | "",
58 | "```",
59 | "analyze_issue_with_seer(organizationSlug='my-organization', issueId='ERROR-456')",
60 | "```",
61 | "</examples>",
62 | "",
63 | "<hints>",
64 | "- Use this tool when you need deeper analysis beyond basic issue details",
65 | "- If the user provides an issueUrl, extract it and use that parameter alone",
66 | "- The analysis includes actual code snippets and fixes, not just error descriptions",
67 | "- Results are cached - subsequent calls return instantly",
68 | "</hints>",
69 | ].join("\n"),
70 | inputSchema: {
71 | organizationSlug: ParamOrganizationSlug.optional(),
72 | regionUrl: ParamRegionUrl.optional(),
73 | issueId: ParamIssueShortId.optional(),
74 | issueUrl: ParamIssueUrl.optional(),
75 | instruction: z
76 | .string()
77 | .describe("Optional custom instruction for the AI analysis")
78 | .optional(),
79 | },
80 | annotations: {
81 | readOnlyHint: false,
82 | destructiveHint: false,
83 | openWorldHint: true,
84 | },
85 | async handler(params, context: ServerContext) {
86 | const apiService = apiServiceFromContext(context, {
87 | regionUrl: params.regionUrl,
88 | });
89 | const { organizationSlug: orgSlug, issueId: parsedIssueId } =
90 | parseIssueParams({
91 | organizationSlug: params.organizationSlug,
92 | issueId: params.issueId,
93 | issueUrl: params.issueUrl,
94 | });
95 |
96 | setTag("organization.slug", orgSlug);
97 |
98 | let output = `# Seer Analysis for Issue ${parsedIssueId}\n\n`;
99 |
100 | // Step 1: Check if analysis already exists
101 | let autofixState = await retryWithBackoff(
102 | () =>
103 | apiService.getAutofixState({
104 | organizationSlug: orgSlug,
105 | issueId: parsedIssueId!,
106 | }),
107 | {
108 | maxRetries: SEER_MAX_RETRIES,
109 | initialDelay: SEER_INITIAL_RETRY_DELAY,
110 | shouldRetry: (error) => {
111 | // Retry on server errors (5xx) or non-API errors (network issues)
112 | return (
113 | error instanceof ApiServerError || !(error instanceof ApiError)
114 | );
115 | },
116 | },
117 | );
118 |
119 | // Step 2: Start analysis if none exists
120 | if (!autofixState.autofix) {
121 | output += `Starting new analysis...\n\n`;
122 | const startResult = await apiService.startAutofix({
123 | organizationSlug: orgSlug,
124 | issueId: parsedIssueId,
125 | instruction: params.instruction,
126 | });
127 | output += `Analysis started with Run ID: ${startResult.run_id}\n\n`;
128 |
129 | // Give it a moment to initialize
130 | await new Promise((resolve) => setTimeout(resolve, 1000));
131 |
132 | // Refresh state with retry logic
133 | autofixState = await retryWithBackoff(
134 | () =>
135 | apiService.getAutofixState({
136 | organizationSlug: orgSlug,
137 | issueId: parsedIssueId!,
138 | }),
139 | {
140 | maxRetries: SEER_MAX_RETRIES,
141 | initialDelay: SEER_INITIAL_RETRY_DELAY,
142 | shouldRetry: (error) => {
143 | // Retry on server errors (5xx) or non-API errors (network issues)
144 | return (
145 | error instanceof ApiServerError || !(error instanceof ApiError)
146 | );
147 | },
148 | },
149 | );
150 | } else {
151 | output += `Found existing analysis (Run ID: ${autofixState.autofix.run_id})\n\n`;
152 |
153 | // Check if existing analysis is already complete
154 | const existingStatus = autofixState.autofix.status;
155 | if (isTerminalStatus(existingStatus)) {
156 | // Return results immediately, no polling needed
157 | output += `## Analysis ${getStatusDisplayName(existingStatus)}\n\n`;
158 |
159 | for (const step of autofixState.autofix.steps) {
160 | output += getOutputForAutofixStep(step);
161 | output += "\n";
162 | }
163 |
164 | if (existingStatus !== "COMPLETED") {
165 | output += `\n**Status**: ${existingStatus}\n`;
166 | output += getHumanInterventionGuidance(existingStatus);
167 | output += "\n";
168 | }
169 |
170 | return output;
171 | }
172 | }
173 |
174 | // Step 3: Poll until complete or timeout (only for non-terminal states)
175 | const startTime = Date.now();
176 | let lastStatus = "";
177 | let consecutiveErrors = 0;
178 |
179 | while (Date.now() - startTime < SEER_TIMEOUT) {
180 | if (!autofixState.autofix) {
181 | output += `Error: Analysis state lost. Please try again by running:\n`;
182 | output += `\`\`\`\n`;
183 | output += params.issueUrl
184 | ? `analyze_issue_with_seer(issueUrl="${params.issueUrl}")`
185 | : `analyze_issue_with_seer(organizationSlug="${orgSlug}", issueId="${parsedIssueId}")`;
186 | output += `\n\`\`\`\n`;
187 | return output;
188 | }
189 |
190 | const status = autofixState.autofix.status;
191 |
192 | // Check if completed (terminal state)
193 | if (isTerminalStatus(status)) {
194 | output += `## Analysis ${getStatusDisplayName(status)}\n\n`;
195 |
196 | // Add all step outputs
197 | for (const step of autofixState.autofix.steps) {
198 | output += getOutputForAutofixStep(step);
199 | output += "\n";
200 | }
201 |
202 | if (status !== "COMPLETED") {
203 | output += `\n**Status**: ${status}\n`;
204 | output += getHumanInterventionGuidance(status);
205 | }
206 |
207 | return output;
208 | }
209 |
210 | // Update status if changed
211 | if (status !== lastStatus) {
212 | const activeStep = autofixState.autofix.steps.find(
213 | (step) =>
214 | step.status === "PROCESSING" || step.status === "IN_PROGRESS",
215 | );
216 | if (activeStep) {
217 | output += `Processing: ${activeStep.title}...\n`;
218 | }
219 | lastStatus = status;
220 | }
221 |
222 | // Wait before next poll
223 | await new Promise((resolve) =>
224 | setTimeout(resolve, SEER_POLLING_INTERVAL),
225 | );
226 |
227 | // Refresh state with error handling
228 | try {
229 | autofixState = await retryWithBackoff(
230 | () =>
231 | apiService.getAutofixState({
232 | organizationSlug: orgSlug,
233 | issueId: parsedIssueId!,
234 | }),
235 | {
236 | maxRetries: SEER_MAX_RETRIES,
237 | initialDelay: SEER_INITIAL_RETRY_DELAY,
238 | shouldRetry: (error) => {
239 | // Retry on server errors (5xx) or non-API errors (network issues)
240 | return (
241 | error instanceof ApiServerError || !(error instanceof ApiError)
242 | );
243 | },
244 | },
245 | );
246 | consecutiveErrors = 0; // Reset error counter on success
247 | } catch (error) {
248 | consecutiveErrors++;
249 |
250 | // If we've had too many consecutive errors, give up
251 | if (consecutiveErrors >= 3) {
252 | output += `\n## Error During Analysis\n\n`;
253 | output += `Unable to retrieve analysis status after multiple attempts.\n`;
254 | output += `Error: ${error instanceof Error ? error.message : String(error)}\n\n`;
255 | output += `You can check the status later by running the same command again:\n`;
256 | output += `\`\`\`\n`;
257 | output += params.issueUrl
258 | ? `analyze_issue_with_seer(issueUrl="${params.issueUrl}")`
259 | : `analyze_issue_with_seer(organizationSlug="${orgSlug}", issueId="${parsedIssueId}")`;
260 | output += `\n\`\`\`\n`;
261 | return output;
262 | }
263 |
264 | // Log the error but continue polling
265 | output += `Temporary error retrieving status (attempt ${consecutiveErrors}/3), retrying...\n`;
266 | }
267 | }
268 |
269 | // Show current progress
270 | if (autofixState.autofix) {
271 | output += `**Current Status**: ${getStatusDisplayName(autofixState.autofix.status)}\n\n`;
272 | for (const step of autofixState.autofix.steps) {
273 | output += getOutputForAutofixStep(step);
274 | output += "\n";
275 | }
276 | }
277 |
278 | // Timeout reached
279 | output += `\n## Analysis Timed Out\n\n`;
280 | output += `The analysis is taking longer than expected (>${SEER_TIMEOUT / 1000}s).\n\n`;
281 |
282 | output += `\nYou can check the status later by running the same command again:\n`;
283 | output += `\`\`\`\n`;
284 | output += params.issueUrl
285 | ? `analyze_issue_with_seer(issueUrl="${params.issueUrl}")`
286 | : `analyze_issue_with_seer(organizationSlug="${orgSlug}", issueId="${parsedIssueId}")`;
287 | output += `\n\`\`\`\n`;
288 |
289 | return output;
290 | },
291 | });
292 |
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/gen_ai.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "namespace": "gen_ai",
3 | "description": "This document defines the attributes used to describe telemetry in the context of Generative Artificial Intelligence (GenAI) Models requests and responses.\n",
4 | "attributes": {
5 | "gen_ai.provider.name": {
6 | "description": "The Generative AI provider as identified by the client or server instrumentation.",
7 | "type": "string",
8 | "note": "The attribute SHOULD be set based on the instrumentation's best\nknowledge and may differ from the actual model provider.\n\nMultiple providers, including Azure OpenAI, Gemini, and AI hosting platforms\nare accessible using the OpenAI REST API and corresponding client libraries,\nbut may proxy or host models from different providers.\n\nThe `gen_ai.request.model`, `gen_ai.response.model`, and `server.address`\nattributes may help identify the actual system in use.\n\nThe `gen_ai.provider.name` attribute acts as a discriminator that\nidentifies the GenAI telemetry format flavor specific to that provider\nwithin GenAI semantic conventions.\nIt SHOULD be set consistently with provider-specific attributes and signals.\nFor example, GenAI spans, metrics, and events related to AWS Bedrock\nshould have the `gen_ai.provider.name` set to `aws.bedrock` and include\napplicable `aws.bedrock.*` attributes and are not expected to include\n`openai.*` attributes.\n",
9 | "stability": "development",
10 | "examples": [
11 | "openai",
12 | "gcp.gen_ai",
13 | "gcp.vertex_ai",
14 | "gcp.gemini",
15 | "anthropic",
16 | "cohere",
17 | "azure.ai.inference",
18 | "azure.ai.openai",
19 | "ibm.watsonx.ai",
20 | "aws.bedrock",
21 | "perplexity",
22 | "x_ai",
23 | "deepseek",
24 | "groq",
25 | "mistral_ai"
26 | ]
27 | },
28 | "gen_ai.request.model": {
29 | "description": "The name of the GenAI model a request is being made to.",
30 | "type": "string",
31 | "stability": "development",
32 | "examples": ["gpt-4"]
33 | },
34 | "gen_ai.request.max_tokens": {
35 | "description": "The maximum number of tokens the model generates for a request.",
36 | "type": "number",
37 | "stability": "development",
38 | "examples": ["100"]
39 | },
40 | "gen_ai.request.choice.count": {
41 | "description": "The target number of candidate completions to return.",
42 | "type": "number",
43 | "stability": "development",
44 | "examples": ["3"]
45 | },
46 | "gen_ai.request.temperature": {
47 | "description": "The temperature setting for the GenAI request.",
48 | "type": "number",
49 | "stability": "development",
50 | "examples": ["0"]
51 | },
52 | "gen_ai.request.top_p": {
53 | "description": "The top_p sampling setting for the GenAI request.",
54 | "type": "number",
55 | "stability": "development",
56 | "examples": ["1"]
57 | },
58 | "gen_ai.request.top_k": {
59 | "description": "The top_k sampling setting for the GenAI request.",
60 | "type": "number",
61 | "stability": "development",
62 | "examples": ["1"]
63 | },
64 | "gen_ai.request.stop_sequences": {
65 | "description": "List of sequences that the model will use to stop generating further tokens.",
66 | "type": "string",
67 | "stability": "development",
68 | "examples": ["[\"forest\",\"lived\"]"]
69 | },
70 | "gen_ai.request.frequency_penalty": {
71 | "description": "The frequency penalty setting for the GenAI request.",
72 | "type": "number",
73 | "stability": "development",
74 | "examples": ["0.1"]
75 | },
76 | "gen_ai.request.presence_penalty": {
77 | "description": "The presence penalty setting for the GenAI request.",
78 | "type": "number",
79 | "stability": "development",
80 | "examples": ["0.1"]
81 | },
82 | "gen_ai.request.encoding_formats": {
83 | "description": "The encoding formats requested in an embeddings operation, if specified.",
84 | "type": "string",
85 | "note": "In some GenAI systems the encoding formats are called embedding types. Also, some GenAI systems only accept a single format per request.\n",
86 | "stability": "development",
87 | "examples": ["[\"base64\"]", "[\"float\",\"binary\"]"]
88 | },
89 | "gen_ai.request.seed": {
90 | "description": "Requests with same seed value more likely to return same result.",
91 | "type": "number",
92 | "stability": "development",
93 | "examples": ["100"]
94 | },
95 | "gen_ai.response.id": {
96 | "description": "The unique identifier for the completion.",
97 | "type": "string",
98 | "stability": "development",
99 | "examples": ["chatcmpl-123"]
100 | },
101 | "gen_ai.response.model": {
102 | "description": "The name of the model that generated the response.",
103 | "type": "string",
104 | "stability": "development",
105 | "examples": ["gpt-4-0613"]
106 | },
107 | "gen_ai.response.finish_reasons": {
108 | "description": "Array of reasons the model stopped generating tokens, corresponding to each generation received.",
109 | "type": "string",
110 | "stability": "development",
111 | "examples": ["[\"stop\"]", "[\"stop\",\"length\"]"]
112 | },
113 | "gen_ai.usage.input_tokens": {
114 | "description": "The number of tokens used in the GenAI input (prompt).",
115 | "type": "number",
116 | "stability": "development",
117 | "examples": ["100"]
118 | },
119 | "gen_ai.usage.output_tokens": {
120 | "description": "The number of tokens used in the GenAI response (completion).",
121 | "type": "number",
122 | "stability": "development",
123 | "examples": ["180"]
124 | },
125 | "gen_ai.token.type": {
126 | "description": "The type of token being counted.",
127 | "type": "string",
128 | "stability": "development",
129 | "examples": ["input", "output", "output"]
130 | },
131 | "gen_ai.conversation.id": {
132 | "description": "The unique identifier for a conversation (session, thread), used to store and correlate messages within this conversation.",
133 | "type": "string",
134 | "stability": "development",
135 | "examples": ["conv_5j66UpCpwteGg4YSxUnt7lPY"]
136 | },
137 | "gen_ai.agent.id": {
138 | "description": "The unique identifier of the GenAI agent.",
139 | "type": "string",
140 | "stability": "development",
141 | "examples": ["asst_5j66UpCpwteGg4YSxUnt7lPY"]
142 | },
143 | "gen_ai.agent.name": {
144 | "description": "Human-readable name of the GenAI agent provided by the application.",
145 | "type": "string",
146 | "stability": "development",
147 | "examples": ["Math Tutor", "Fiction Writer"]
148 | },
149 | "gen_ai.agent.description": {
150 | "description": "Free-form description of the GenAI agent provided by the application.",
151 | "type": "string",
152 | "stability": "development",
153 | "examples": ["Helps with math problems", "Generates fiction stories"]
154 | },
155 | "gen_ai.tool.name": {
156 | "description": "Name of the tool utilized by the agent.",
157 | "type": "string",
158 | "stability": "development",
159 | "examples": ["Flights"]
160 | },
161 | "gen_ai.tool.call.id": {
162 | "description": "The tool call identifier.",
163 | "type": "string",
164 | "stability": "development",
165 | "examples": ["call_mszuSIzqtI65i1wAUOE8w5H4"]
166 | },
167 | "gen_ai.tool.description": {
168 | "description": "The tool description.",
169 | "type": "string",
170 | "stability": "development",
171 | "examples": ["Multiply two numbers"]
172 | },
173 | "gen_ai.tool.type": {
174 | "description": "Type of the tool utilized by the agent",
175 | "type": "string",
176 | "note": "Extension: A tool executed on the agent-side to directly call external APIs, bridging the gap between the agent and real-world systems.\n Agent-side operations involve actions that are performed by the agent on the server or within the agent's controlled environment.\nFunction: A tool executed on the client-side, where the agent generates parameters for a predefined function, and the client executes the logic.\n Client-side operations are actions taken on the user's end or within the client application.\nDatastore: A tool used by the agent to access and query structured or unstructured external data for retrieval-augmented tasks or knowledge updates.\n",
177 | "stability": "development",
178 | "examples": ["function", "extension", "datastore"]
179 | },
180 | "gen_ai.data_source.id": {
181 | "description": "The data source identifier.",
182 | "type": "string",
183 | "note": "Data sources are used by AI agents and RAG applications to store grounding data. A data source may be an external database, object store, document collection, website, or any other storage system used by the GenAI agent or application. The `gen_ai.data_source.id` SHOULD match the identifier used by the GenAI system rather than a name specific to the external storage, such as a database or object store. Semantic conventions referencing `gen_ai.data_source.id` MAY also leverage additional attributes, such as `db.*`, to further identify and describe the data source.\n",
184 | "stability": "development",
185 | "examples": ["H7STPQYOND"]
186 | },
187 | "gen_ai.operation.name": {
188 | "description": "The name of the operation being performed.",
189 | "type": "string",
190 | "note": "If one of the predefined values applies, but specific system uses a different name it's RECOMMENDED to document it in the semantic conventions for specific GenAI system and use system-specific name in the instrumentation. If a different name is not documented, instrumentation libraries SHOULD use applicable predefined value.\n",
191 | "stability": "development",
192 | "examples": [
193 | "chat",
194 | "generate_content",
195 | "text_completion",
196 | "embeddings",
197 | "create_agent",
198 | "invoke_agent",
199 | "execute_tool"
200 | ]
201 | },
202 | "gen_ai.output.type": {
203 | "description": "Represents the content type requested by the client.",
204 | "type": "string",
205 | "note": "This attribute SHOULD be used when the client requests output of a specific type. The model may return zero or more outputs of this type.\nThis attribute specifies the output modality and not the actual output format. For example, if an image is requested, the actual output could be a URL pointing to an image file.\nAdditional output format details may be recorded in the future in the `gen_ai.output.{type}.*` attributes.\n",
206 | "stability": "development",
207 | "examples": ["text", "json", "image", "speech"]
208 | }
209 | }
210 | }
211 |
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/fragments/remote-setup.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { Accordion } from "../ui/accordion";
2 | import CodeSnippet from "../ui/code-snippet";
3 | import SetupGuide from "./setup-guide";
4 | import { Prose } from "../ui/prose";
5 | import { NPM_REMOTE_NAME } from "@/constants";
6 | import { Button } from "../ui/button";
7 | import { Heading } from "../ui/base";
8 |
9 | const mcpServerName = import.meta.env.DEV ? "sentry-dev" : "sentry";
10 |
11 | export default function RemoteSetup() {
12 | const endpoint = new URL("/mcp", window.location.href).href;
13 |
14 | const mcpRemoteSnippet = `npx ${NPM_REMOTE_NAME}@latest ${endpoint}`;
15 | // the shared configuration for all clients
16 | const coreConfig = {
17 | command: "npx",
18 | args: ["-y", `${NPM_REMOTE_NAME}@latest`, endpoint],
19 | };
20 |
21 | const codexRemoteConfigToml = [
22 | "[mcp_servers.sentry]",
23 | 'command = "npx"',
24 | `args = ["-y", "${NPM_REMOTE_NAME}@latest", "${endpoint}"]`,
25 | ].join("\n");
26 |
27 | const sentryMCPConfig = {
28 | url: endpoint,
29 | };
30 |
31 | // https://code.visualstudio.com/docs/copilot/chat/mcp-servers
32 | const vsCodeHandler = `vscode:mcp/install?${encodeURIComponent(
33 | JSON.stringify({
34 | name: mcpServerName,
35 | serverUrl: endpoint,
36 | }),
37 | )}`;
38 | const zedInstructions = JSON.stringify(
39 | {
40 | context_servers: {
41 | [mcpServerName]: coreConfig,
42 | settings: {},
43 | },
44 | },
45 | undefined,
46 | 2,
47 | );
48 |
49 | return (
50 | <>
51 | <Prose className="mb-6">
52 | <p>
53 | If you've got a client that natively supports the current MCP
54 | specification, including OAuth, you can connect directly.
55 | </p>
56 | <CodeSnippet snippet={endpoint} />
57 | <p>
58 | <strong>Organization and Project Constraints:</strong> You can
59 | optionally constrain your MCP session to a specific organization and
60 | project by including them in the URL path:
61 | </p>
62 | <ul>
63 | <li>
64 | <code>{endpoint}/:organization</code> — Restricts the session to a
65 | specific organization
66 | </li>
67 | <li>
68 | <code>{endpoint}/:organization/:project</code> — Restricts the
69 | session to a specific organization and project
70 | </li>
71 | </ul>
72 | </Prose>
73 | <Heading as="h3">Integration Guides</Heading>
74 | <Accordion type="single" collapsible>
75 | <SetupGuide id="cursor" title="Cursor">
76 | <Button
77 | variant="secondary"
78 | size="sm"
79 | onClick={() => {
80 | const deepLink =
81 | "cursor://anysphere.cursor-deeplink/mcp/install?name=Sentry&config=eyJ1cmwiOiJodHRwczovL21jcC5zZW50cnkuZGV2L21jcCJ9";
82 | window.location.href = deepLink;
83 | }}
84 | className="mt-2 mb-2 bg-violet-300 text-black hover:bg-violet-400 hover:text-black"
85 | >
86 | Install in Cursor
87 | </Button>
88 | <ol>
89 | <li>
90 | Or manually: <strong>Cmd + Shift + J</strong> to open Cursor
91 | Settings.
92 | </li>
93 | <li>
94 | Select <strong>Tools and Integrations</strong>.
95 | </li>
96 | <li>
97 | Select <strong>New MCP Server</strong>.
98 | </li>
99 | <li>
100 | <CodeSnippet
101 | noMargin
102 | snippet={JSON.stringify(
103 | {
104 | mcpServers: {
105 | sentry: sentryMCPConfig,
106 | },
107 | },
108 | undefined,
109 | 2,
110 | )}
111 | />
112 | </li>
113 | <li>
114 | Optional: To use the service with <code>cursor-agent</code>:
115 | <CodeSnippet noMargin snippet={`cursor-agent mcp login sentry`} />
116 | </li>
117 | </ol>
118 | </SetupGuide>
119 |
120 | <SetupGuide id="claude-code" title="Claude Code">
121 | <ol>
122 | <li>Open your terminal to access the CLI.</li>
123 | <li>
124 | <CodeSnippet
125 | noMargin
126 | snippet={`claude mcp add --transport http sentry ${endpoint}`}
127 | />
128 | </li>
129 | <li>
130 | This will trigger an OAuth authentication flow to connect Claude
131 | Code to your Sentry account.
132 | </li>
133 | <li>
134 | You may need to manually authenticate if it doesnt happen
135 | automatically, which can be doe via <code>/mcp</code>.
136 | </li>
137 | </ol>
138 | <p>
139 | <small>
140 | For more details, see the{" "}
141 | <a href="https://docs.anthropic.com/en/docs/claude-code/mcp">
142 | Claude Code MCP documentation
143 | </a>
144 | .
145 | </small>
146 | </p>
147 | </SetupGuide>
148 |
149 | <SetupGuide id="codex-cli" title="Codex">
150 | <ol>
151 | <li>Open your terminal to access the CLI.</li>
152 | <li>
153 | <CodeSnippet
154 | noMargin
155 | snippet={`codex mcp add sentry -- ${coreConfig.command} ${coreConfig.args.join(" ")}`}
156 | />
157 | </li>
158 | <li>
159 | Next time you run <code>codex</code>, the Sentry MCP server will
160 | be available. It will automatically open the OAuth flow to connect
161 | to your Sentry account.
162 | </li>
163 | </ol>
164 | Or
165 | <ol>
166 | <li>
167 | Edit <code>~/.codex/config.toml</code> and add the remote MCP
168 | configuration:
169 | <CodeSnippet noMargin snippet={codexRemoteConfigToml} />
170 | </li>
171 | <li>
172 | Save the file and restart any running <code>codex</code> session
173 | </li>
174 | <li>
175 | Next time you run <code>codex</code>, the Sentry MCP server will
176 | be available. It will automatically open the OAuth flow to connect
177 | to your Sentry account.
178 | </li>
179 | </ol>
180 | </SetupGuide>
181 |
182 | <SetupGuide id="windsurf" title="Windsurf">
183 | <ol>
184 | <li>Open Windsurf Settings.</li>
185 | <li>
186 | Under <strong>Cascade</strong>, you'll find{" "}
187 | <strong>Model Context Protocol Servers</strong>.
188 | </li>
189 | <li>
190 | Select <strong>Add Server</strong>.
191 | </li>
192 | <li>
193 | <CodeSnippet
194 | noMargin
195 | snippet={JSON.stringify(
196 | {
197 | mcpServers: {
198 | sentry: coreConfig,
199 | },
200 | },
201 | undefined,
202 | 2,
203 | )}
204 | />
205 | </li>
206 | </ol>
207 | </SetupGuide>
208 |
209 | <SetupGuide id="vscode" title="Visual Studio Code">
210 | <Button
211 | variant="secondary"
212 | size="sm"
213 | onClick={() => {
214 | window.location.href = vsCodeHandler;
215 | }}
216 | className="mt-2 mb-2 bg-violet-300 text-black hover:bg-violet-400 hover:text-black"
217 | >
218 | Install in VSCode
219 | </Button>
220 | <p>
221 | If this doesn't work, you can manually add the server using the
222 | following steps:
223 | </p>
224 | <ol>
225 | <li>
226 | <strong>CMD + P</strong> and search for{" "}
227 | <strong>MCP: Add Server</strong>.
228 | </li>
229 | <li>
230 | Select <strong>HTTP (HTTP or Server-Sent Events)</strong>.
231 | </li>
232 | <li>
233 | Enter the following configuration, and hit enter
234 | <strong> {endpoint}</strong>
235 | </li>
236 | <li>
237 | Enter the name <strong>Sentry</strong> and hit enter.
238 | </li>
239 | <li>Allow the authentication flow to complete.</li>
240 | <li>
241 | Activate the server using <strong>MCP: List Servers</strong> and
242 | selecting <strong>Sentry</strong>, and selecting{" "}
243 | <strong>Start Server</strong>.
244 | </li>
245 | </ol>
246 | <p>
247 | <small>Note: MCP is supported in VSCode 1.99 and above.</small>
248 | </p>
249 | </SetupGuide>
250 |
251 | <SetupGuide id="warp" title="Warp">
252 | <ol>
253 | <li>
254 | Open{" "}
255 | <a
256 | href="https://warp.dev"
257 | target="_blank"
258 | rel="noopener noreferrer"
259 | >
260 | Warp
261 | </a>{" "}
262 | and navigate to MCP server settings using one of these methods:
263 | <ul>
264 | <li>
265 | From Warp Drive: <strong>Personal → MCP Servers</strong>
266 | </li>
267 | <li>
268 | From Command Palette: search for{" "}
269 | <strong>Open MCP Servers</strong>
270 | </li>
271 | <li>
272 | From Settings:{" "}
273 | <strong>Settings → AI → Manage MCP servers</strong>
274 | </li>
275 | </ul>
276 | </li>
277 | <li>
278 | Click <strong>+ Add</strong> button.
279 | </li>
280 | <li>
281 | Select <strong>CLI Server (Command)</strong> option.
282 | </li>
283 | <li>
284 | <CodeSnippet
285 | noMargin
286 | snippet={JSON.stringify(
287 | {
288 | Sentry: {
289 | ...coreConfig,
290 | env: {},
291 | working_directory: null,
292 | },
293 | },
294 | undefined,
295 | 2,
296 | )}
297 | />
298 | </li>
299 | </ol>
300 | <p>
301 | <small>
302 | For more details, see the{" "}
303 | <a
304 | href="https://docs.warp.dev/knowledge-and-collaboration/mcp"
305 | target="_blank"
306 | rel="noopener noreferrer"
307 | >
308 | Warp MCP documentation
309 | </a>
310 | .
311 | </small>
312 | </p>
313 | </SetupGuide>
314 |
315 | <SetupGuide id="zed" title="Zed">
316 | <ol>
317 | <li>
318 | <strong>CMD + ,</strong> to open Zed settings.
319 | </li>
320 | <li>
321 | <CodeSnippet noMargin snippet={zedInstructions} />
322 | </li>
323 | </ol>
324 | </SetupGuide>
325 | </Accordion>
326 | </>
327 | );
328 | }
329 |
```
--------------------------------------------------------------------------------
/docs/cloudflare/mcpagent-architecture.md:
--------------------------------------------------------------------------------
```markdown
1 | # McpAgent Architecture Documentation
2 |
3 | ## Overview
4 |
5 | McpAgent is a base class from the Cloudflare agents library that provides the foundation for building MCP (Model Context Protocol) servers as Durable Objects. It handles the protocol transport, state management, and lifecycle of MCP server instances.
6 |
7 | Our Sentry MCP implementation extends this base class to provide authenticated, constraint-scoped access to Sentry's API through MCP tools and resources.
8 |
9 | ## Key Components
10 |
11 | ### 1. Sentry MCP Agent Implementation
12 |
13 | ```typescript
14 | class SentryMCPBase extends McpAgent<
15 | Env,
16 | { constraints?: Constraints },
17 | WorkerProps & {
18 | organizationSlug?: string;
19 | projectSlug?: string;
20 | }
21 | > {
22 | // MCP server created in constructor for performance
23 | server = new McpServer({
24 | name: "Sentry MCP",
25 | version: LIB_VERSION,
26 | });
27 |
28 | // Lifecycle methods
29 | async init(): Promise<void>;
30 | async fetch(request: Request): Promise<Response>;
31 |
32 | // State management (simplified)
33 | state: { constraints?: Constraints };
34 | setState(state): void;
35 | }
36 | ```
37 |
38 | ### 2. Transport Methods and Constraint Handling
39 |
40 | McpAgent supports two transport protocols:
41 |
42 | - **`serve()`**: Streamable HTTP transport (recommended)
43 | - **`serveSSE()`**: Server-Sent Events transport (legacy)
44 |
45 | Our implementation adds **constraint verification** before routing to handlers:
46 |
47 | ```typescript
48 | // Usage in index.ts
49 | export default {
50 | async fetch(request: Request, env: Env, ctx: ExecutionContext) {
51 | const url = new URL(request.url);
52 |
53 | // SSE endpoint
54 | if (url.pathname === "/sse" || url.pathname === "/sse/message") {
55 | return SentryMCP.serveSSE("/sse").fetch(request, env, ctx);
56 | }
57 |
58 | // Pattern match for constraint extraction
59 | const pattern = new URLPattern({ pathname: "/mcp/:org?/:project?" });
60 | const result = pattern.exec(url);
61 | if (result) {
62 | const { groups } = result.pathname;
63 |
64 | // Extract constraints from URL
65 | const organizationSlug = groups?.org ?? "";
66 | const projectSlug = groups?.project ?? "";
67 |
68 | // Verify access using OAuth token
69 | const verification = await verifyConstraintsAccess(
70 | { organizationSlug, projectSlug },
71 | {
72 | accessToken: ctx.props?.accessToken,
73 | sentryHost: env.SENTRY_HOST || "sentry.io",
74 | }
75 | );
76 |
77 | if (!verification.ok) {
78 | return new Response(verification.message, {
79 | status: verification.status,
80 | });
81 | }
82 |
83 | // Mutate props with verified constraints
84 | ctx.props.constraints = verification.constraints;
85 |
86 | return SentryMCP.serve(pattern.pathname).fetch(request, env, ctx);
87 | }
88 |
89 | return new Response("Not found", { status: 404 });
90 | },
91 | };
92 | ```
93 |
94 | ## Lifecycle
95 |
96 | ### 1. Durable Object Creation
97 |
98 | ```
99 | Request arrives → URL pattern matching → Constraint verification →
100 | OAuth Provider validates token → Extracts props from token →
101 | Mutates props with constraints → Creates DO with sessionId →
102 | DO instance created → init() called
103 | ```
104 |
105 | **Key points:**
106 | - DO ID (sessionId) is generated from connection context by agents library
107 | - Different constraint contexts get separate DO instances
108 | - Props are mutated with verified constraints before DO creation
109 | - DO persists in memory between requests (~30 seconds idle timeout)
110 |
111 | ### 2. Request Flow
112 |
113 | ```
114 | First Request (DO Creation):
115 | 1. URL pattern matching extracts org/project from path
116 | 2. verifyConstraintsAccess() validates org/project exist and user has access
117 | 3. OAuth provider validates token
118 | 4. Props mutated with verified constraints
119 | 5. DO instance created with unique sessionId for this constraint context
120 | 6. init() called - configures MCP server with constraints
121 | 7. fetch() called for the request (guaranteed after init() completes)
122 | 8. Returns response
123 |
124 | Subsequent Requests (Same Constraint Context):
125 | 1. Routes to existing DO instance (same sessionId)
126 | 2. fetch() called - init() is NOT called again
127 | 3. Returns response
128 |
129 | After Hibernation:
130 | 1. DO wakes from hibernation
131 | 2. init() called to restore state and reconfigure MCP server
132 | 3. fetch() called for the request (guaranteed after init() completes)
133 | 4. Returns response
134 | ```
135 |
136 | **Important Lifecycle Guarantee**: McpAgent ensures `init()` always completes before `fetch()` is called. This is guaranteed both on DO creation and when waking from hibernation.
137 |
138 | **Constraint Immutability**: Once props are mutated with constraints and the DO is created, the constraint configuration remains immutable throughout the DO's lifetime.
139 |
140 | ### 3. Method Responsibilities
141 |
142 | **`init()`**:
143 | - Called when DO is created or wakes from hibernation
144 | - Configures the MCP server with user authentication and constraints
145 | - Restores constraint state from props
146 | - NOT called for every request
147 |
148 | **`fetch()`**:
149 | - Called for EVERY request
150 | - Handles the actual MCP protocol communication
151 | - Can access `this.props` including mutated constraints
152 | - All MCP tools automatically scoped to the constraint context
153 | - Returns the MCP response
154 |
155 | ## Props System
156 |
157 | Props are the bridge between OAuth, constraint verification, and the Durable Object:
158 |
159 | ```typescript
160 | // Base props from OAuth
161 | type WorkerProps = ServerContext & {
162 | id: string; // User ID from OAuth token
163 | name: string; // User name
164 | scope: string; // OAuth scopes
165 | accessToken: string; // Sentry API token
166 | };
167 |
168 | // Extended props with constraints
169 | type ExtendedProps = WorkerProps & {
170 | organizationSlug?: string; // Extracted from URL
171 | projectSlug?: string; // Extracted from URL
172 | constraints?: Constraints; // Verified constraints
173 | };
174 | ```
175 |
176 | **How props work:**
177 | 1. URL pattern matching extracts org/project from request path
178 | 2. `verifyConstraintsAccess()` validates constraints using OAuth token
179 | 3. OAuth provider decrypts the OAuth token and extracts base props
180 | 4. Props are mutated with verified constraints before DO creation
181 | 5. DO can access complete props via `this.props`
182 |
183 | **Important:** Props are mutated once during request routing, then remain immutable throughout the DO's lifetime.
184 |
185 | ## Constraint Verification System
186 |
187 | The constraint verification system validates org/project access before DO creation:
188 |
189 | ```typescript
190 | export async function verifyConstraintsAccess(
191 | { organizationSlug, projectSlug }: Constraints,
192 | { accessToken, sentryHost }: { accessToken: string; sentryHost?: string }
193 | ): Promise<
194 | | { ok: true; constraints: Constraints }
195 | | { ok: false; status: number; message: string; eventId?: string }
196 | > {
197 | // Verify organization exists and user has access
198 | const org = await api.getOrganization(organizationSlug);
199 | const regionUrl = org.links?.regionUrl || null;
200 |
201 | // Verify project access if specified
202 | if (projectSlug) {
203 | await api.getProject(
204 | { organizationSlug, projectSlugOrId: projectSlug },
205 | regionUrl ? { host: new URL(regionUrl).host } : undefined
206 | );
207 | }
208 |
209 | return {
210 | ok: true,
211 | constraints: { organizationSlug, projectSlug, regionUrl },
212 | };
213 | }
214 | ```
215 |
216 | **Benefits:**
217 | - Early validation prevents unauthorized access
218 | - Regional URL detection for proper API routing
219 | - Consistent error handling with proper HTTP status codes
220 | - Integration with Sentry error tracking
221 |
222 | ## State Management
223 |
224 | Our implementation uses simplified state management focused on constraints:
225 |
226 | ```typescript
227 | // State structure
228 | type State = {
229 | constraints?: Constraints;
230 | };
231 |
232 | // In init()
233 | if (!this.state) {
234 | this.setState({
235 | constraints: this.props.constraints,
236 | });
237 | }
238 |
239 | // Access state
240 | const constraints = this.state.constraints || {};
241 | ```
242 |
243 | State is persisted automatically and restored after hibernation. The constraints from props are preserved in state for consistency.
244 |
245 | ## Storage
246 |
247 | Durable Objects have access to persistent storage:
248 |
249 | ```typescript
250 | // Store data
251 | await this.ctx.storage.put("key", value);
252 |
253 | // Retrieve data
254 | const value = await this.ctx.storage.get("key");
255 |
256 | // Delete data
257 | await this.ctx.storage.delete("key");
258 | ```
259 |
260 | Storage persists across DO hibernation and restarts.
261 |
262 | ## Hibernation
263 |
264 | After ~30 seconds of inactivity:
265 | 1. DO goes to sleep (removed from memory)
266 | 2. State is persisted to storage
267 | 3. On next request, DO wakes up
268 | 4. `init()` is called to restore state
269 | 5. Normal request processing resumes
270 |
271 | ## Integration with OAuth Provider
272 |
273 | The OAuth provider and McpAgent work together:
274 |
275 | ```
276 | OAuth Provider McpAgent
277 | ------------- ---------
278 | 1. Receives request
279 | 2. Validates OAuth token
280 | 3. Extracts props from token
281 | 4. Generates DO ID from props
282 | 5. Gets/creates DO instance → Constructor called (if new)
283 | 6. Passes props to DO → Props available as this.props
284 | 7. Calls handler.fetch() → init() called (if new/hibernated)
285 | → fetch() called
286 | ← Returns response
287 | 8. Returns response to client
288 | ```
289 |
290 | ## Limitations and Constraints
291 |
292 | 1. **Static handler creation**: `serve()` and `serveSSE()` are static methods, can't access instance data
293 | 2. **Props are immutable**: Set once at DO creation, can't be updated
294 | 3. **URL rewriting**: Original paths lost during transport, must use headers
295 | 4. **One DO per user**: DO ID based on userId, all user requests go to same instance
296 | 5. **No request context in init()**: Can't access request data during initialization
297 | 6. **Constraint immutability**: Constraints are verified and set once at DO creation, remaining immutable throughout the DO's lifetime. Different constraint contexts create separate DO instances via unique sessionIds.
298 |
299 | ## Best Practices
300 |
301 | 1. **Verify constraints early**: Use `verifyConstraintsAccess()` before DO creation
302 | 2. **Use storage sparingly**: Storage operations are expensive
303 | 3. **Cache in memory**: Use instance variables for frequently accessed data
304 | 4. **Prepare for hibernation**: Constraints are preserved in state automatically
305 | 5. **Trust lifecycle guarantees**: McpAgent ensures init() completes before fetch()
306 | 6. **Leverage immutable constraints**: Once set, constraints don't change - design accordingly
307 |
308 | ## Implementation Reference
309 |
310 | The actual implementation can be found in:
311 | - Main class: @packages/mcp-cloudflare/src/server/lib/mcp-agent.ts
312 | - Constraint utilities: @packages/mcp-cloudflare/src/server/lib/constraint-utils.ts
313 | - Type definitions: @packages/mcp-cloudflare/src/server/types.ts
314 |
315 | ## Related Documentation
316 |
317 | - OAuth Architecture: @docs/cloudflare/oauth-architecture.md — How OAuth provider integrates
318 | - MCP Transport (stdio) Implementation: @packages/mcp-server/src/transports/stdio.ts — Core server transport
319 | - Constraint DO Analysis: @docs/cloudflare/constraint-do-analysis.md — Alternative architectures considered
320 |
```