This is page 2 of 11. Use http://codebase.md/getsentry/sentry-mcp?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-mocks/src/fixtures/tags.json:
--------------------------------------------------------------------------------
```json
[
{
"key": "transaction",
"name": "Transaction",
"totalValues": 1080
},
{
"key": "runtime.name",
"name": "Runtime.Name",
"totalValues": 1080
},
{
"key": "level",
"name": "Level",
"totalValues": 1144
},
{
"key": "device",
"name": "Device",
"totalValues": 25
},
{
"key": "os",
"name": "OS",
"totalValues": 1133
},
{
"key": "user",
"name": "User",
"totalValues": 1080
},
{
"key": "runtime",
"name": "Runtime",
"totalValues": 1080
},
{
"key": "release",
"name": "Release",
"totalValues": 1135
},
{
"key": "url",
"name": "URL",
"totalValues": 1080
},
{
"key": "uptime_rule",
"name": "Uptime Rule",
"totalValues": 9
},
{
"key": "server_name",
"name": "Server",
"totalValues": 1080
},
{
"key": "browser",
"name": "Browser",
"totalValues": 56
},
{
"key": "os.name",
"name": "Os.Name",
"totalValues": 1135
},
{
"key": "device.family",
"name": "Device.Family",
"totalValues": 25
},
{
"key": "replayId",
"name": "Replayid",
"totalValues": 55
},
{
"key": "client_os.name",
"name": "Client Os.Name",
"totalValues": 1
},
{
"key": "environment",
"name": "Environment",
"totalValues": 1144
},
{
"key": "service",
"name": "Service",
"totalValues": 1135
},
{
"key": "browser.name",
"name": "Browser.Name",
"totalValues": 56
}
]
```
--------------------------------------------------------------------------------
/packages/mcp-server-mocks/src/fixtures/trace-items-attributes-spans-string.json:
--------------------------------------------------------------------------------
```json
[
{
"key": "span.op",
"name": "Span Operation"
},
{
"key": "span.description",
"name": "Span Description"
},
{
"key": "span.status",
"name": "Span Status"
},
{
"key": "transaction",
"name": "Transaction"
},
{
"key": "transaction.op",
"name": "Transaction Operation"
},
{
"key": "transaction.status",
"name": "Transaction Status"
},
{
"key": "project",
"name": "Project"
},
{
"key": "environment",
"name": "Environment"
},
{
"key": "release",
"name": "Release"
},
{
"key": "user.id",
"name": "User ID"
},
{
"key": "user.email",
"name": "User Email"
},
{
"key": "user.username",
"name": "Username"
},
{
"key": "platform",
"name": "Platform"
},
{
"key": "sdk.name",
"name": "SDK Name"
},
{
"key": "sdk.version",
"name": "SDK Version"
},
{
"key": "http.method",
"name": "HTTP Method"
},
{
"key": "http.url",
"name": "HTTP URL"
},
{
"key": "browser.name",
"name": "Browser Name"
},
{
"key": "os.name",
"name": "OS Name"
},
{
"key": "device",
"name": "Device"
},
{
"key": "geo.country_code",
"name": "Country Code"
},
{
"key": "geo.region",
"name": "Geographic Region"
},
{
"key": "geo.city",
"name": "City"
},
{
"key": "custom.tier",
"name": "Customer Tier"
},
{
"key": "custom.feature_flag",
"name": "Feature Flag"
}
]
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/server.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "server",
"description": "These attributes may be used to describe the server in a connection-based network interaction where there is one side that initiates the connection (the client is the side that initiates the connection). This covers all TCP network interactions since TCP is connection-based and one side initiates the connection (an exception is made for peer-to-peer communication over TCP where the \"user-facing\" surface of the protocol / API doesn't expose a clear notion of client and server). This also covers UDP network interactions where one side initiates the interaction, e.g. QUIC (HTTP/3) and DNS.\n",
"attributes": {
"server.address": {
"description": "Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.",
"type": "string",
"note": "When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent the server address behind any intermediaries, for example proxies, if it's available.\n",
"stability": "stable",
"examples": ["example.com", "10.1.2.80", "/tmp/my.sock"]
},
"server.port": {
"description": "Server port number.",
"type": "number",
"note": "When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries, for example proxies, if it's available.\n",
"stability": "stable",
"examples": ["80", "8080", "443"]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/chat/auth-form.tsx:
--------------------------------------------------------------------------------
```typescript
import { Button } from "../ui/button";
import { AlertCircle } from "lucide-react";
import type { AuthFormProps } from "./types";
export function AuthForm({ authError, onOAuthLogin }: AuthFormProps) {
return (
<div className="sm:p-8 p-4 flex flex-col items-center">
<div className="max-w-md w-full space-y-6">
{/* Chat illustration - hidden on short screens */}
<div className="text-slate-400 hidden [@media(min-height:500px)]:block">
<img
src="/flow-transparent.png"
alt="Flow"
width={1536}
height={1024}
className="w-full mb-6 bg-violet-300 rounded"
/>
</div>
<div className="text-center space-y-2">
<h1 className="text-2xl font-bold">Live MCP Demo</h1>
<p className="text-slate-400">
Connect your Sentry account to test the Model Context Protocol with
real data from your projects.
</p>
</div>
<div className="space-y-4">
{authError && (
<div className="p-3 bg-red-900/20 border border-red-500/30 rounded flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-400" />
<div className="text-red-400 text-sm">{authError}</div>
</div>
)}
<Button
onClick={onOAuthLogin}
variant="default"
className="w-full cursor-pointer"
>
Connect with Sentry
</Button>
</div>
</div>
</div>
);
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/client.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "client",
"description": "These attributes may be used to describe the client in a connection-based network interaction where there is one side that initiates the connection (the client is the side that initiates the connection). This covers all TCP network interactions since TCP is connection-based and one side initiates the connection (an exception is made for peer-to-peer communication over TCP where the \"user-facing\" surface of the protocol / API doesn't expose a clear notion of client and server). This also covers UDP network interactions where one side initiates the interaction, e.g. QUIC (HTTP/3) and DNS.\n",
"attributes": {
"client.address": {
"description": "Client address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.",
"type": "string",
"note": "When observed from the server side, and when communicating through an intermediary, `client.address` SHOULD represent the client address behind any intermediaries, for example proxies, if it's available.\n",
"stability": "stable",
"examples": ["client.example.com", "10.1.2.80", "/tmp/my.sock"]
},
"client.port": {
"description": "Client port number.",
"type": "number",
"note": "When observed from the server side, and when communicating through an intermediary, `client.port` SHOULD represent the client port behind any intermediaries, for example proxies, if it's available.\n",
"stability": "stable",
"examples": ["65123"]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/ui/slash-command-actions.tsx:
--------------------------------------------------------------------------------
```typescript
/**
* Component for rendering clickable slash command buttons
*/
import { Button } from "./button";
import { Terminal } from "lucide-react";
interface SlashCommandActionsProps {
onCommandSelect: (command: string) => void;
}
const SLASH_COMMANDS = [
{ command: "help", description: "Show help message" },
{ command: "tools", description: "List available MCP tools" },
{ command: "resources", description: "List available MCP resources" },
{ command: "prompts", description: "List available MCP prompts" },
{ command: "clear", description: "Clear all chat messages" },
{ command: "logout", description: "Log out of the current session" },
];
export function SlashCommandActions({
onCommandSelect,
}: SlashCommandActionsProps) {
return (
<div className="mt-4 space-y-3">
<h4 className="text-sm font-medium text-slate-300 mb-2">
Try these commands:
</h4>
<div className="space-y-2">
{SLASH_COMMANDS.map((cmd) => (
<div key={cmd.command} className="flex items-center gap-3">
<Button
onClick={() => onCommandSelect(cmd.command)}
size="sm"
variant="outline"
className="flex items-center gap-2 text-xs bg-blue-900/50 border-blue-700/50 hover:bg-blue-800/50 hover:border-blue-600/50 text-blue-300 font-mono"
>
<Terminal className="h-3 w-3" />/{cmd.command}
</Button>
<span className="text-xs text-slate-400">{cmd.description}</span>
</div>
))}
</div>
</div>
);
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
import whoami from "./whoami";
import findOrganizations from "./find-organizations";
import findTeams from "./find-teams";
import findProjects from "./find-projects";
import findReleases from "./find-releases";
import getIssueDetails from "./get-issue-details";
import getTraceDetails from "./get-trace-details";
import getEventAttachment from "./get-event-attachment";
import updateIssue from "./update-issue";
import searchEvents from "./search-events";
import createTeam from "./create-team";
import createProject from "./create-project";
import updateProject from "./update-project";
import createDsn from "./create-dsn";
import findDsns from "./find-dsns";
import analyzeIssueWithSeer from "./analyze-issue-with-seer";
import searchDocs from "./search-docs";
import getDoc from "./get-doc";
import searchIssues from "./search-issues";
// Default export: object mapping tool names to tools
export default {
whoami,
find_organizations: findOrganizations,
find_teams: findTeams,
find_projects: findProjects,
find_releases: findReleases,
get_issue_details: getIssueDetails,
get_trace_details: getTraceDetails,
get_event_attachment: getEventAttachment,
update_issue: updateIssue,
search_events: searchEvents,
create_team: createTeam,
create_project: createProject,
update_project: updateProject,
create_dsn: createDsn,
find_dsns: findDsns,
analyze_issue_with_seer: analyzeIssueWithSeer,
search_docs: searchDocs,
get_doc: getDoc,
search_issues: searchIssues,
} as const;
// Type export
export type ToolName = keyof typeof import("./index").default;
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/os.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "os",
"description": "The operating system (OS) on which the process represented by this resource is running.\n",
"attributes": {
"os.type": {
"description": "The operating system type.\n",
"type": "string",
"stability": "development",
"examples": [
"windows",
"linux",
"darwin",
"freebsd",
"netbsd",
"openbsd",
"dragonflybsd",
"hpux",
"aix",
"solaris",
"z_os",
"zos"
]
},
"os.description": {
"description": "Human readable (not intended to be parsed) OS version information, like e.g. reported by `ver` or `lsb_release -a` commands.\n",
"type": "string",
"stability": "development",
"examples": [
"Microsoft Windows [Version 10.0.18363.778]",
"Ubuntu 18.04.1 LTS"
]
},
"os.name": {
"description": "Human readable operating system name.",
"type": "string",
"stability": "development",
"examples": ["iOS", "Android", "Ubuntu"]
},
"os.version": {
"description": "The version string of the operating system as defined in [Version Attributes](/docs/resource/README.md#version-attributes).\n",
"type": "string",
"stability": "development",
"examples": ["14.2.1", "18.04.1"]
},
"os.build_id": {
"description": "Unique identifier for a particular build or compilation of the operating system.",
"type": "string",
"stability": "development",
"examples": ["TQ3C.230805.001.B2", "20E247", "22621"]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-server-mocks/src/fixtures/project.json:
--------------------------------------------------------------------------------
```json
{
"team": {
"id": "4509106733776896",
"slug": "the-goats",
"name": "the-goats"
},
"teams": [
{
"id": "4509106733776896",
"slug": "the-goats",
"name": "the-goats"
}
],
"id": "4509109104082945",
"name": "cloudflare-mcp",
"slug": "cloudflare-mcp",
"isBookmarked": false,
"isMember": true,
"access": [
"event:admin",
"alerts:read",
"project:write",
"org:integrations",
"alerts:write",
"member:read",
"team:write",
"project:read",
"event:read",
"event:write",
"project:admin",
"org:read",
"team:admin",
"project:releases",
"team:read"
],
"hasAccess": true,
"dateCreated": "2025-04-06T14:13:37.825970Z",
"environments": [],
"eventProcessing": {
"symbolicationDegraded": false
},
"features": [
"discard-groups",
"alert-filters",
"similarity-embeddings",
"similarity-indexing",
"similarity-view"
],
"firstEvent": null,
"firstTransactionEvent": false,
"hasSessions": false,
"hasProfiles": false,
"hasReplays": false,
"hasFeedbacks": false,
"hasNewFeedbacks": false,
"hasMonitors": false,
"hasMinifiedStackTrace": false,
"hasInsightsHttp": false,
"hasInsightsDb": false,
"hasInsightsAssets": false,
"hasInsightsAppStart": false,
"hasInsightsScreenLoad": false,
"hasInsightsVitals": false,
"hasInsightsCaches": false,
"hasInsightsQueues": false,
"hasInsightsLlmMonitoring": false,
"platform": "node",
"platforms": [],
"latestRelease": null,
"hasUserReports": false,
"hasFlags": false,
"latestDeploys": null
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/utils/chat-error-handler.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Simplified chat error handling utilities
* Only handles two concerns: auth errors vs other errors
*/
/**
* Check if an error is authentication-related (401)
*/
export function isAuthError(error: Error): boolean {
const message = error.message.toLowerCase();
// Check for 401 status code or auth-related keywords
return (
message.includes("401") ||
message.includes("auth_expired") ||
message.includes("invalid_token") ||
message.includes("session has expired") ||
message.includes("unauthorized")
);
}
/**
* Extract a user-friendly error message from the error
*/
export function getErrorMessage(error: Error): string {
try {
// Try to parse JSON error response
const jsonMatch = error.message.match(/\{.*\}/);
if (jsonMatch) {
const data = JSON.parse(jsonMatch[0]);
if (data.error) {
return data.error;
}
}
} catch {
// Ignore JSON parse errors
}
// Check for specific error types
if (isAuthError(error)) {
return "Your session has expired. Please log in again.";
}
if (
error.message.includes("429") ||
error.message.toLowerCase().includes("rate_limit")
) {
return "You've sent too many messages. Please wait a moment before trying again.";
}
if (
error.message.includes("403") ||
error.message.toLowerCase().includes("permission")
) {
return "You don't have permission to access this organization.";
}
if (error.message.includes("500")) {
return "Something went wrong on our end. Please try again.";
}
// Default message
return "An error occurred. Please try again.";
}
```
--------------------------------------------------------------------------------
/packages/mcp-server-evals/src/evals/list-teams.eval.ts:
--------------------------------------------------------------------------------
```typescript
import { describeEval } from "vitest-evals";
import { FIXTURES, NoOpTaskRunner, ToolPredictionScorer } from "./utils";
describeEval("list-teams", {
data: async () => {
return [
{
input: `What teams do I have access to in Sentry for '${FIXTURES.organizationSlug}'`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "find_teams",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
regionUrl: "https://us.sentry.io",
},
},
],
},
{
input: `Do I have access to the team '${FIXTURES.teamSlug}' for '${FIXTURES.organizationSlug}'`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "find_teams",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
regionUrl: "https://us.sentry.io",
},
},
],
},
{
input: `Do I have access to the team 'an-imaginary-team' for '${FIXTURES.organizationSlug}'`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "find_teams",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
regionUrl: "https://us.sentry.io",
},
},
],
},
];
},
task: NoOpTaskRunner(),
scorers: [ToolPredictionScorer()],
threshold: 0.6,
timeout: 30000,
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/find-organizations.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import findOrganizations from "./find-organizations.js";
describe("find_organizations", () => {
it("serializes", async () => {
const result = await findOrganizations.handler(
{},
{
constraints: {
organizationSlug: null,
},
accessToken: "access-token",
userId: "1",
},
);
expect(result).toMatchInlineSnapshot(`
"# Organizations
## **sentry-mcp-evals**
**Web URL:** https://sentry.io/sentry-mcp-evals
**Region URL:** https://us.sentry.io
# Using this information
- The organization's name is the identifier for the organization, and is used in many tools for \`organizationSlug\`.
- If a tool supports passing in the \`regionUrl\`, you MUST pass in the correct value shown above for each organization.
- For Sentry's Cloud Service (sentry.io), always use the regionUrl to ensure requests go to the correct region.
"
`);
});
it("handles empty regionUrl parameter", async () => {
const result = await findOrganizations.handler(
{},
{
constraints: {
organizationSlug: null,
},
accessToken: "access-token",
userId: "1",
},
);
expect(result).toContain("Organizations");
});
it("handles undefined regionUrl parameter", async () => {
const result = await findOrganizations.handler(
{},
{
constraints: {
organizationSlug: null,
},
accessToken: "access-token",
userId: "1",
},
);
expect(result).toContain("Organizations");
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/hardware.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "hardware",
"description": "Attributes for hardware.\n",
"attributes": {
"hw.id": {
"description": "An identifier for the hardware component, unique within the monitored host\n",
"type": "string",
"stability": "development",
"examples": ["win32battery_battery_testsysa33_1"]
},
"hw.name": {
"description": "An easily-recognizable name for the hardware component\n",
"type": "string",
"stability": "development",
"examples": ["eth0"]
},
"hw.parent": {
"description": "Unique identifier of the parent component (typically the `hw.id` attribute of the enclosure, or disk controller)\n",
"type": "string",
"stability": "development",
"examples": ["dellStorage_perc_0"]
},
"hw.type": {
"description": "Type of the component\n",
"type": "string",
"note": "Describes the category of the hardware component for which `hw.state` is being reported. For example, `hw.type=temperature` along with `hw.state=degraded` would indicate that the temperature of the hardware component has been reported as `degraded`.\n",
"stability": "development",
"examples": [
"battery",
"cpu",
"disk_controller",
"enclosure",
"fan",
"gpu",
"logical_disk",
"memory",
"network",
"physical_disk",
"power_supply",
"tape_drive",
"temperature",
"voltage"
]
},
"hw.state": {
"description": "The current state of the component\n",
"type": "string",
"stability": "development",
"examples": ["ok", "degraded", "failed"]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/db.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "db",
"description": "Database operations attributes",
"attributes": {
"db.system.name": {
"description": "Identifies the database management system product",
"type": "string",
"examples": [
"postgresql",
"mysql",
"microsoft.sql_server",
"mongodb",
"redis",
"cassandra"
]
},
"db.collection.name": {
"description": "Name of a collection (table, container) within the database",
"type": "string"
},
"db.namespace": {
"description": "Fully qualified database name within server address and port",
"type": "string"
},
"db.operation.name": {
"description": "Name of the operation or command being executed",
"type": "string",
"examples": [
"SELECT",
"INSERT",
"UPDATE",
"DELETE",
"CREATE",
"DROP",
"find",
"insert",
"update",
"delete"
]
},
"db.response.status_code": {
"description": "Status code returned by the database",
"type": "string"
},
"db.operation.batch.size": {
"description": "Number of queries in a batch operation",
"type": "number"
},
"db.query.summary": {
"description": "Low-cardinality summary of a database query",
"type": "string"
},
"db.query.text": {
"description": "The actual database query being executed",
"type": "string"
},
"db.stored_procedure.name": {
"description": "Name of a stored procedure within the database",
"type": "string"
},
"db.query.parameter.<key>": {
"description": "Database query parameter values",
"type": "string"
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/utils/auth-errors.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Shared utilities for detecting and handling authentication errors
*/
export interface AuthErrorInfo {
isAuthError: boolean;
isExpired: boolean;
isForbidden: boolean;
statusCode?: number;
}
/**
* Analyze an error to determine if it's authentication-related
*/
export function analyzeAuthError(error: unknown): AuthErrorInfo {
const result: AuthErrorInfo = {
isAuthError: false,
isExpired: false,
isForbidden: false,
};
if (!(error instanceof Error)) {
return result;
}
const errorMessage = error.message.toLowerCase();
// Check for 401 Unauthorized errors
if (
errorMessage.includes("401") ||
errorMessage.includes("unauthorized") ||
errorMessage.includes("authentication") ||
errorMessage.includes("invalid token") ||
errorMessage.includes("access token")
) {
result.isAuthError = true;
result.isExpired = true;
result.statusCode = 401;
}
// Check for 403 Forbidden errors
if (errorMessage.includes("403") || errorMessage.includes("forbidden")) {
result.isAuthError = true;
result.isForbidden = true;
result.statusCode = 403;
}
return result;
}
/**
* Get appropriate error response based on auth error type
*/
export function getAuthErrorResponse(authInfo: AuthErrorInfo) {
if (authInfo.isExpired) {
return {
error: "Authentication with Sentry has expired. Please log in again.",
name: "AUTH_EXPIRED" as const,
};
}
if (authInfo.isForbidden) {
return {
error: "You don't have permission to access this Sentry organization.",
name: "INSUFFICIENT_PERMISSIONS" as const,
};
}
return {
error: "Authentication error occurred",
name: "SENTRY_AUTH_INVALID" as const,
};
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/ui/slash-command-text.tsx:
--------------------------------------------------------------------------------
```typescript
/**
* Component that renders text with clickable slash commands
*/
import { Fragment } from "react";
interface SlashCommandTextProps {
children: string;
onSlashCommand: (command: string) => void;
}
const SLASH_COMMAND_REGEX = /\/([a-zA-Z]+)/g;
export function SlashCommandText({
children,
onSlashCommand,
}: SlashCommandTextProps) {
const parts = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
// Reset regex state before using
SLASH_COMMAND_REGEX.lastIndex = 0;
// Find all slash commands in the text
match = SLASH_COMMAND_REGEX.exec(children);
while (match !== null) {
const fullMatch = match[0]; // e.g., "/help"
const command = match[1]; // e.g., "help"
const startIndex = match.index;
// Add text before the command
if (startIndex > lastIndex) {
parts.push(
<Fragment key={`text-${lastIndex}`}>
{children.slice(lastIndex, startIndex)}
</Fragment>,
);
}
// Add clickable command
parts.push(
<button
key={`command-${startIndex}`}
onClick={() => onSlashCommand(command)}
className="inline-flex items-center gap-1 px-1 py-0.5 text-xs bg-blue-900/50 border border-blue-700/50 rounded text-blue-300 hover:bg-blue-800/50 hover:border-blue-600/50 transition-colors font-mono cursor-pointer"
type="button"
>
{fullMatch}
</button>,
);
lastIndex = startIndex + fullMatch.length;
// Continue searching
match = SLASH_COMMAND_REGEX.exec(children);
}
// Add remaining text
if (lastIndex < children.length) {
parts.push(
<Fragment key={`text-${lastIndex}`}>
{children.slice(lastIndex)}
</Fragment>,
);
}
return <>{parts}</>;
}
```
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
```yaml
name: Test
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
# pnpm/action-setup@v4
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda
name: Install pnpm
with:
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Run build
run: pnpm build
- name: Run linter
run: pnpm lint
- name: Run tests
run: pnpm test:ci
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: unittests
name: codecov-unittests
fail_ci_if_error: false
- name: Upload results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Publish Test Report
uses: mikepenz/action-junit-report@cf701569b05ccdd861a76b8607a66d76f6fd4857
if: ${{ !cancelled() }}
with:
report_paths: "**/*.junit.xml"
comment: false
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/cassandra.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "cassandra",
"description": "This section defines attributes for Cassandra.\n",
"attributes": {
"cassandra.coordinator.dc": {
"description": "The data center of the coordinating node for a query.\n",
"type": "string",
"stability": "development",
"examples": ["us-west-2"]
},
"cassandra.coordinator.id": {
"description": "The ID of the coordinating node for a query.\n",
"type": "string",
"stability": "development",
"examples": ["be13faa2-8574-4d71-926d-27f16cf8a7af"]
},
"cassandra.consistency.level": {
"description": "The consistency level of the query. Based on consistency values from [CQL](https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/dml/dmlConfigConsistency.html).\n",
"type": "string",
"stability": "development",
"examples": [
"all",
"each_quorum",
"quorum",
"local_quorum",
"one",
"two",
"three",
"local_one",
"any",
"serial",
"local_serial"
]
},
"cassandra.query.idempotent": {
"description": "Whether or not the query is idempotent.\n",
"type": "boolean",
"stability": "development"
},
"cassandra.page.size": {
"description": "The fetch size used for paging, i.e. how many rows will be returned at once.\n",
"type": "number",
"stability": "development",
"examples": ["5000"]
},
"cassandra.speculative_execution.count": {
"description": "The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively.\n",
"type": "number",
"stability": "development",
"examples": ["0", "2"]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/ui/json-schema-params.tsx:
--------------------------------------------------------------------------------
```typescript
type JsonSchema =
| {
properties?: Record<string, { description?: string } | undefined>;
required?: string[];
}
| null
| undefined;
interface JsonSchemaParamsProps {
schema: unknown;
title?: string;
}
/**
* Renders a standardized Parameters box from a JSON Schema-like object.
* - Expects an object with a `properties` map; falls back to a flat key->description map.
* - Returns null when there are no parameters to display.
*/
export default function JsonSchemaParams({
schema,
title = "Parameters",
}: JsonSchemaParamsProps) {
let entries: Array<[string, { description?: string } | undefined]> = [];
const obj = (schema as JsonSchema) ?? undefined;
if (obj && typeof obj === "object" && "properties" in obj) {
const props = obj.properties;
if (props && Object.keys(props).length > 0) {
entries = Object.entries(props);
}
} else if (schema && typeof schema === "object") {
const flat = schema as Record<string, { description?: string } | undefined>;
const keys = Object.keys(flat);
if (keys.length > 0) {
entries = Object.entries(flat);
}
}
if (entries.length === 0) return null;
return (
<section className="rounded-md border border-slate-700/60 bg-black/30 p-3">
<div className="text-xs uppercase tracking-wide text-slate-300/80 mb-1">
{title}
</div>
<dl className="space-y-0">
{entries.map(([key, property]) => (
<div key={key} className="p-2 bg-black/20">
<dt className="text-sm font-medium text-violet-300">{key}</dt>
<dd className="text-sm text-slate-300 mt-0.5">
{property?.description || ""}
</dd>
</div>
))}
</dl>
</section>
);
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/ui/typewriter.tsx:
--------------------------------------------------------------------------------
```typescript
import { useState, useEffect, useRef } from "react";
interface TypewriterProps {
text: string;
speed?: number;
children?: (displayedText: string) => React.ReactNode;
className?: string;
onComplete?: () => void;
}
export function Typewriter({
text,
speed = 30,
children,
className = "",
onComplete,
}: TypewriterProps) {
const [displayedText, setDisplayedText] = useState("");
const [isComplete, setIsComplete] = useState(false);
const indexRef = useRef(0);
const previousTextRef = useRef("");
useEffect(() => {
// Reset if text has changed (new content streaming in)
if (text !== previousTextRef.current) {
const previousText = previousTextRef.current;
// Check if new text is an extension of the previous text
if (text.startsWith(previousText) && text.length > previousText.length) {
// Text got longer, continue from where we left off
indexRef.current = Math.max(displayedText.length, previousText.length);
} else {
// Text completely changed, restart
indexRef.current = 0;
setDisplayedText("");
setIsComplete(false);
}
previousTextRef.current = text;
}
if (indexRef.current >= text.length) {
if (!isComplete) {
setIsComplete(true);
onComplete?.();
}
return;
}
const timer = setTimeout(() => {
setDisplayedText(text.slice(0, indexRef.current + 1));
indexRef.current++;
}, speed);
return () => clearTimeout(timer);
}, [text, speed, displayedText.length, isComplete, onComplete]);
return (
<span className={className}>
{children ? children(displayedText) : displayedText}
{!isComplete && <span className="animate-pulse">|</span>}
</span>
);
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/test-setup.ts:
--------------------------------------------------------------------------------
```typescript
import { config } from "dotenv";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { startMockServer } from "@sentry/mcp-server-mocks";
import type { ServerContext } from "./types.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.resolve(__dirname, "../../../");
// Load environment variables from multiple possible locations
// IMPORTANT: Do NOT use override:true as it would overwrite shell/CI environment variables
// Load local package .env first (for package-specific overrides)
config({ path: path.resolve(__dirname, "../.env") });
// Load root .env second (for shared defaults - won't override local or shell vars)
config({ path: path.join(rootDir, ".env") });
startMockServer({ ignoreOpenAI: true });
/**
* Creates a ServerContext for testing with default values and optional overrides.
*
* @param overrides - Partial ServerContext to override default values
* @returns Complete ServerContext for testing
*
* @example
* ```typescript
* // Default context
* const context = getServerContext();
*
* // With constraint overrides
* const context = getServerContext({
* constraints: { organizationSlug: "my-org" }
* });
*
* // With user override
* const context = getServerContext({
* userId: "custom-user-id"
* });
* ```
*/
export function getServerContext(
overrides: Partial<ServerContext> = {},
): ServerContext {
const defaultContext: ServerContext = {
accessToken: "access-token",
userId: "1",
constraints: {
organizationSlug: null,
projectSlug: null,
},
};
return {
...defaultContext,
...overrides,
// Ensure constraints are properly merged
constraints: {
...defaultContext.constraints,
...overrides.constraints,
},
};
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/whoami.ts:
--------------------------------------------------------------------------------
```typescript
import { defineTool } from "../internal/tool-helpers/define";
import { apiServiceFromContext } from "../internal/tool-helpers/api";
import type { ServerContext } from "../types";
export default defineTool({
name: "whoami",
description: [
"Identify the authenticated user in Sentry.",
"",
"Use this tool when you need to:",
"- Get the user's name and email address.",
].join("\n"),
inputSchema: {},
requiredScopes: [], // No specific scopes required - uses authentication token
annotations: {
readOnlyHint: true,
openWorldHint: true,
},
async handler(params, context: ServerContext) {
// User data endpoints (like /auth/) should never use regionUrl
// as they must always query the main API server, not region-specific servers
const apiService = apiServiceFromContext(context);
// API client will throw ApiClientError/ApiServerError which the MCP server wrapper handles
const user = await apiService.getAuthenticatedUser();
let output = `You are authenticated as ${user.name} (${user.email}).\n\nYour Sentry User ID is ${user.id}.`;
// Add constraints information
const constraints = context.constraints;
if (
constraints.organizationSlug ||
constraints.projectSlug ||
constraints.regionUrl
) {
output += "\n\n## Session Constraints\n\n";
if (constraints.organizationSlug) {
output += `- **Organization**: ${constraints.organizationSlug}\n`;
}
if (constraints.projectSlug) {
output += `- **Project**: ${constraints.projectSlug}\n`;
}
if (constraints.regionUrl) {
output += `- **Region URL**: ${constraints.regionUrl}\n`;
}
output += "\nThese constraints limit the scope of this MCP session.";
}
return output;
},
});
```
--------------------------------------------------------------------------------
/packages/mcp-server-mocks/src/fixtures/trace-meta.json:
--------------------------------------------------------------------------------
```json
{
"logs": 0,
"errors": 0,
"performance_issues": 0,
"span_count": 112.0,
"transaction_child_count_map": [
{
"transaction.event_id": "0daf40dc453a429c8c57e4c215c4e82c",
"count()": 10.0
},
{
"transaction.event_id": "845c0f1eb7544741a9d08cf28e144f42",
"count()": 1.0
},
{
"transaction.event_id": "b49a5b53cba046a2bab9323d8f00de96",
"count()": 7.0
},
{
"transaction.event_id": "c0db3d88529744d393f091b692c024ca",
"count()": 11.0
},
{
"transaction.event_id": "ee6e7f39107847f980e06119bf116d38",
"count()": 7.0
},
{
"transaction.event_id": "efa09ae9091e400e8865fe48aaae80d6",
"count()": 12.0
},
{
"transaction.event_id": "f398bbc635c64e2091d94679dade2957",
"count()": 3.0
},
{
"transaction.event_id": "f531c48c3eaa43be9587dd880b8ec4bb",
"count()": 6.0
},
{
"transaction.event_id": "f779775e1c6a4f09b62d68818a25d7b5",
"count()": 55.0
}
],
"span_count_map": {
"cache.get": 41.0,
"middleware.django": 24.0,
"db": 11.0,
"function": 6.0,
"db.redis": 4.0,
"feature.flagpole.batch_has": 4.0,
"processor": 2.0,
"execute": 2.0,
"fetch_organization_projects": 2.0,
"other": 1.0,
"db.clickhouse": 1.0,
"validator": 1.0,
"serialize": 1.0,
"http.client": 1.0,
"base.paginate.on_results": 1.0,
"ratelimit.__call__": 1.0,
"base.dispatch.request": 1.0,
"discover.endpoint": 1.0,
"serialize.iterate": 1.0,
"base.dispatch.setup": 1.0,
"serialize.get_attrs": 1.0,
"build_plan.storage_query_plan_builder": 1.0,
"serialize.get_attrs.project.options": 1.0,
"check_object_permissions_on_organization": 1.0,
"allocation_policy.get_quota_allowance": 1.0
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/mcp.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "mcp",
"description": "Model Context Protocol attributes for MCP tool calls and sessions",
"attributes": {
"mcp.tool.name": {
"description": "Tool name (e.g., search_issues, search_events)",
"type": "string",
"examples": [
"search_issues",
"search_events",
"get_issue_details",
"update_issue"
]
},
"mcp.session.id": {
"description": "MCP session identifier",
"type": "string"
},
"mcp.transport": {
"description": "MCP transport protocol used",
"type": "string",
"examples": ["stdio", "http", "websocket"]
},
"mcp.request.id": {
"description": "MCP request identifier",
"type": "string"
},
"mcp.response.status": {
"description": "MCP response status",
"type": "string",
"examples": ["success", "error"]
},
"mcp.client.name": {
"description": "Name of the MCP client application",
"type": "string",
"examples": [
"Cursor",
"Claude Code",
"VSCode MCP Extension",
"sentry-mcp-stdio"
]
},
"mcp.client.version": {
"description": "Version of the MCP client application",
"type": "string",
"examples": ["0.16.0", "1.0.0", "2.3.1"]
},
"mcp.server.name": {
"description": "Name of the MCP server application",
"type": "string",
"examples": ["Sentry MCP Server", "GitHub MCP Server", "Slack MCP Server"]
},
"mcp.server.version": {
"description": "Version of the MCP server application",
"type": "string",
"examples": ["0.1.0", "1.2.3", "2.0.0"]
},
"mcp.protocol.version": {
"description": "MCP protocol version being used",
"type": "string",
"examples": ["2024-11-05", "1.0", "2.0"]
}
},
"custom": true
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/cloudevents.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "cloudevents",
"description": "This document defines attributes for CloudEvents.\n",
"attributes": {
"cloudevents.event_id": {
"description": "The [event_id](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#id) uniquely identifies the event.\n",
"type": "string",
"stability": "development",
"examples": ["123e4567-e89b-12d3-a456-426614174000", "0001"]
},
"cloudevents.event_source": {
"description": "The [source](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#source-1) identifies the context in which an event happened.\n",
"type": "string",
"stability": "development",
"examples": [
"https://github.com/cloudevents",
"/cloudevents/spec/pull/123",
"my-service"
]
},
"cloudevents.event_spec_version": {
"description": "The [version of the CloudEvents specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#specversion) which the event uses.\n",
"type": "string",
"stability": "development",
"examples": ["1.0"]
},
"cloudevents.event_type": {
"description": "The [event_type](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#type) contains a value describing the type of event related to the originating occurrence.\n",
"type": "string",
"stability": "development",
"examples": [
"com.github.pull_request.opened",
"com.example.object.deleted.v2"
]
},
"cloudevents.event_subject": {
"description": "The [subject](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#subject) of the event in the context of the event producer (identified by source).\n",
"type": "string",
"stability": "development",
"examples": ["mynewfile.jpg"]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/aspnetcore.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "aspnetcore",
"description": "ASP.NET Core attributes",
"attributes": {
"aspnetcore.rate_limiting.policy": {
"description": "Rate limiting policy name.",
"type": "string",
"stability": "stable",
"examples": ["fixed", "sliding", "token"]
},
"aspnetcore.rate_limiting.result": {
"description": "Rate-limiting result, shows whether the lease was acquired or contains a rejection reason",
"type": "string",
"stability": "stable",
"examples": [
"acquired",
"endpoint_limiter",
"global_limiter",
"request_canceled"
]
},
"aspnetcore.routing.is_fallback": {
"description": "A value that indicates whether the matched route is a fallback route.",
"type": "boolean",
"stability": "stable",
"examples": ["true"]
},
"aspnetcore.diagnostics.handler.type": {
"description": "Full type name of the [`IExceptionHandler`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler) implementation that handled the exception.",
"type": "string",
"stability": "stable",
"examples": ["Contoso.MyHandler"]
},
"aspnetcore.request.is_unhandled": {
"description": "Flag indicating if request was handled by the application pipeline.",
"type": "boolean",
"stability": "stable",
"examples": ["true"]
},
"aspnetcore.routing.match_status": {
"description": "Match result - success or failure",
"type": "string",
"stability": "stable",
"examples": ["success", "failure"]
},
"aspnetcore.diagnostics.exception.result": {
"description": "ASP.NET Core exception middleware handling result",
"type": "string",
"stability": "stable",
"examples": ["handled", "unhandled", "skipped", "aborted"]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/ui/code-snippet.tsx:
--------------------------------------------------------------------------------
```typescript
import { Copy, Check } from "lucide-react";
import { useState, useEffect, useRef } from "react";
import { Button } from "./button";
export default function CodeSnippet({
snippet,
noMargin,
}: {
snippet: string;
noMargin?: boolean;
}) {
const [copied, setCopied] = useState(false);
const timeoutRef = useRef<number | null>(null);
// Clean up timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(snippet);
// Only show success if the operation actually succeeded
setCopied(true);
// Clear any existing timeout before setting a new one
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setCopied(false);
timeoutRef.current = null;
}, 2000);
} catch (error) {
// Handle clipboard write failure silently or you could show an error state
console.error("Failed to copy to clipboard:", error);
}
};
return (
<div className={`relative text-white ${noMargin ? "" : "mb-6"}`}>
<div className="absolute top-2.5 right-2.5 flex items-center justify-end">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-500 cursor-pointer"
onClick={handleCopy}
>
{copied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4 text-slate-500" />
)}
<span className="sr-only">Copy Snippet</span>
</Button>
</div>
<pre
className="p-4 overflow-x-auto text-slate-200 text-sm bg-slate-950"
style={{ margin: 0 }}
>
{snippet}
</pre>
</div>
);
}
```
--------------------------------------------------------------------------------
/packages/mcp-server-evals/src/evals/update-issue.eval.ts:
--------------------------------------------------------------------------------
```typescript
import { describeEval } from "vitest-evals";
import { FIXTURES, NoOpTaskRunner, ToolPredictionScorer } from "./utils";
describeEval("update-issue", {
data: async () => {
return [
// Core use case: Resolve an issue
{
input: `Resolve the issue ${FIXTURES.issueId} in organization ${FIXTURES.organizationSlug}. Output only the new status as a single word.`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "update_issue",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
issueId: FIXTURES.issueId,
status: "resolved",
regionUrl: "https://us.sentry.io",
},
},
],
},
// Core use case: Assign an issue
{
input: `Assign the issue ${FIXTURES.issueId} in organization ${FIXTURES.organizationSlug} to 'john.doe'. Output only the assigned username.`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "update_issue",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
issueId: FIXTURES.issueId,
assignedTo: "john.doe",
regionUrl: "https://us.sentry.io",
},
},
],
},
// Core use case: Using issue URL (alternative input method)
{
input: `Resolve the issue at ${FIXTURES.issueUrl}. Output only the new status as a single word.`,
expectedTools: [
{
name: "update_issue",
arguments: {
issueUrl: FIXTURES.issueUrl,
status: "resolved",
},
},
],
},
];
},
task: NoOpTaskRunner(),
scorers: [ToolPredictionScorer()],
threshold: 0.6,
timeout: 30000,
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/create-team.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import { setTag } from "@sentry/core";
import { defineTool } from "../internal/tool-helpers/define";
import { apiServiceFromContext } from "../internal/tool-helpers/api";
import type { ServerContext } from "../types";
import { ParamOrganizationSlug, ParamRegionUrl } from "../schema";
export default defineTool({
name: "create_team",
requiredScopes: ["team:write"],
description: [
"Create a new team in Sentry.",
"",
"🔍 USE THIS TOOL WHEN USERS WANT TO:",
"- 'Create a new team'",
"- 'Set up a team called [X]'",
"- 'I need a team for my project'",
"",
"Be careful when using this tool!",
"",
"<examples>",
"### Create a new team",
"```",
"create_team(organizationSlug='my-organization', name='the-goats')",
"```",
"</examples>",
"",
"<hints>",
"- If any parameter is ambiguous, you should clarify with the user what they meant.",
"</hints>",
].join("\n"),
inputSchema: {
organizationSlug: ParamOrganizationSlug,
regionUrl: ParamRegionUrl.optional(),
name: z.string().trim().describe("The name of the team to create."),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
openWorldHint: true,
},
async handler(params, context: ServerContext) {
const apiService = apiServiceFromContext(context, {
regionUrl: params.regionUrl,
});
const organizationSlug = params.organizationSlug;
setTag("organization.slug", organizationSlug);
const team = await apiService.createTeam({
organizationSlug,
name: params.name,
});
let output = `# New Team in **${organizationSlug}**\n\n`;
output += `**ID**: ${team.id}\n`;
output += `**Slug**: ${team.slug}\n`;
output += `**Name**: ${team.name}\n`;
output += "# Using this information\n\n";
output += `- You should always inform the user of the Team Slug value.\n`;
return output;
},
});
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/index.html:
--------------------------------------------------------------------------------
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sentry MCP</title>
<link href="./src/client/index.css" rel="stylesheet" />
<link rel="icon" href="/favicon.ico" />
<!-- Primary Meta Tags -->
<meta name="title" content="Sentry MCP" />
<meta name="description" content="A Model Context Protocol implementation for interacting with Sentry." />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://mcp.sentry.dev/" />
<meta property="og:title" content="A model context protocol implementation for interacting with Sentry." />
<meta property="og:description"
content="Simply put, its a way to plug Sentry's API into an LLM, letting you ask questions about your data in context of the LLM itself. This lets you take an agent that you already use, like Cursor, and pull in additional information from Sentry to help with tasks like debugging, code generation, and more." />
<meta property="og:image" content="https://mcp.sentry.dev/flow.jpg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://mcp.sentry.dev/" />
<meta property="twitter:title" content="A model context protocol implementation for interacting with Sentry." />
<meta property="twitter:description"
content="Simply put, its a way to plug Sentry's API into an LLM, letting you ask questions about your data in context of the LLM itself. This lets you take an agent that you already use, like Cursor, and pull in additional information from Sentry to help with tasks like debugging, code generation, and more." />
<meta property="twitter:image" content="https://mcp.sentry.dev/flow.jpg" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/client/main.tsx"></script>
</body>
</html>
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/tool-helpers/validate-region-url.ts:
--------------------------------------------------------------------------------
```typescript
import { UserInputError } from "../../errors";
import { SENTRY_ALLOWED_REGION_DOMAINS } from "../../constants";
/**
* Validates that a regionUrl is valid.
* Prevents SSRF attacks by only allowing the base host itself or domains from an allowlist.
*
* Rules:
* 1. By default, only the base host itself is allowed as regionUrl
* 2. For other domains, they must be in SENTRY_ALLOWED_REGION_DOMAINS
* 3. Protocol MUST be HTTPS for security
*
* @param regionUrl - The region URL to validate
* @param baseHost - The base host to validate against
* @returns The validated host if valid
* @throws {UserInputError} If the regionUrl is invalid or not allowed
*/
export function validateRegionUrl(regionUrl: string, baseHost: string): string {
let parsedUrl: URL;
try {
parsedUrl = new URL(regionUrl);
} catch {
throw new UserInputError(
`Invalid regionUrl provided: ${regionUrl}. Must be a valid URL.`,
);
}
// Validate protocol - MUST be HTTPS for security
if (parsedUrl.protocol !== "https:") {
throw new UserInputError(
`Invalid regionUrl provided: ${regionUrl}. Must use HTTPS protocol for security.`,
);
}
// Validate that the host is not just the protocol name
if (parsedUrl.host === "https" || parsedUrl.host === "http") {
throw new UserInputError(
`Invalid regionUrl provided: ${regionUrl}. The host cannot be just a protocol name.`,
);
}
const regionHost = parsedUrl.host.toLowerCase();
const baseLower = baseHost.toLowerCase();
// First, allow if it's the same as the base host
if (regionHost === baseLower) {
return regionHost;
}
// Otherwise, check against the allowlist
if (!SENTRY_ALLOWED_REGION_DOMAINS.has(regionHost)) {
throw new UserInputError(
`Invalid regionUrl: ${regionUrl}. The domain '${regionHost}' is not allowed. Allowed domains are: ${Array.from(SENTRY_ALLOWED_REGION_DOMAINS).join(", ")}`,
);
}
return regionHost;
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/search-issues/agent.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import type { SentryApiService } from "../../api-client";
import { ConfigurationError } from "../../errors";
import { callEmbeddedAgent } from "../../internal/agents/callEmbeddedAgent";
import { createDatasetFieldsTool } from "../../internal/agents/tools/dataset-fields";
import { createWhoamiTool } from "../../internal/agents/tools/whoami";
import { systemPrompt } from "./config";
const outputSchema = z.object({
query: z
.string()
.default("")
.nullish()
.describe("The Sentry issue search query"),
sort: z
.enum(["date", "freq", "new", "user"])
.default("date")
.nullish()
.describe("How to sort the results"),
explanation: z
.string()
.describe("Brief explanation of how you translated this query."),
});
export interface SearchIssuesAgentOptions {
query: string;
organizationSlug: string;
apiService: SentryApiService;
projectId?: string;
}
/**
* Search issues agent - single entry point for translating natural language queries to Sentry issue search syntax
* This returns both the translated query result AND the tool calls made by the agent
*/
export async function searchIssuesAgent(
options: SearchIssuesAgentOptions,
): Promise<{
result: z.infer<typeof outputSchema>;
toolCalls: any[];
}> {
if (!process.env.OPENAI_API_KEY) {
throw new ConfigurationError(
"OPENAI_API_KEY environment variable is required for semantic search",
);
}
// Create tools pre-bound with the provided API service and organization
return await callEmbeddedAgent({
system: systemPrompt,
prompt: options.query,
tools: {
issueFields: createDatasetFieldsTool({
apiService: options.apiService,
organizationSlug: options.organizationSlug,
dataset: "search_issues",
projectId: options.projectId,
}),
whoami: createWhoamiTool({ apiService: options.apiService }),
},
schema: outputSchema,
});
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/hooks/use-mcp-metadata.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Custom hook to fetch and manage MCP metadata
*
* Provides immediate access to prompts and tools without waiting for chat stream
*/
import { useState, useEffect, useCallback } from "react";
export interface McpMetadata {
type: "mcp-metadata";
prompts: Array<{
name: string;
description: string;
parameters: Record<
string,
{
type: string;
required: boolean;
description?: string;
}
>;
}>;
tools: string[];
resources?: Array<{
name: string;
description: string;
}>;
timestamp: string;
}
interface UseMcpMetadataResult {
metadata: McpMetadata | null;
isLoading: boolean;
error: string | null;
refetch: () => Promise<void>;
}
export function useMcpMetadata(enabled = true): UseMcpMetadataResult {
const [metadata, setMetadata] = useState<McpMetadata | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchMetadata = useCallback(async () => {
if (!enabled) {
return;
}
setIsLoading(true);
setError(null);
try {
const response = await fetch("/api/metadata", {
credentials: "include", // Include cookies
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
const data = await response.json();
setMetadata(data);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Failed to fetch metadata";
setError(errorMessage);
console.error("Failed to fetch MCP metadata:", err);
} finally {
setIsLoading(false);
}
}, [enabled]);
// Fetch metadata when auth token changes or component mounts
useEffect(() => {
fetchMetadata();
}, [fetchMetadata]);
return {
metadata,
isLoading,
error,
refetch: fetchMetadata,
};
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/whoami.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import whoami from "./whoami.js";
import {
createTestContext,
createTestContextWithConstraints,
} from "../test-utils/context.js";
describe("whoami", () => {
it("serializes without constraints", async () => {
const result = await whoami.handler(
{},
createTestContext({
constraints: {},
accessToken: "access-token",
userId: "123456",
}),
);
expect(result).toMatchInlineSnapshot(
`
"You are authenticated as Test User ([email protected]).
Your Sentry User ID is 123456."
`,
);
});
it("serializes with constraints", async () => {
const result = await whoami.handler(
{},
createTestContextWithConstraints(
{
organizationSlug: "sentry",
projectSlug: "mcp-server",
regionUrl: "https://us.sentry.io",
},
{
accessToken: "access-token",
userId: "123456",
},
),
);
expect(result).toMatchInlineSnapshot(
`
"You are authenticated as Test User ([email protected]).
Your Sentry User ID is 123456.
## Session Constraints
- **Organization**: sentry
- **Project**: mcp-server
- **Region URL**: https://us.sentry.io
These constraints limit the scope of this MCP session."
`,
);
});
it("serializes with partial constraints", async () => {
const result = await whoami.handler(
{},
createTestContextWithConstraints(
{
organizationSlug: "sentry",
},
{
accessToken: "access-token",
userId: "123456",
},
),
);
expect(result).toMatchInlineSnapshot(
`
"You are authenticated as Test User ([email protected]).
Your Sentry User ID is 123456.
## Session Constraints
- **Organization**: sentry
These constraints limit the scope of this MCP session."
`,
);
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/ui/sliding-panel.tsx:
--------------------------------------------------------------------------------
```typescript
/**
* Reusable sliding panel component
* Handles responsive slide-out behavior
*/
import type { ReactNode } from "react";
import { useScrollLock } from "../../hooks/use-scroll-lock";
interface SlidingPanelProps {
isOpen: boolean;
onClose?: () => void;
children: ReactNode;
className?: string;
}
export function SlidingPanel({
isOpen,
onClose,
children,
className = "",
}: SlidingPanelProps) {
// Lock body scroll when panel is open on mobile
useScrollLock(isOpen);
return (
<>
{/* Mobile: Slide from right */}
<div
className={`md:hidden fixed inset-0 bg-transparent max-w-none max-h-none w-full h-full m-0 p-0 z-40 ${
isOpen ? "" : "pointer-events-none"
}`}
>
{/* Backdrop */}
<div
className={`fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity ${
isOpen
? "opacity-100 pointer-events-auto duration-200"
: "opacity-0 pointer-events-none duration-300"
}`}
onClick={isOpen ? onClose : undefined}
onKeyDown={
isOpen ? (e) => e.key === "Escape" && onClose?.() : undefined
}
role={isOpen ? "button" : undefined}
tabIndex={isOpen ? 0 : -1}
aria-label={isOpen ? "Close panel" : undefined}
/>
{/* Panel */}
<div
className={`fixed inset-y-0 right-0 w-full max-w-2xl bg-slate-950 border-l border-slate-800 z-50 shadow-2xl flex flex-col transition-transform duration-500 ease-in-out ${
isOpen ? "translate-x-0" : "translate-x-full"
} ${className}`}
>
{children}
</div>
</div>
{/* Desktop: Fixed right half */}
<div
className={`${
isOpen ? "hidden md:flex" : "hidden"
} fixed top-0 right-0 h-screen w-1/2 bg-slate-950 flex-col border-l border-slate-800 transition-opacity duration-300 ${className}`}
>
{children}
</div>
</>
);
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/error.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "error",
"description": "This document defines the shared attributes used to report an error.\n",
"attributes": {
"error.type": {
"description": "Describes a class of error the operation ended with.\n",
"type": "string",
"note": "The `error.type` SHOULD be predictable, and SHOULD have low cardinality.\n\nWhen `error.type` is set to a type (e.g., an exception type), its\ncanonical class name identifying the type within the artifact SHOULD be used.\n\nInstrumentations SHOULD document the list of errors they report.\n\nThe cardinality of `error.type` within one instrumentation library SHOULD be low.\nTelemetry consumers that aggregate data from multiple instrumentation libraries and applications\nshould be prepared for `error.type` to have high cardinality at query time when no\nadditional filters are applied.\n\nIf the operation has completed successfully, instrumentations SHOULD NOT set `error.type`.\n\nIf a specific domain defines its own set of error identifiers (such as HTTP or gRPC status codes),\nit's RECOMMENDED to:\n\n- Use a domain-specific attribute\n- Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not.\n",
"stability": "stable",
"examples": ["_OTHER"]
},
"error.message": {
"description": "A message providing more detail about an error in human-readable form.",
"type": "string",
"note": "`error.message` should provide additional context and detail about an error.\nIt is NOT RECOMMENDED to duplicate the value of `error.type` in `error.message`.\nIt is also NOT RECOMMENDED to duplicate the value of `exception.message` in `error.message`.\n\n`error.message` is NOT RECOMMENDED for metrics or spans due to its unbounded cardinality and overlap with span status.\n",
"stability": "development",
"examples": [
"Unexpected input type: string",
"The user has exceeded their storage quota"
]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/hooks/use-streaming-simulation.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Hook for simulating streaming animation for local messages (like slash commands)
* This provides the same UX as AI-generated responses for locally generated content
*/
import { useState, useCallback, useRef, useEffect } from "react";
interface StreamingSimulationState {
isStreaming: boolean;
streamingMessageId: string | null;
}
export function useStreamingSimulation() {
const [state, setState] = useState<StreamingSimulationState>({
isStreaming: false,
streamingMessageId: null,
});
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Start streaming simulation for a specific message
const startStreaming = useCallback((messageId: string, duration = 1000) => {
setState({
isStreaming: true,
streamingMessageId: messageId,
});
// Clear any existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Stop streaming after the specified duration
timeoutRef.current = setTimeout(() => {
setState({
isStreaming: false,
streamingMessageId: null,
});
}, duration);
}, []);
// Stop streaming simulation immediately
const stopStreaming = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
setState({
isStreaming: false,
streamingMessageId: null,
});
}, []);
// Check if a specific message is currently streaming
const isMessageStreaming = useCallback(
(messageId: string) => {
return state.isStreaming && state.streamingMessageId === messageId;
},
[state.isStreaming, state.streamingMessageId],
);
// Cleanup on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return {
isStreaming: state.isStreaming,
streamingMessageId: state.streamingMessageId,
startStreaming,
stopStreaming,
isMessageStreaming,
};
}
```
--------------------------------------------------------------------------------
/packages/mcp-server-mocks/src/fixtures/trace-items-attributes.json:
--------------------------------------------------------------------------------
```json
[
{
"key": "span.op",
"name": "Span Operation"
},
{
"key": "span.description",
"name": "Span Description"
},
{
"key": "span.status",
"name": "Span Status"
},
{
"key": "span.duration",
"name": "Span Duration"
},
{
"key": "transaction",
"name": "Transaction"
},
{
"key": "transaction.op",
"name": "Transaction Operation"
},
{
"key": "transaction.duration",
"name": "Transaction Duration"
},
{
"key": "transaction.status",
"name": "Transaction Status"
},
{
"key": "project",
"name": "Project"
},
{
"key": "environment",
"name": "Environment"
},
{
"key": "release",
"name": "Release"
},
{
"key": "user.id",
"name": "User ID"
},
{
"key": "user.email",
"name": "User Email"
},
{
"key": "user.username",
"name": "Username"
},
{
"key": "error.type",
"name": "Error Type"
},
{
"key": "error.value",
"name": "Error Value"
},
{
"key": "error.handled",
"name": "Error Handled"
},
{
"key": "message",
"name": "Message"
},
{
"key": "level",
"name": "Level"
},
{
"key": "platform",
"name": "Platform"
},
{
"key": "sdk.name",
"name": "SDK Name"
},
{
"key": "sdk.version",
"name": "SDK Version"
},
{
"key": "http.method",
"name": "HTTP Method"
},
{
"key": "http.status_code",
"name": "HTTP Status Code"
},
{
"key": "http.url",
"name": "HTTP URL"
},
{
"key": "browser.name",
"name": "Browser Name"
},
{
"key": "os.name",
"name": "OS Name"
},
{
"key": "device",
"name": "Device"
},
{
"key": "geo.country_code",
"name": "Country Code"
},
{
"key": "geo.region",
"name": "Geographic Region"
},
{
"key": "geo.city",
"name": "City"
},
{
"key": "custom.tier",
"name": "Customer Tier"
},
{
"key": "custom.feature_flag",
"name": "Feature Flag"
}
]
```
--------------------------------------------------------------------------------
/packages/mcp-test-client/src/logger.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Simple terminal output logger for the MCP client.
*
* In an ideal world, this would use a state manager to track the last active logger,
* and it'd accept streams to the log functions. It'd then handle automatically
* terminating a previous block, inserting a new block, and restarting the previous
* block when streams receive new data. This is just a simplified version as this is
* not a big concern in this project.
*/
import chalk from "chalk";
let responseStarted = false;
export const logError = (msg: string, detail?: any) =>
process.stdout.write(
`\n${chalk.red("●")} ${msg}${detail ? `\n ⎿ ${chalk.gray(detail instanceof Error ? detail.message : detail)}` : ""}\n`,
);
export const logSuccess = (msg: string, detail?: string) =>
process.stdout.write(
`\n${chalk.green("●")} ${msg}${detail ? `\n ⎿ ${chalk.gray(detail)}` : ""}\n`,
);
export const logInfo = (msg: string, detail?: string) =>
process.stdout.write(
`\n${chalk.blue("●")} ${msg}${detail ? `\n ⎿ ${chalk.gray(detail)}` : ""}\n`,
);
export const logUser = (msg: string) =>
process.stdout.write(`\n${chalk.gray(">")} ${chalk.gray(msg)}\n`);
export const logTool = (name: string, args?: any) => {
const params =
args && Object.keys(args).length > 0
? `(${Object.entries(args)
.map(
([k, v]) =>
`${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`,
)
.join(", ")})`
: "()";
process.stdout.write(`\n${chalk.green("●")} ${name}${params}\n`);
};
export const logToolResult = (msg: string) =>
process.stdout.write(` ⎿ ${chalk.white(msg)}\n`);
export const logStreamStart = () => {
if (!responseStarted) {
process.stdout.write(`\n${chalk.white("●")} `);
responseStarted = true;
}
};
export const logStreamWrite = (chunk: string) =>
process.stdout.write(chunk.replace(/\n/g, "\n "));
export const logStreamEnd = () => {
if (responseStarted) {
process.stdout.write("\n");
responseStarted = false;
}
};
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/update-project.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import updateProject from "./update-project.js";
describe("update_project", () => {
it("updates name and platform", async () => {
const result = await updateProject.handler(
{
organizationSlug: "sentry-mcp-evals",
projectSlug: "cloudflare-mcp",
name: "New Project Name",
slug: undefined,
platform: "python",
teamSlug: undefined,
regionUrl: undefined,
},
{
constraints: {
organizationSlug: null,
},
accessToken: "access-token",
userId: "1",
},
);
expect(result).toMatchInlineSnapshot(`
"# Updated Project in **sentry-mcp-evals**
**ID**: 4509109104082945
**Slug**: cloudflare-mcp
**Name**: New Project Name
**Platform**: python
## Updates Applied
- Updated name to "New Project Name"
- Updated platform to "python"
# Using this information
- The project is now accessible at slug: \`cloudflare-mcp\`
"
`);
});
it("assigns project to new team", async () => {
const result = await updateProject.handler(
{
organizationSlug: "sentry-mcp-evals",
projectSlug: "cloudflare-mcp",
name: undefined,
slug: undefined,
platform: undefined,
teamSlug: "backend-team",
regionUrl: undefined,
},
{
constraints: {
organizationSlug: null,
},
accessToken: "access-token",
userId: "1",
},
);
expect(result).toMatchInlineSnapshot(`
"# Updated Project in **sentry-mcp-evals**
**ID**: 4509106749636608
**Slug**: cloudflare-mcp
**Name**: cloudflare-mcp
**Platform**: node
## Updates Applied
- Updated team assignment to "backend-team"
# Using this information
- The project is now accessible at slug: \`cloudflare-mcp\`
- The project is now assigned to the \`backend-team\` team
"
`);
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/hooks/use-scroll-lock.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Hook to lock body scroll when a component is active
* Handles edge cases like iOS Safari and nested locks
*/
import { useEffect, useRef } from "react";
// Track active locks to handle nested components
let activeLocks = 0;
let originalStyles: {
overflow?: string;
position?: string;
top?: string;
width?: string;
} = {};
export function useScrollLock(enabled = true) {
const scrollPositionRef = useRef(0);
useEffect(() => {
if (!enabled) return;
// Save scroll position and lock scroll
const lockScroll = () => {
// First lock - save original styles
if (activeLocks === 0) {
scrollPositionRef.current = window.scrollY;
originalStyles = {
overflow: document.body.style.overflow,
position: document.body.style.position,
top: document.body.style.top,
width: document.body.style.width,
};
// Apply scroll lock styles
document.body.style.overflow = "hidden";
// iOS Safari fix - prevent rubber band scrolling
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
document.body.style.position = "fixed";
document.body.style.top = `-${scrollPositionRef.current}px`;
document.body.style.width = "100%";
}
}
activeLocks++;
};
// Restore scroll position and unlock
const unlockScroll = () => {
activeLocks--;
// Last lock removed - restore original styles
if (activeLocks === 0) {
document.body.style.overflow = originalStyles.overflow || "";
document.body.style.position = originalStyles.position || "";
document.body.style.top = originalStyles.top || "";
document.body.style.width = originalStyles.width || "";
// Restore scroll position for iOS
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
window.scrollTo(0, scrollPositionRef.current);
}
originalStyles = {};
}
};
lockScroll();
// Cleanup
return () => {
unlockScroll();
};
}, [enabled]);
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/telemetry.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "telemetry",
"description": "This document defines attributes for telemetry SDK.\n",
"attributes": {
"telemetry.sdk.name": {
"description": "The name of the telemetry SDK as defined above.\n",
"type": "string",
"note": "The OpenTelemetry SDK MUST set the `telemetry.sdk.name` attribute to `opentelemetry`.\nIf another SDK, like a fork or a vendor-provided implementation, is used, this SDK MUST set the\n`telemetry.sdk.name` attribute to the fully-qualified class or module name of this SDK's main entry point\nor another suitable identifier depending on the language.\nThe identifier `opentelemetry` is reserved and MUST NOT be used in this case.\nAll custom identifiers SHOULD be stable across different versions of an implementation.\n",
"stability": "stable",
"examples": ["opentelemetry"]
},
"telemetry.sdk.language": {
"description": "The language of the telemetry SDK.\n",
"type": "string",
"stability": "stable",
"examples": [
"cpp",
"dotnet",
"erlang",
"go",
"java",
"nodejs",
"php",
"python",
"ruby",
"rust",
"swift",
"webjs"
]
},
"telemetry.sdk.version": {
"description": "The version string of the telemetry SDK.\n",
"type": "string",
"stability": "stable",
"examples": ["1.2.3"]
},
"telemetry.distro.name": {
"description": "The name of the auto instrumentation agent or distribution, if used.\n",
"type": "string",
"note": "Official auto instrumentation agents and distributions SHOULD set the `telemetry.distro.name` attribute to\na string starting with `opentelemetry-`, e.g. `opentelemetry-java-instrumentation`.\n",
"stability": "development",
"examples": ["parts-unlimited-java"]
},
"telemetry.distro.version": {
"description": "The version string of the auto instrumentation agent or distribution, if used.\n",
"type": "string",
"stability": "development",
"examples": ["1.2.3"]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/permissions.parseScopes.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import { parseScopes, expandScopes, type Scope } from "./permissions";
describe("parseScopes", () => {
it("parses comma-separated string with trimming and de-dup", () => {
const { valid, invalid } = parseScopes(
"event:write, foo, org:admin, , event:write",
);
const v = new Set<Scope>(valid);
expect(v.has("event:write")).toBe(true);
expect(v.has("org:admin")).toBe(true);
expect(invalid).toEqual(["foo"]);
});
it("parses arrays and filters non-strings", () => {
const { valid, invalid } = parseScopes([
"member:read",
"x",
123 as unknown,
" team:write ",
"",
]);
const v = new Set<Scope>(valid);
expect(v.has("member:read")).toBe(true);
expect(v.has("team:write")).toBe(true);
expect(invalid).toEqual(["x"]);
});
it("handles empty or undefined inputs", () => {
expect(parseScopes("")).toEqual({ valid: new Set<Scope>(), invalid: [] });
expect(parseScopes(undefined)).toEqual({
valid: new Set<Scope>(),
invalid: [],
});
expect(parseScopes([])).toEqual({ valid: new Set<Scope>(), invalid: [] });
});
});
// Consolidated strict-like parseScopes cases
describe("parseScopes (strict-like cases)", () => {
it("returns invalid tokens for unknown scopes", () => {
const { valid, invalid } = parseScopes("foo,bar,org:admin");
expect(invalid).toEqual(["foo", "bar"]);
expect([...valid]).toContain("org:admin");
});
it("returns only valid set when all are valid", () => {
const { valid, invalid } = parseScopes("event:admin,org:read");
expect(invalid).toEqual([]);
const out = new Set<Scope>(valid);
expect(out.has("event:admin")).toBe(true);
expect(out.has("org:read")).toBe(true);
});
});
// Related behavior validation for expandScopes
describe("expandScopes", () => {
it("includes implied lower scopes", () => {
const expanded = expandScopes(new Set<Scope>(["event:write"]));
expect(expanded.has("event:read")).toBe(true);
expect(expanded.has("event:write")).toBe(true);
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/transports/stdio.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Standard I/O Transport for MCP Server.
*
* Provides stdio-based communication for the Sentry MCP server, typically used
* when the server runs as a subprocess communicating via stdin/stdout pipes.
*
* @example Basic Usage
* ```typescript
* import { Server } from "@modelcontextprotocol/sdk/server/index.js";
* import { startStdio } from "./transports/stdio.js";
*
* const server = new Server();
* const context = {
* accessToken: process.env.SENTRY_TOKEN,
* host: "sentry.io"
* };
*
* await startStdio(server, context);
* ```
*/
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { configureServer } from "../server";
import type { ServerContext } from "../types";
import * as Sentry from "@sentry/node";
import { LIB_VERSION } from "../version";
/**
* Starts the MCP server with stdio transport and telemetry.
*
* Configures the server with all tools, prompts, and resources, then connects
* using stdio transport for process-based communication. All operations are
* wrapped in Sentry tracing for observability.
*
* @param server - MCP server instance to configure and start
* @param context - Server context with authentication and configuration
*
* @example CLI Integration
* ```typescript
* // In a CLI tool or IDE extension:
* const server = new McpServer();
* await startStdio(server, {
* accessToken: userToken,
* host: userHost,
* userId: "user-123",
* clientId: "cursor-ide"
* });
* ```
*/
export async function startStdio(server: McpServer, context: ServerContext) {
await Sentry.startNewTrace(async () => {
return await Sentry.startSpan(
{
name: "mcp.server/stdio",
attributes: {
"mcp.transport": "stdio",
"network.transport": "pipe",
"service.version": LIB_VERSION,
},
},
async () => {
const transport = new StdioServerTransport();
await configureServer({ server, context });
await server.connect(transport);
},
);
});
}
```
--------------------------------------------------------------------------------
/.github/workflows/eval.yml:
--------------------------------------------------------------------------------
```yaml
name: Eval
on:
workflow_dispatch:
push:
branches: [main]
paths:
- "packages/mcp-server/src/tools*"
- "packages/mcp-server-evals/**"
- "packages/mcp-server-mocks/**"
- ".github/workflows/eval.yml"
pull_request:
paths:
- "packages/mcp-server/src/tools*"
- "packages/mcp-server-evals/**"
- "packages/mcp-server-mocks/**"
- ".github/workflows/eval.yml"
jobs:
eval:
environment: Actions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
# pnpm/action-setup@v4
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda
name: Install pnpm
with:
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Run build
run: pnpm build
- name: Run evals
run: pnpm eval:ci evals
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: evals
name: codecov-evals
fail_ci_if_error: false
- name: Upload results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Publish Test Report
uses: mikepenz/action-junit-report@cf701569b05ccdd861a76b8607a66d76f6fd4857
if: ${{ !cancelled() }}
with:
report_paths: "**/*.junit.xml"
comment: false
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/ui/button.tsx:
--------------------------------------------------------------------------------
```typescript
import type * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
{
variants: {
variant: {
default: "bg-violet-300 text-black shadow-xs hover:bg-violet-300/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20",
outline:
"bg-slate-800/50 border border-slate-600/50 shadow-xs hover:bg-slate-700/50 hover:text-white ",
secondary:
"bg-background shadow-xs hover:bg-violet-300 hover:text-black",
ghost: "hover:text-white hover:bg-slate-700/50 ",
link: "text-primary hover:underline hover:text-violet-300 cursor-pointer",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
xs: "h-7 gap-1.5 px-2 has-[>svg]:px-1.5",
lg: "h-10 px-6 has-[>svg]:px-4",
icon: "size-9",
},
active: {
true: "text-violet-300 underline",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
active = false,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
active?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className, active }))}
{...props}
/>
);
}
export { Button, buttonVariants };
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/find-releases.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import findReleases from "./find-releases.js";
describe("find_releases", () => {
it("works without project", async () => {
const result = await findReleases.handler(
{
organizationSlug: "sentry-mcp-evals",
projectSlug: undefined,
regionUrl: undefined,
query: undefined,
},
{
constraints: {
organizationSlug: null,
},
accessToken: "access-token",
userId: "1",
},
);
expect(result).toMatchInlineSnapshot(`
"# Releases in **sentry-mcp-evals**
## 8ce89484-0fec-4913-a2cd-e8e2d41dee36
**Created**: 2025-04-13T19:54:21.764Z
**First Event**: 2025-04-13T19:54:21.000Z
**Last Event**: 2025-04-13T20:28:23.000Z
**New Issues**: 0
**Projects**: cloudflare-mcp
# Using this information
- You can reference the Release version in commit messages or documentation.
- You can search for issues in a specific release using the \`find_errors()\` tool with the query \`release:8ce89484-0fec-4913-a2cd-e8e2d41dee36\`.
"
`);
});
it("works with project", async () => {
const result = await findReleases.handler(
{
organizationSlug: "sentry-mcp-evals",
projectSlug: "cloudflare-mcp",
regionUrl: undefined,
query: undefined,
},
{
constraints: {
organizationSlug: null,
},
accessToken: "access-token",
userId: "1",
},
);
expect(result).toMatchInlineSnapshot(`
"# Releases in **sentry-mcp-evals/cloudflare-mcp**
## 8ce89484-0fec-4913-a2cd-e8e2d41dee36
**Created**: 2025-04-13T19:54:21.764Z
**First Event**: 2025-04-13T19:54:21.000Z
**Last Event**: 2025-04-13T20:28:23.000Z
**New Issues**: 0
**Projects**: cloudflare-mcp
# Using this information
- You can reference the Release version in commit messages or documentation.
- You can search for issues in a specific release using the \`find_errors()\` tool with the query \`release:8ce89484-0fec-4913-a2cd-e8e2d41dee36\`.
"
`);
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/browser.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "browser",
"description": "The web browser attributes\n",
"attributes": {
"browser.brands": {
"description": "Array of brand name and version separated by a space",
"type": "string",
"note": "This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.brands`).\n",
"stability": "development",
"examples": ["[\" Not A;Brand 99\",\"Chromium 99\",\"Chrome 99\"]"]
},
"browser.platform": {
"description": "The platform on which the browser is running",
"type": "string",
"note": "This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.platform`). If unavailable, the legacy `navigator.platform` API SHOULD NOT be used instead and this attribute SHOULD be left unset in order for the values to be consistent.\nThe list of possible values is defined in the [W3C User-Agent Client Hints specification](https://wicg.github.io/ua-client-hints/#sec-ch-ua-platform). Note that some (but not all) of these values can overlap with values in the [`os.type` and `os.name` attributes](./os.md). However, for consistency, the values in the `browser.platform` attribute should capture the exact value that the user agent provides.\n",
"stability": "development",
"examples": ["Windows", "macOS", "Android"]
},
"browser.mobile": {
"description": "A boolean that is true if the browser is running on a mobile device",
"type": "boolean",
"note": "This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.mobile`). If unavailable, this attribute SHOULD be left unset.\n",
"stability": "development"
},
"browser.language": {
"description": "Preferred language of the user using the browser",
"type": "string",
"note": "This value is intended to be taken from the Navigator API `navigator.language`.\n",
"stability": "development",
"examples": ["en", "en-US", "fr", "fr-FR"]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/errors.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, expect, it } from "vitest";
import { UserInputError, ConfigurationError } from "./errors";
describe("UserInputError", () => {
it("should create a UserInputError with the correct message and name", () => {
const message = "Invalid input provided";
const error = new UserInputError(message);
expect(error.message).toBe(message);
expect(error.name).toBe("UserInputError");
expect(error instanceof Error).toBe(true);
expect(error instanceof UserInputError).toBe(true);
});
it("should be distinguishable from regular Error", () => {
const userInputError = new UserInputError("User input error");
const regularError = new Error("Regular error");
expect(userInputError instanceof UserInputError).toBe(true);
expect(regularError instanceof UserInputError).toBe(false);
});
it("should support error cause", () => {
const cause = new Error("Original error");
const error = new UserInputError("User input error", { cause });
expect(error.cause).toBe(cause);
});
});
describe("ConfigurationError", () => {
it("should create a ConfigurationError with the correct message and name", () => {
const message = "Invalid configuration";
const error = new ConfigurationError(message);
expect(error.message).toBe(message);
expect(error.name).toBe("ConfigurationError");
expect(error instanceof Error).toBe(true);
expect(error instanceof ConfigurationError).toBe(true);
});
it("should be distinguishable from regular Error and UserInputError", () => {
const configError = new ConfigurationError("Config error");
const userInputError = new UserInputError("User input error");
const regularError = new Error("Regular error");
expect(configError instanceof ConfigurationError).toBe(true);
expect(userInputError instanceof ConfigurationError).toBe(false);
expect(regularError instanceof ConfigurationError).toBe(false);
});
it("should support error cause", () => {
const cause = new Error("DNS resolution failed");
const error = new ConfigurationError("Unable to connect to server", {
cause,
});
expect(error.cause).toBe(cause);
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
```typescript
import type * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "lucide-react";
import { cn } from "../../lib/utils";
function Accordion({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return (
<AccordionPrimitive.Root
data-slot="accordion"
className="space-y-1"
{...props}
/>
);
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("px-4 bg-slate-950 rounded", className)}
{...props}
/>
);
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex m-0">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-center justify-between gap-4 py-4 text-left font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180 text-lg text-white hover:text-violet-300 cursor-pointer ",
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm border-t border-t-slate-800"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/fetch-utils.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Fetch with timeout using AbortController
* @param url - The URL to fetch
* @param options - Standard fetch options
* @param timeoutMs - Timeout in milliseconds (default: 30000)
* @returns Promise<Response>
* @throws Error if request times out
*/
export async function fetchWithTimeout(
url: string | URL,
options: RequestInit = {},
timeoutMs = 30000,
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
return response;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new Error(`Request timeout after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
/**
* Retry a function with exponential backoff
* @param fn - The async function to retry
* @param options - Retry options
* @param options.maxRetries - Maximum number of retries (default: 3)
* @param options.initialDelay - Initial delay in milliseconds (default: 1000)
* @param options.shouldRetry - Predicate to determine if error should be retried (default: always retry)
* @returns Promise with the function result
* @throws The last error if all retries are exhausted
*/
export async function retryWithBackoff<T>(
fn: () => Promise<T>,
{
maxRetries = 3,
initialDelay = 1000,
shouldRetry = (error: unknown) => true,
}: {
maxRetries?: number;
initialDelay?: number;
shouldRetry?: (error: unknown) => boolean;
} = {},
): Promise<T> {
let lastError: unknown;
let delay = initialDelay;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Don't retry if we've exhausted attempts or if the error is non-retryable
if (attempt === maxRetries || !shouldRetry(error)) {
throw error;
}
// Wait before retrying with exponential backoff
await new Promise((resolve) => setTimeout(resolve, delay));
delay = Math.min(delay * 2, 30000); // Cap at 30 seconds
}
}
throw lastError;
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/chat/tool-invocation.tsx:
--------------------------------------------------------------------------------
```typescript
import { useState } from "react";
import { Bolt, ChevronDown, ChevronRight } from "lucide-react";
import type { ToolMessage, ToolInvocationProps } from "./types";
import { isTextMessage } from "./types";
function getTokenCount(content: ToolMessage[]): number {
return content.reduce((acc, message) => {
if (isTextMessage(message)) {
return acc + message.text.length;
}
return acc;
}, 0);
}
export function ToolInvocation({
tool,
messageId,
index,
}: ToolInvocationProps) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="border border-slate-900 rounded overflow-hidden">
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full p-3 text-left cursor-pointer hover:bg-slate-900/50 transition-colors"
>
<div className="flex items-center gap-2 text-violet-400">
{isExpanded ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
<Bolt className="h-3 w-3" />
<span className="font-mono">{tool.toolName}</span>
{tool.state === "result" && (
<span className="text-xs text-slate-500 ml-auto">
{`~${getTokenCount(tool.result?.content ?? []).toLocaleString()}
tokens`}
</span>
)}
</div>
</button>
{isExpanded && tool.state === "result" && tool.result && (
<div className="px-3 pb-3 border-t border-slate-600/30 text-slate-300">
<div className="mt-2">
<ToolContent content={tool.result.content} />
</div>
</div>
)}
</div>
);
}
export function ToolContent({ content }: { content: ToolMessage[] }) {
return (
<div className="space-y-3">
{content.map((message: ToolMessage, index: number) => (
<div key={`message-${message.type}-${index}`} className="space-y-2">
<pre className="text-slate-400 text-sm whitespace-pre-wrap overflow-x-auto">
{isTextMessage(message)
? message.text
: JSON.stringify(message, null, 2)}
</pre>
</div>
))}
</div>
);
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/find-dsns.ts:
--------------------------------------------------------------------------------
```typescript
import { setTag } from "@sentry/core";
import { defineTool } from "../internal/tool-helpers/define";
import { apiServiceFromContext } from "../internal/tool-helpers/api";
import type { ServerContext } from "../types";
import {
ParamOrganizationSlug,
ParamRegionUrl,
ParamProjectSlug,
} from "../schema";
export default defineTool({
name: "find_dsns",
requiredScopes: ["project:read"],
description: [
"List all Sentry DSNs for a specific project.",
"",
"Use this tool when you need to:",
"- Retrieve a SENTRY_DSN for a specific project",
"",
"<hints>",
"- If the user passes a parameter in the form of name/otherName, its likely in the format of <organizationSlug>/<projectSlug>.",
"- If only one parameter is provided, and it could be either `organizationSlug` or `projectSlug`, its probably `organizationSlug`, but if you're really uncertain you might want to call `find_organizations()` first.",
"</hints>",
].join("\n"),
inputSchema: {
organizationSlug: ParamOrganizationSlug,
regionUrl: ParamRegionUrl.optional(),
projectSlug: ParamProjectSlug,
},
annotations: {
readOnlyHint: true,
openWorldHint: true,
},
async handler(params, context: ServerContext) {
const apiService = apiServiceFromContext(context, {
regionUrl: params.regionUrl,
});
const organizationSlug = params.organizationSlug;
setTag("organization.slug", organizationSlug);
setTag("project.slug", params.projectSlug);
const clientKeys = await apiService.listClientKeys({
organizationSlug,
projectSlug: params.projectSlug,
});
let output = `# DSNs in **${organizationSlug}/${params.projectSlug}**\n\n`;
if (clientKeys.length === 0) {
output +=
"No DSNs were found.\n\nYou can create new one using the `create_dsn` tool.";
return output;
}
for (const clientKey of clientKeys) {
output += `## ${clientKey.name}\n`;
output += `**ID**: ${clientKey.id}\n`;
output += `**DSN**: ${clientKey.dsn.public}\n\n`;
}
output += "# Using this information\n\n";
output +=
"- The `SENTRY_DSN` value is a URL that you can use to initialize Sentry's SDKs.\n";
return output;
},
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/cli/parse.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import { parseArgv, parseEnv, merge } from "./parse";
describe("cli/parseArgv", () => {
it("parses known flags and short aliases", () => {
const parsed = parseArgv([
"--access-token=tok",
"--host=sentry.io",
"--url=https://example.com",
"--mcp-url=https://mcp.example.com",
"--sentry-dsn=dsn",
"--openai-base-url=https://api.example.com/v1",
"--scopes=org:read",
"--add-scopes=event:write",
"--all-scopes",
"-h",
"-v",
]);
expect(parsed.accessToken).toBe("tok");
expect(parsed.host).toBe("sentry.io");
expect(parsed.url).toBe("https://example.com");
expect(parsed.mcpUrl).toBe("https://mcp.example.com");
expect(parsed.sentryDsn).toBe("dsn");
expect(parsed.openaiBaseUrl).toBe("https://api.example.com/v1");
expect(parsed.scopes).toBe("org:read");
expect(parsed.addScopes).toBe("event:write");
expect(parsed.allScopes).toBe(true);
expect(parsed.help).toBe(true);
expect(parsed.version).toBe(true);
expect(parsed.unknownArgs).toEqual([]);
});
it("collects unknown args", () => {
const parsed = parseArgv(["--unknown", "--another=1"]);
expect(parsed.unknownArgs.length).toBeGreaterThan(0);
});
});
describe("cli/parseEnv + merge", () => {
it("applies precedence: CLI over env", () => {
const env = parseEnv({
SENTRY_ACCESS_TOKEN: "envtok",
SENTRY_HOST: "envhost",
MCP_URL: "envmcp",
SENTRY_DSN: "envdsn",
MCP_SCOPES: "org:read",
MCP_ADD_SCOPES: "event:write",
} as any);
const cli = parseArgv([
"--access-token=clitok",
"--host=clihost",
"--mcp-url=climcp",
"--sentry-dsn=clidsn",
"--openai-base-url=https://api.cli/v1",
"--scopes=org:admin",
"--add-scopes=project:write",
]);
const merged = merge(cli, env);
expect(merged.accessToken).toBe("clitok");
expect(merged.host).toBe("clihost");
expect(merged.mcpUrl).toBe("climcp");
expect(merged.sentryDsn).toBe("clidsn");
expect(merged.openaiBaseUrl).toBe("https://api.cli/v1");
expect(merged.scopes).toBe("org:admin");
expect(merged.addScopes).toBe("project:write");
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@sentry/mcp-cloudflare",
"version": "0.18.0",
"private": true,
"type": "module",
"license": "FSL-1.1-ALv2",
"files": ["./dist/*"],
"exports": {
".": {
"types": "./dist/index.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -b && vite build",
"dev": "vite",
"deploy": "pnpm exec wrangler deploy",
"cf:versions:upload": "npx wrangler versions upload",
"preview": "vite preview",
"cf-typegen": "wrangler types",
"test": "vitest run",
"test:ci": "vitest run --coverage --reporter=default --reporter=junit --outputFile=tests.junit.xml",
"test:watch": "vitest",
"tsc": "tsc --noEmit"
},
"devDependencies": {
"@cloudflare/vite-plugin": "catalog:",
"@cloudflare/vitest-pool-workers": "catalog:",
"@cloudflare/workers-types": "catalog:",
"@sentry/mcp-server": "workspace:*",
"@sentry/mcp-server-mocks": "workspace:*",
"@sentry/mcp-server-tsconfig": "workspace:*",
"@sentry/vite-plugin": "catalog:",
"@tailwindcss/typography": "catalog:",
"@tailwindcss/vite": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/react-scroll-to-bottom": "^4.2.5",
"@vitejs/plugin-react": "catalog:",
"tailwindcss": "catalog:",
"urlpattern-polyfill": "^10.1.0",
"vite": "catalog:",
"vitest": "catalog:",
"wrangler": "catalog:"
},
"dependencies": {
"@ai-sdk/openai": "catalog:",
"@ai-sdk/react": "catalog:",
"@cloudflare/workers-oauth-provider": "catalog:",
"@modelcontextprotocol/sdk": "catalog:",
"@radix-ui/react-accordion": "catalog:",
"@radix-ui/react-slot": "catalog:",
"@sentry/cloudflare": "catalog:",
"@sentry/react": "catalog:",
"agents": "catalog:",
"ai": "catalog:",
"better-sqlite3": "catalog:",
"class-variance-authority": "catalog:",
"clsx": "catalog:",
"hono": "catalog:",
"lucide-react": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-markdown": "catalog:",
"react-scroll-to-bottom": "^4.2.0",
"remark-gfm": "catalog:",
"tailwind-merge": "catalog:",
"tw-animate-css": "catalog:",
"workers-mcp": "catalog:",
"zod": "catalog:"
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/find-teams.ts:
--------------------------------------------------------------------------------
```typescript
import { setTag } from "@sentry/core";
import { defineTool } from "../internal/tool-helpers/define";
import { apiServiceFromContext } from "../internal/tool-helpers/api";
import { UserInputError } from "../errors";
import type { ServerContext } from "../types";
import {
ParamOrganizationSlug,
ParamRegionUrl,
ParamSearchQuery,
} from "../schema";
const RESULT_LIMIT = 25;
export default defineTool({
name: "find_teams",
requiredScopes: ["team:read"],
description: [
"Find teams in an organization in Sentry.",
"",
"Use this tool when you need to:",
"- View teams in a Sentry organization",
"- Find a team's slug to aid other tool requests",
"- Search for specific teams by name or slug",
"",
`Returns up to ${RESULT_LIMIT} results. If you hit this limit, use the query parameter to narrow down results.`,
].join("\n"),
inputSchema: {
organizationSlug: ParamOrganizationSlug,
regionUrl: ParamRegionUrl.optional(),
query: ParamSearchQuery.optional(),
},
annotations: {
readOnlyHint: true,
openWorldHint: true,
},
async handler(params, context: ServerContext) {
const apiService = apiServiceFromContext(context, {
regionUrl: params.regionUrl,
});
const organizationSlug = params.organizationSlug;
if (!organizationSlug) {
throw new UserInputError(
"Organization slug is required. Please provide an organizationSlug parameter.",
);
}
setTag("organization.slug", organizationSlug);
const teams = await apiService.listTeams(organizationSlug, {
query: params.query,
});
let output = `# Teams in **${organizationSlug}**\n\n`;
if (params.query) {
output += `**Search query:** "${params.query}"\n\n`;
}
if (teams.length === 0) {
output += params.query
? `No teams found matching "${params.query}".\n`
: "No teams found.\n";
return output;
}
output += teams.map((team) => `- ${team.slug}\n`).join("");
if (teams.length === RESULT_LIMIT) {
output += `\n---\n\n**Note:** Showing ${RESULT_LIMIT} results (maximum). There may be more teams available. Use the \`query\` parameter to search for specific teams.`;
}
return output;
},
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/find-projects.ts:
--------------------------------------------------------------------------------
```typescript
import { setTag } from "@sentry/core";
import { defineTool } from "../internal/tool-helpers/define";
import { apiServiceFromContext } from "../internal/tool-helpers/api";
import { UserInputError } from "../errors";
import type { ServerContext } from "../types";
import {
ParamOrganizationSlug,
ParamRegionUrl,
ParamSearchQuery,
} from "../schema";
const RESULT_LIMIT = 25;
export default defineTool({
name: "find_projects",
requiredScopes: ["project:read"],
description: [
"Find projects in Sentry.",
"",
"Use this tool when you need to:",
"- View projects in a Sentry organization",
"- Find a project's slug to aid other tool requests",
"- Search for specific projects by name or slug",
"",
`Returns up to ${RESULT_LIMIT} results. If you hit this limit, use the query parameter to narrow down results.`,
].join("\n"),
inputSchema: {
organizationSlug: ParamOrganizationSlug,
regionUrl: ParamRegionUrl.optional(),
query: ParamSearchQuery.optional(),
},
annotations: {
readOnlyHint: true,
openWorldHint: true,
},
async handler(params, context: ServerContext) {
const apiService = apiServiceFromContext(context, {
regionUrl: params.regionUrl,
});
const organizationSlug = params.organizationSlug;
if (!organizationSlug) {
throw new UserInputError(
"Organization slug is required. Please provide an organizationSlug parameter.",
);
}
setTag("organization.slug", organizationSlug);
const projects = await apiService.listProjects(organizationSlug, {
query: params.query,
});
let output = `# Projects in **${organizationSlug}**\n\n`;
if (params.query) {
output += `**Search query:** "${params.query}"\n\n`;
}
if (projects.length === 0) {
output += params.query
? `No projects found matching "${params.query}".\n`
: "No projects found.\n";
return output;
}
output += projects.map((project) => `- **${project.slug}**\n`).join("");
if (projects.length === RESULT_LIMIT) {
output += `\n---\n\n**Note:** Showing ${RESULT_LIMIT} results (maximum). There may be more projects available. Use the \`query\` parameter to search for specific projects.`;
}
return output;
},
});
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/app.ts:
--------------------------------------------------------------------------------
```typescript
import { Hono } from "hono";
import { csrf } from "hono/csrf";
import { secureHeaders } from "hono/secure-headers";
import * as Sentry from "@sentry/cloudflare";
import type { Env } from "./types";
import sentryOauth from "./oauth";
import chatOauth from "./routes/chat-oauth";
import chat from "./routes/chat";
import search from "./routes/search";
import metadata from "./routes/metadata";
import { logIssue } from "@sentry/mcp-server/telem/logging";
import { createRequestLogger } from "./logging";
import mcpRoutes from "./routes/mcp";
const app = new Hono<{
Bindings: Env;
}>()
.use("*", createRequestLogger())
// Set user IP address from X-Real-IP header for Sentry
.use("*", async (c, next) => {
const clientIP =
c.req.header("X-Real-IP") ||
c.req.header("CF-Connecting-IP") ||
c.req.header("X-Forwarded-For")?.split(",")[0]?.trim();
if (clientIP) {
Sentry.setUser({ ip_address: clientIP });
}
await next();
})
// Apply security middleware globally
.use(
"*",
secureHeaders({
xFrameOptions: "DENY",
xContentTypeOptions: "nosniff",
referrerPolicy: "strict-origin-when-cross-origin",
strictTransportSecurity: "max-age=31536000; includeSubDomains",
}),
)
.use(
"*",
csrf({
origin: (origin, c) => {
if (!origin) {
return true;
}
const requestUrl = new URL(c.req.url);
return origin === requestUrl.origin;
},
}),
)
.get("/robots.txt", (c) => {
return c.text(["User-agent: *", "Allow: /$", "Disallow: /"].join("\n"));
})
.get("/llms.txt", (c) => {
return c.text(
[
"# sentry-mcp",
"",
"This service implements the Model Context Protocol for interacting with Sentry (https://sentry.io/welcome/).",
"",
`The MCP's server address is: ${new URL("/mcp", c.req.url).href}`,
"",
].join("\n"),
);
})
.route("/oauth", sentryOauth)
.route("/api/auth", chatOauth)
.route("/api/chat", chat)
.route("/api/search", search)
.route("/api/metadata", metadata)
.route("/.mcp", mcpRoutes);
// TODO: propagate the error as sentry isnt injecting into hono
app.onError((err, c) => {
logIssue(err);
return c.text("Internal Server Error", 500);
});
export default app;
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/geo.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "geo",
"description": "Geo fields can carry data about a specific location related to an event. This geolocation information can be derived from techniques such as Geo IP, or be user-supplied.\nNote: Geo attributes are typically used under another namespace, such as client.* and describe the location of the corresponding entity (device, end-user, etc). Semantic conventions that reference geo attributes (as a root namespace) or embed them (under their own namespace) SHOULD document what geo attributes describe in the scope of that convention.\n",
"attributes": {
"geo.locality.name": {
"description": "Locality name. Represents the name of a city, town, village, or similar populated place.\n",
"type": "string",
"stability": "development",
"examples": ["Montreal", "Berlin"]
},
"geo.continent.code": {
"description": "Two-letter code representing continent’s name.\n",
"type": "string",
"stability": "development",
"examples": ["AF", "AN", "AS", "EU", "NA", "OC", "SA"]
},
"geo.country.iso_code": {
"description": "Two-letter ISO Country Code ([ISO 3166-1 alpha2](https://wikipedia.org/wiki/ISO_3166-1#Codes)).\n",
"type": "string",
"stability": "development",
"examples": ["CA"]
},
"geo.location.lon": {
"description": "Longitude of the geo location in [WGS84](https://wikipedia.org/wiki/World_Geodetic_System#WGS84).\n",
"type": "number",
"stability": "development",
"examples": ["-73.61483"]
},
"geo.location.lat": {
"description": "Latitude of the geo location in [WGS84](https://wikipedia.org/wiki/World_Geodetic_System#WGS84).\n",
"type": "number",
"stability": "development",
"examples": ["45.505918"]
},
"geo.postal_code": {
"description": "Postal code associated with the location. Values appropriate for this field may also be known as a postcode or ZIP code and will vary widely from country to country.\n",
"type": "string",
"stability": "development",
"examples": ["94040"]
},
"geo.region.iso_code": {
"description": "Region ISO code ([ISO 3166-2](https://wikipedia.org/wiki/ISO_3166-2)).\n",
"type": "string",
"stability": "development",
"examples": ["CA-QC"]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/lib/approval-dialog.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, beforeEach, vi } from "vitest";
import { renderApprovalDialog } from "./approval-dialog";
describe("approval-dialog", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("renderApprovalDialog", () => {
it("should include state in the form", async () => {
const mockRequest = new Request("https://example.com/oauth/authorize", {
method: "GET",
});
const options = {
client: {
clientId: "test-client-id",
clientName: "Test Client",
redirectUris: ["https://example.com/callback"],
tokenEndpointAuthMethod: "client_secret_basic",
},
server: {
name: "Test Server",
},
state: { oauthReqInfo: { clientId: "test-client" } },
};
const response = await renderApprovalDialog(mockRequest, options);
const html = await response.text();
// Check that state is included in the form
expect(html).toContain('name="state"');
expect(html).toContain('value="');
});
it("should sanitize HTML content", async () => {
const mockRequest = new Request("https://example.com/oauth/authorize", {
method: "GET",
});
const options = {
client: {
clientId: "test-client-id",
clientName: "<script>alert('xss')</script>",
redirectUris: ["https://example.com/callback"],
tokenEndpointAuthMethod: "client_secret_basic",
},
server: {
name: "Test Server",
},
state: { test: "data" },
};
const response = await renderApprovalDialog(mockRequest, options);
const html = await response.text();
// Check that script tags in client name are escaped and no script tags are present
expect(html).not.toContain("<script>alert('xss')</script>");
expect(html).toContain(
"<script>alert('xss')</script>",
);
// Should not contain any script tags (JavaScript-free implementation)
expect(html).not.toContain("<script>");
});
});
// parseRedirectApproval behavior (form parsing, cookies, permissions) is
// validated at the route level in oauth/authorize.test.ts to keep concerns
// consolidated around HTTP behavior. This test file focuses on pure
// rendering concerns of the dialog itself.
});
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "sentry-mcp",
"version": "0.0.1",
"private": true,
"type": "module",
"packageManager": "[email protected]",
"engines": {
"node": ">=20"
},
"license": "FSL-1.1-ALv2",
"author": "Sentry",
"description": "Sentry MCP Server",
"homepage": "https://github.com/getsentry/sentry-mcp",
"keywords": ["sentry"],
"bugs": {
"url": "https://github.com/getsentry/sentry-mcp/issues"
},
"repository": {
"type": "git",
"url": "[email protected]:getsentry/sentry-mcp.git"
},
"scripts": {
"docs:check": "node scripts/check-doc-links.mjs",
"dev": "dotenv -e .env -e .env.local -- turbo dev",
"build": "turbo build after-build",
"deploy": "turbo deploy",
"eval": "dotenv -e .env -e .env.local -- turbo eval",
"eval:ci": "CI=true dotenv -e .env -e .env.local -- pnpm --stream -r run eval:ci",
"format": "biome format --write",
"lint": "biome lint",
"lint:fix": "biome lint --fix",
"inspector": "pnpx @modelcontextprotocol/inspector@latest",
"prepare": "simple-git-hooks",
"start:client": "pnpm run --filter ./packages/mcp-test-client start",
"start:stdio": "pnpm --stream run --filter ./packages/mcp-server start",
"test": "dotenv -e .env -e .env.local -- turbo test",
"test:ci": "CI=true dotenv -e .env -e .env.local -- pnpm --stream -r run test:ci",
"test:watch": "dotenv -e .env -e .env.local -- turbo test:watch",
"tsc": "turbo tsc"
},
"dependencies": {
"@biomejs/biome": "catalog:",
"@types/node": "catalog:",
"@vitest/coverage-v8": "catalog:",
"dotenv": "catalog:",
"dotenv-cli": "catalog:",
"lint-staged": "catalog:",
"simple-git-hooks": "catalog:",
"tsdown": "catalog:",
"tsx": "catalog:",
"turbo": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:",
"vitest-evals": "catalog:"
},
"simple-git-hooks": {
"pre-commit": "pnpm exec lint-staged --concurrent false"
},
"lint-staged": {
"*": [
"biome format --write --no-errors-on-unmatched --files-ignore-unknown=true",
"biome lint --fix --no-errors-on-unmatched --files-ignore-unknown=true"
]
},
"pnpm": {
"onlyBuiltDependencies": [
"@biomejs/biome",
"better-sqlite3",
"esbuild",
"sharp",
"simple-git-hooks",
"workerd"
]
},
"devDependencies": {
"@types/json-schema": "^7.0.15"
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/cli/resolve.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import { finalize } from "./resolve";
import { ALL_SCOPES } from "../permissions";
describe("cli/finalize", () => {
it("throws on missing access token", () => {
expect(() => finalize({ unknownArgs: [] } as any)).toThrow(
/No access token was provided/,
);
});
it("throws on invalid scopes", () => {
expect(() =>
finalize({ accessToken: "tok", scopes: "foo", unknownArgs: [] }),
).toThrow(/Invalid scopes provided: foo/);
});
it("expands implied scopes for --scopes", () => {
const cfg = finalize({
accessToken: "tok",
scopes: "event:write",
unknownArgs: [],
});
expect(cfg.finalScopes?.has("event:write")).toBe(true);
expect(cfg.finalScopes?.has("event:read")).toBe(true);
});
it("merges defaults for --add-scopes and expands", () => {
const cfg = finalize({
accessToken: "tok",
addScopes: "project:write",
unknownArgs: [],
});
expect(cfg.finalScopes?.has("project:write")).toBe(true);
// Defaults include project:read
expect(cfg.finalScopes?.has("project:read")).toBe(true);
});
it("grants all scopes with --all-scopes", () => {
const cfg = finalize({
accessToken: "tok",
allScopes: true,
unknownArgs: [],
});
expect(cfg.finalScopes?.size).toBe(ALL_SCOPES.length);
expect(cfg.finalScopes?.has("org:admin")).toBe(true);
});
it("normalizes host from URL", () => {
const cfg = finalize({
accessToken: "tok",
url: "https://sentry.example.com",
unknownArgs: [],
});
expect(cfg.sentryHost).toBe("sentry.example.com");
});
it("accepts valid OpenAI base URL", () => {
const cfg = finalize({
accessToken: "tok",
openaiBaseUrl: "https://api.proxy.example/v1",
unknownArgs: [],
});
expect(cfg.openaiBaseUrl).toBe(
new URL("https://api.proxy.example/v1").toString(),
);
});
it("rejects invalid OpenAI base URL", () => {
expect(() =>
finalize({
accessToken: "tok",
openaiBaseUrl: "ftp://example.com",
unknownArgs: [],
}),
).toThrow(/OPENAI base URL must use http or https scheme/);
});
it("throws on non-https URL", () => {
expect(() =>
finalize({ accessToken: "tok", url: "http://bad", unknownArgs: [] }),
).toThrow(/must be a full HTTPS URL/);
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/create-dsn.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import { setTag } from "@sentry/core";
import { defineTool } from "../internal/tool-helpers/define";
import { apiServiceFromContext } from "../internal/tool-helpers/api";
import type { ServerContext } from "../types";
import {
ParamOrganizationSlug,
ParamRegionUrl,
ParamProjectSlug,
} from "../schema";
export default defineTool({
name: "create_dsn",
requiredScopes: ["project:write"],
description: [
"Create an additional DSN for an EXISTING project.",
"",
"🔍 USE THIS TOOL WHEN:",
"- Project already exists and needs additional DSN",
"- 'Create another DSN for project X'",
"- 'I need a production DSN for existing project'",
"",
"❌ DO NOT USE for new projects (use create_project instead)",
"",
"Be careful when using this tool!",
"",
"<examples>",
"### Create additional DSN for existing project",
"```",
"create_dsn(organizationSlug='my-organization', projectSlug='my-project', name='Production')",
"```",
"</examples>",
"",
"<hints>",
"- If the user passes a parameter in the form of name/otherName, its likely in the format of <organizationSlug>/<projectSlug>.",
"- If any parameter is ambiguous, you should clarify with the user what they meant.",
"</hints>",
].join("\n"),
inputSchema: {
organizationSlug: ParamOrganizationSlug,
regionUrl: ParamRegionUrl.optional(),
projectSlug: ParamProjectSlug,
name: z
.string()
.trim()
.describe("The name of the DSN to create, for example 'Production'."),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
openWorldHint: true,
},
async handler(params, context: ServerContext) {
const apiService = apiServiceFromContext(context, {
regionUrl: params.regionUrl,
});
const organizationSlug = params.organizationSlug;
setTag("organization.slug", organizationSlug);
setTag("project.slug", params.projectSlug);
const clientKey = await apiService.createClientKey({
organizationSlug,
projectSlug: params.projectSlug,
name: params.name,
});
let output = `# New DSN in **${organizationSlug}/${params.projectSlug}**\n\n`;
output += `**DSN**: ${clientKey.dsn.public}\n`;
output += `**Name**: ${clientKey.name}\n\n`;
output += "# Using this information\n\n";
output +=
"- The `SENTRY_DSN` value is a URL that you can use to initialize Sentry's SDKs.\n";
return output;
},
});
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/oauth/state.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
// Minimal, stateless HMAC-signed OAuth state utilities
// Format: `${signatureHex}.${base64(payloadJson)}`
// Safe envelope: keep the full downstream AuthRequest (+permissions) under `req`
// and include only iat/exp metadata at top-level to avoid collisions.
export const OAuthStateSchema = z.object({
req: z.record(z.unknown()),
iat: z.number().int(),
exp: z.number().int(),
});
export type OAuthState = z.infer<typeof OAuthStateSchema> & {
req: Record<string, unknown>;
};
async function importKey(secret: string): Promise<CryptoKey> {
if (!secret) {
throw new Error(
"COOKIE_SECRET is not defined. A secret key is required for signing state.",
);
}
const enc = new TextEncoder();
return crypto.subtle.importKey(
"raw",
enc.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"],
);
}
async function signHex(key: CryptoKey, data: string): Promise<string> {
const enc = new TextEncoder();
const signatureBuffer = await crypto.subtle.sign(
"HMAC",
key,
enc.encode(data),
);
return Array.from(new Uint8Array(signatureBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
async function verifyHex(
key: CryptoKey,
signatureHex: string,
data: string,
): Promise<boolean> {
try {
const enc = new TextEncoder();
const signatureBytes = new Uint8Array(
signatureHex.match(/.{1,2}/g)!.map((byte) => Number.parseInt(byte, 16)),
);
return await crypto.subtle.verify(
"HMAC",
key,
signatureBytes.buffer,
enc.encode(data),
);
} catch {
return false;
}
}
export async function signState(
payload: OAuthState,
secret: string,
): Promise<string> {
const key = await importKey(secret);
const json = JSON.stringify(payload);
const sig = await signHex(key, json);
// Using standard base64 to match other usage in the codebase
const b64 = btoa(json);
return `${sig}.${b64}`;
}
export async function verifyAndParseState(
compact: string,
secret: string,
): Promise<OAuthState> {
const [sig, b64] = compact.split(".");
if (!sig || !b64) {
throw new Error("Invalid state format");
}
const json = atob(b64);
const key = await importKey(secret);
const ok = await verifyHex(key, sig, json);
if (!ok) {
throw new Error("Invalid state signature");
}
const parsed = OAuthStateSchema.parse(JSON.parse(json));
const now = Date.now();
if (parsed.exp <= now) {
throw new Error("State expired");
}
return parsed;
}
```
--------------------------------------------------------------------------------
/scripts/check-doc-links.mjs:
--------------------------------------------------------------------------------
```
#!/usr/bin/env node
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, resolve } from "node:path";
const root = resolve(process.cwd());
const docsDir = join(root, "docs");
/** Recursively collect docs files */
function walk(dir) {
const entries = readdirSync(dir);
const files = [];
for (const e of entries) {
const p = join(dir, e);
const s = statSync(p);
if (s.isDirectory()) files.push(...walk(p));
else if (e.endsWith(".md") || e.endsWith(".mdc")) files.push(p);
}
return files;
}
const files = walk(docsDir);
const problems = [];
for (const file of files) {
const rel = file.slice(root.length + 1);
const content = readFileSync(file, "utf8");
// Strip fenced code blocks to avoid false positives in examples
const contentNoFences = content.replace(/```[\s\S]*?```/g, "");
// 1) Flag local Markdown links like [text](./file.md) or [text](../file.mdc)
const localLinkRe = /\[[^\]]+\]\((\.\.?\/[^)]+)\)/g;
for (const m of contentNoFences.matchAll(localLinkRe)) {
// Skip illustrative placeholders
if (m[1].includes("...")) continue;
problems.push({
file: rel,
type: "local-markdown-link",
message: `Use @path for local docs instead of Markdown links: ${m[0]}`,
});
}
// 1b) Flag Markdown links that point to @path
const atMarkdownLinkRe = /\[[^\]]+\]\(@[^)]+\)/g;
for (const m of contentNoFences.matchAll(atMarkdownLinkRe)) {
problems.push({
file: rel,
type: "atpath-markdown-link",
message: `Do not wrap @paths in Markdown links: ${m[0]}`,
});
}
// 2) Validate @path references point to real files (only for clear file tokens)
// Matches @path segments with known extensions or obvious repo files
const atPathRe = /@([A-Za-z0-9_.\-\/]+\.(?:mdc|md|ts|tsx|js|json))/g;
for (const m of contentNoFences.matchAll(atPathRe)) {
const relPath = m[1];
const abs = join(root, relPath);
try {
const st = statSync(abs);
if (!st.isFile()) {
problems.push({
file: rel,
type: "missing-file",
message: `@${relPath} is not a file`,
});
}
} catch {
problems.push({
file: rel,
type: "missing-file",
message: `@${relPath} does not exist`,
});
}
}
}
if (problems.length) {
console.error("[docs:check] Problems found:\n");
for (const p of problems) {
console.error(`- ${p.type}: ${p.file} -> ${p.message}`);
}
process.exit(1);
} else {
console.log("[docs:check] OK: no local Markdown links and all @paths exist.");
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/cli/resolve.ts:
--------------------------------------------------------------------------------
```typescript
import {
ALL_SCOPES,
parseScopes,
resolveScopes,
type Scope,
} from "../permissions";
import { DEFAULT_SCOPES } from "../constants";
import {
validateAndParseSentryUrlThrows,
validateOpenAiBaseUrlThrows,
validateSentryHostThrows,
} from "../utils/url-utils";
import type { MergedArgs, ResolvedConfig } from "./types";
export function formatInvalid(invalid: string[], envName?: string): string {
const where = envName ? `${envName} provided` : "Invalid scopes provided";
return `Error: ${where}: ${invalid.join(", ")}\nAvailable scopes: ${ALL_SCOPES.join(", ")}`;
}
export function finalize(input: MergedArgs): ResolvedConfig {
// Access token required
if (!input.accessToken) {
throw new Error(
"Error: No access token was provided. Pass one with `--access-token` or via `SENTRY_ACCESS_TOKEN`.",
);
}
// Determine host from url/host with validation
let sentryHost = "sentry.io";
if (input.url) {
sentryHost = validateAndParseSentryUrlThrows(input.url);
} else if (input.host) {
validateSentryHostThrows(input.host);
sentryHost = input.host;
}
// Scopes resolution
let finalScopes: Set<Scope> | undefined = undefined;
if (input.allScopes) {
finalScopes = new Set<Scope>(ALL_SCOPES as ReadonlyArray<Scope>);
} else if (input.scopes || input.addScopes) {
// Strict validation: any invalid token is an error
if (input.scopes) {
const { valid, invalid } = parseScopes(input.scopes);
if (invalid.length > 0) {
throw new Error(formatInvalid(invalid));
}
if (valid.size === 0) {
throw new Error(
"Error: Invalid scopes provided. No valid scopes found.",
);
}
finalScopes = resolveScopes({
override: valid,
defaults: DEFAULT_SCOPES,
});
} else if (input.addScopes) {
const { valid, invalid } = parseScopes(input.addScopes);
if (invalid.length > 0) {
throw new Error(formatInvalid(invalid));
}
if (valid.size === 0) {
throw new Error(
"Error: Invalid additional scopes provided. No valid scopes found.",
);
}
finalScopes = resolveScopes({ add: valid, defaults: DEFAULT_SCOPES });
}
}
const resolvedOpenAiBaseUrl = input.openaiBaseUrl
? validateOpenAiBaseUrlThrows(input.openaiBaseUrl)
: undefined;
return {
accessToken: input.accessToken,
sentryHost,
mcpUrl: input.mcpUrl,
sentryDsn: input.sentryDsn,
openaiBaseUrl: resolvedOpenAiBaseUrl,
finalScopes,
organizationSlug: input.organizationSlug,
projectSlug: input.projectSlug,
};
}
```
--------------------------------------------------------------------------------
/packages/mcp-server-evals/src/evals/search-issues.eval.ts:
--------------------------------------------------------------------------------
```typescript
import { describeEval } from "vitest-evals";
import { FIXTURES, NoOpTaskRunner, ToolPredictionScorer } from "./utils";
// Note: This eval requires OPENAI_API_KEY to be set in the environment
// The search_issues tool uses the AI SDK to translate natural language queries
describeEval("search-issues", {
data: async () => {
return [
// Core test: Basic issue search
{
input: `Show me unresolved issues in ${FIXTURES.organizationSlug}`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "search_issues",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
naturalLanguageQuery: "unresolved issues",
},
},
],
},
// Core test: Search with 'me' reference (tests whoami integration)
{
input: `Find issues assigned to me in ${FIXTURES.organizationSlug}`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "whoami",
arguments: {},
},
{
name: "search_issues",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
naturalLanguageQuery: "issues assigned to me",
},
},
],
},
// Core test: Project-specific search
{
input: `Search for database errors in ${FIXTURES.organizationSlug}/${FIXTURES.projectSlug}`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "search_issues",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
projectSlugOrId: FIXTURES.projectSlug,
naturalLanguageQuery: "database errors",
},
},
],
},
// Core test: Complex natural language query
{
input: `Find critical production errors affecting more than 100 users in ${FIXTURES.organizationSlug}`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "search_issues",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
naturalLanguageQuery:
"critical production errors affecting more than 100 users",
},
},
],
},
];
},
task: NoOpTaskRunner(),
scorers: [ToolPredictionScorer()],
threshold: 0.6,
timeout: 30000,
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/device.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "device",
"description": "Describes device attributes.\n",
"attributes": {
"device.id": {
"description": "A unique identifier representing the device\n",
"type": "string",
"note": "Its value SHOULD be identical for all apps on a device and it SHOULD NOT change if an app is uninstalled and re-installed.\nHowever, it might be resettable by the user for all apps on a device.\nHardware IDs (e.g. vendor-specific serial number, IMEI or MAC address) MAY be used as values.\n\nMore information about Android identifier best practices can be found [here](https://developer.android.com/training/articles/user-data-ids).\n\n> [!WARNING]\n>\n> This attribute may contain sensitive (PII) information. Caution should be taken when storing personal data or anything which can identify a user. GDPR and data protection laws may apply,\n> ensure you do your own due diligence.\n>\n> Due to these reasons, this identifier is not recommended for consumer applications and will likely result in rejection from both Google Play and App Store.\n> However, it may be appropriate for specific enterprise scenarios, such as kiosk devices or enterprise-managed devices, with appropriate compliance clearance.\n> Any instrumentation providing this identifier MUST implement it as an opt-in feature.\n>\n> See [`app.installation.id`](/docs/registry/attributes/app.md#app-installation-id) for a more privacy-preserving alternative.\n",
"stability": "development",
"examples": ["123456789012345", "01:23:45:67:89:AB"]
},
"device.manufacturer": {
"description": "The name of the device manufacturer\n",
"type": "string",
"note": "The Android OS provides this field via [Build](https://developer.android.com/reference/android/os/Build#MANUFACTURER). iOS apps SHOULD hardcode the value `Apple`.\n",
"stability": "development",
"examples": ["Apple", "Samsung"]
},
"device.model.identifier": {
"description": "The model identifier for the device\n",
"type": "string",
"note": "It's recommended this value represents a machine-readable version of the model identifier rather than the market or consumer-friendly name of the device.\n",
"stability": "development",
"examples": ["iPhone3,4", "SM-G920F"]
},
"device.model.name": {
"description": "The marketing name for the device model\n",
"type": "string",
"note": "It's recommended this value represents a human-readable version of the device model rather than a machine-readable alternative.\n",
"stability": "development",
"examples": ["iPhone 6s Plus", "Samsung Galaxy S6"]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-server-evals/src/evals/list-issues.eval.ts:
--------------------------------------------------------------------------------
```typescript
import { describeEval } from "vitest-evals";
import { FIXTURES, NoOpTaskRunner, ToolPredictionScorer } from "./utils";
describeEval("list-issues", {
data: async () => {
return [
{
input: `What are the most common production errors in ${FIXTURES.organizationSlug}?`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "find_issues",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
query: "is:unresolved",
sortBy: "count",
regionUrl: "https://us.sentry.io",
},
},
],
},
{
input: `Show me the top issues in ${FIXTURES.organizationSlug} organization`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "find_issues",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
sortBy: "count",
regionUrl: "https://us.sentry.io",
},
},
],
},
{
input: `What are the most recent issues in ${FIXTURES.organizationSlug}?`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "find_issues",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
sortBy: "last_seen",
regionUrl: "https://us.sentry.io",
},
},
],
},
{
input: `Find the newest production issues in ${FIXTURES.organizationSlug}`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "find_issues",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
sortBy: "first_seen",
regionUrl: "https://us.sentry.io",
},
},
],
},
{
input: `What issues is [email protected] experiencing in ${FIXTURES.organizationSlug}?`,
expectedTools: [
{
name: "find_organizations",
arguments: {},
},
{
name: "find_issues",
arguments: {
organizationSlug: FIXTURES.organizationSlug,
query: "user.email:[email protected]",
regionUrl: "https://us.sentry.io",
},
},
],
},
];
},
task: NoOpTaskRunner(),
scorers: [ToolPredictionScorer()],
threshold: 0.6,
timeout: 30000,
});
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/lib/constraint-utils.ts:
--------------------------------------------------------------------------------
```typescript
import type { Constraints } from "@sentry/mcp-server/types";
import { SentryApiService, ApiError } from "@sentry/mcp-server/api-client";
import { logIssue } from "@sentry/mcp-server/telem/logging";
/**
* Verify that provided org/project constraints exist and the user has access
* by querying Sentry's API using the provided OAuth access token.
*/
export async function verifyConstraintsAccess(
{ organizationSlug, projectSlug }: Constraints,
{
accessToken,
sentryHost = "sentry.io",
}: {
accessToken: string | undefined | null;
sentryHost?: string;
},
): Promise<
| {
ok: true;
constraints: Constraints;
}
| { ok: false; status?: number; message: string; eventId?: string }
> {
if (!organizationSlug) {
// No constraints specified, nothing to verify
return {
ok: true,
constraints: {
organizationSlug: null,
projectSlug: null,
regionUrl: null,
},
};
}
if (!accessToken) {
return {
ok: false,
status: 401,
message: "Missing access token for constraint verification",
};
}
// Use shared API client for consistent behavior and error handling
const api = new SentryApiService({ accessToken, host: sentryHost });
// Verify organization using API client
let regionUrl: string | null | undefined = null;
try {
const org = await api.getOrganization(organizationSlug);
regionUrl = org.links?.regionUrl || null;
} catch (error) {
if (error instanceof ApiError) {
const message =
error.status === 404
? `Organization '${organizationSlug}' not found`
: error.message;
return { ok: false, status: error.status, message };
}
const eventId = logIssue(error);
return {
ok: false,
status: 502,
message: "Failed to verify organization",
eventId,
};
}
// Verify project access if specified
if (projectSlug) {
try {
await api.getProject(
{
organizationSlug,
projectSlugOrId: projectSlug,
},
regionUrl ? { host: new URL(regionUrl).host } : undefined,
);
} catch (error) {
if (error instanceof ApiError) {
const message =
error.status === 404
? `Project '${projectSlug}' not found in organization '${organizationSlug}'`
: error.message;
return { ok: false, status: error.status, message };
}
const eventId = logIssue(error);
return {
ok: false,
status: 502,
message: "Failed to verify project",
eventId,
};
}
}
return {
ok: true,
constraints: {
organizationSlug,
projectSlug: projectSlug || null,
regionUrl: regionUrl || null,
},
};
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/lib/slug-validation.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from "vitest";
import { isValidSlug } from "./slug-validation";
describe("isValidSlug", () => {
describe("valid slugs", () => {
it("should accept alphanumeric slugs", () => {
expect(isValidSlug("test123")).toBe(true);
expect(isValidSlug("ABC")).toBe(true);
expect(isValidSlug("a")).toBe(true);
expect(isValidSlug("9")).toBe(true);
});
it("should accept slugs with dots, dashes, and underscores", () => {
expect(isValidSlug("test-project")).toBe(true);
expect(isValidSlug("test_project")).toBe(true);
expect(isValidSlug("test.project")).toBe(true);
expect(isValidSlug("test-project_v2.1")).toBe(true);
});
it("should accept slugs up to 100 characters", () => {
const maxSlug = "a".repeat(100);
expect(isValidSlug(maxSlug)).toBe(true);
});
});
describe("invalid slugs", () => {
it("should reject empty strings", () => {
expect(isValidSlug("")).toBe(false);
expect(isValidSlug(null as any)).toBe(false);
expect(isValidSlug(undefined as any)).toBe(false);
});
it("should reject slugs over 100 characters", () => {
const longSlug = "a".repeat(101);
expect(isValidSlug(longSlug)).toBe(false);
});
it("should reject path traversal attempts", () => {
expect(isValidSlug("../etc/passwd")).toBe(false);
expect(isValidSlug("test/../admin")).toBe(false);
expect(isValidSlug("test//admin")).toBe(false);
});
it("should reject URL patterns", () => {
expect(isValidSlug("http://evil.com")).toBe(false);
expect(isValidSlug("file://test")).toBe(false);
expect(isValidSlug("test://protocol")).toBe(false);
});
it("should reject percent encoding", () => {
expect(isValidSlug("test%20space")).toBe(false);
expect(isValidSlug("%2E%2E")).toBe(false);
});
it("should reject slugs not starting with alphanumeric", () => {
expect(isValidSlug(".test")).toBe(false);
expect(isValidSlug("-test")).toBe(false);
expect(isValidSlug("_test")).toBe(false);
});
it("should reject slugs not ending with alphanumeric", () => {
expect(isValidSlug("test.")).toBe(false);
expect(isValidSlug("test-")).toBe(false);
expect(isValidSlug("test_")).toBe(false);
});
it("should reject single non-alphanumeric characters", () => {
expect(isValidSlug(".")).toBe(false);
expect(isValidSlug("-")).toBe(false);
expect(isValidSlug("_")).toBe(false);
});
it("should reject special characters", () => {
expect(isValidSlug("test@org")).toBe(false);
expect(isValidSlug("test#tag")).toBe(false);
expect(isValidSlug("test$money")).toBe(false);
expect(isValidSlug("test space")).toBe(false);
});
});
});
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/otel-semantics.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { SentryApiService } from "../../../api-client";
vi.mock("../logging", () => ({
logIssue: vi.fn(),
}));
// Import the actual function - no mocking needed since build runs first
import { lookupOtelSemantics } from "./otel-semantics";
describe("otel-semantics-lookup", () => {
beforeEach(() => {
vi.clearAllMocks();
});
const mockApiService = {} as SentryApiService;
describe("lookupOtelSemantics", () => {
it("should return namespace information for valid namespace", async () => {
const result = await lookupOtelSemantics(
"gen_ai",
"spans",
mockApiService,
"test-org",
);
expect(result).toContain("# OpenTelemetry Semantic Conventions: gen_ai");
expect(result).toContain("## Attributes");
expect(result).toContain("`gen_ai.usage.input_tokens`");
expect(result).toContain("`gen_ai.usage.output_tokens`");
expect(result).toContain("- **Type:**");
expect(result).toContain("- **Description:**");
});
it("should handle namespace with underscore and dash interchangeably", async () => {
const result1 = await lookupOtelSemantics(
"gen_ai",
"spans",
mockApiService,
"test-org",
);
const result2 = await lookupOtelSemantics(
"gen-ai",
"spans",
mockApiService,
"test-org",
);
expect(result1).toBe(result2);
});
it("should return all attributes for a namespace", async () => {
const result = await lookupOtelSemantics(
"http",
"spans",
mockApiService,
"test-org",
);
expect(result).toContain("total)");
expect(result).toContain("`http.request.method`");
expect(result).toContain("`http.response.status_code`");
});
it("should show custom namespace note for mcp", async () => {
const result = await lookupOtelSemantics(
"mcp",
"spans",
mockApiService,
"test-org",
);
expect(result).toContain("**Note:** This is a custom namespace");
});
it("should handle invalid namespace", async () => {
const result = await lookupOtelSemantics(
"totally_invalid_namespace_that_does_not_exist",
"spans",
mockApiService,
"test-org",
);
expect(result).toContain(
"Namespace 'totally_invalid_namespace_that_does_not_exist' not found",
);
});
it("should suggest similar namespaces", async () => {
const result = await lookupOtelSemantics(
"gen",
"spans",
mockApiService,
"test-org",
);
expect(result).toContain("Did you mean:");
expect(result).toContain("gen_ai");
});
});
});
```
--------------------------------------------------------------------------------
/docs/specs/subpath-constraints.md:
--------------------------------------------------------------------------------
```markdown
# Subpath-Based Constraints (End-User Guide)
## What constraints do
Constraints let you scope your Sentry MCP session to a specific organization and optionally a project. When scoped, all tools automatically use that org/project by default and only access data you are permitted to see.
## How to connect
- No scope: connect to `/mcp` (or `/sse` for SSE transport)
- Organization scope: `/mcp/{organizationSlug}`
- Organization + project scope: `/mcp/{organizationSlug}/{projectSlug}`
The same pattern applies to the SSE endpoint: `/sse`, `/sse/{org}`, `/sse/{org}/{project}`.
Examples:
```
/mcp/sentry
/mcp/sentry/my-project
/sse/sentry
/sse/sentry/my-project
```
## What you’ll experience
- Tools automatically use the constrained organization/project as defaults
- You can still pass explicit `organizationSlug`/`projectSlug` to override defaults per call
- If you don’t provide a scope, tools work across your accessible organizations when supported
## Access verification
When you connect with a scoped path, we validate that:
- The slugs are well-formed
- The organization exists and you have access
- If a project is included, the project exists and you have access
If there’s a problem, you’ll receive a clear HTTP error when connecting:
- 400: Invalid slug format
- 401: Missing authentication
- 403: You don’t have access to the specified org/project
- 404: Organization or project not found
## Region awareness
For Sentry Cloud, your organization may be hosted in a regional cluster. When you scope by organization, we automatically determine the region (if available) and use it for API calls. You don’t need to take any action—this happens behind the scenes. For self-hosted Sentry, the region concept doesn’t apply.
## Best practices
- Prefer scoping by organization (and project when known) to reduce ambiguity and improve safety
- Use scoped sessions when collaborating across multiple orgs to avoid cross-org access by mistake
- If a tool reports access errors, reconnect with a different scope or verify your permissions in Sentry
## Frequently asked questions
- Can I switch scope mid-session?
- Yes. Open a new connection using a different subpath (e.g., `/mcp/{org}/{project}`) and use that session.
- Do I need to specify scope for documentation or metadata endpoints?
- No. Public metadata endpoints don’t require scope and support CORS.
- How do tools know my scope?
- The MCP session embeds the constraints, and tools read them as defaults for `organizationSlug` and `projectSlug`.
## Reference
Supported URL patterns:
```
/mcp/{organizationSlug}/{projectSlug}
/mcp/{organizationSlug}
/mcp
/sse/{organizationSlug}/{projectSlug}
/sse/{organizationSlug}
/sse
```
For implementation details and security notes, see:
- `docs/cloudflare/constraint-flow-verification.md`
- `docs/architecture.mdc`
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/utils/slug-validation.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Slug validation utilities to prevent path traversal and injection attacks.
*
* Provides reusable validation functions for use with Zod's superRefine()
* to add security validation for URL parameters.
*/
import { z } from "zod";
/**
* Maximum reasonable length for a slug.
*/
const MAX_SLUG_LENGTH = 100;
/**
* Maximum reasonable length for a numeric ID.
*/
const MAX_ID_LENGTH = 20;
/**
* Helper to check if a string is a numeric ID.
*/
export function isNumericId(value: string): boolean {
return /^\d+$/.test(value);
}
/**
* Valid slug pattern: alphanumeric, hyphens, underscores, and dots.
* Must start with alphanumeric character.
*/
const VALID_SLUG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
/**
* Validates a slug to prevent path traversal and injection attacks.
* Designed to be used with Zod's superRefine() method.
*
* @example
* ```typescript
* const OrganizationSlug = z.string()
* .toLowerCase()
* .trim()
* .superRefine(validateSlug)
* .describe("Organization slug");
*
* const TeamSlug = z.string()
* .toLowerCase()
* .trim()
* .superRefine(validateSlug)
* .describe("Team slug");
* ```
*/
export function validateSlug(val: string, ctx: z.RefinementCtx): void {
// Check for empty string
if (val.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Slug cannot be empty",
});
return;
}
// Check length
if (val.length > MAX_SLUG_LENGTH) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Slug exceeds maximum length of ${MAX_SLUG_LENGTH} characters`,
});
return;
}
// Validate pattern - this implicitly blocks all dangerous characters and patterns
if (!VALID_SLUG_PATTERN.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"Slug must contain only alphanumeric characters, hyphens, underscores, and dots, and must start with an alphanumeric character",
});
}
}
/**
* Validates a parameter that can be either a slug or numeric ID.
* Designed to be used with Zod's superRefine() method.
*
* @example
* ```typescript
* const ProjectSlugOrId = z.string()
* .toLowerCase()
* .trim()
* .superRefine(validateSlugOrId)
* .describe("Project slug or numeric ID");
*
* const IssueSlugOrId = z.string()
* .trim()
* .superRefine(validateSlugOrId)
* .describe("Issue slug or numeric ID");
* ```
*/
export function validateSlugOrId(val: string, ctx: z.RefinementCtx): void {
// Check if it's a numeric ID
if (isNumericId(val)) {
if (val.length > MAX_ID_LENGTH) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Numeric ID exceeds maximum length of ${MAX_ID_LENGTH} characters`,
});
}
// Numeric IDs don't need slug validation
return;
}
// Otherwise validate as a slug
validateSlug(val, ctx);
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/jvm.json:
--------------------------------------------------------------------------------
```json
{
"namespace": "jvm",
"description": "This document defines Java Virtual machine related attributes.\n",
"attributes": {
"jvm.gc.action": {
"description": "Name of the garbage collector action.",
"type": "string",
"note": "Garbage collector action is generally obtained via [GarbageCollectionNotificationInfo#getGcAction()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcAction()).\n",
"stability": "stable",
"examples": ["end of minor GC", "end of major GC"]
},
"jvm.gc.cause": {
"description": "Name of the garbage collector cause.",
"type": "string",
"note": "Garbage collector cause is generally obtained via [GarbageCollectionNotificationInfo#getGcCause()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcCause()).\n",
"stability": "development",
"examples": ["System.gc()", "Allocation Failure"]
},
"jvm.gc.name": {
"description": "Name of the garbage collector.",
"type": "string",
"note": "Garbage collector name is generally obtained via [GarbageCollectionNotificationInfo#getGcName()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcName()).\n",
"stability": "stable",
"examples": ["G1 Young Generation", "G1 Old Generation"]
},
"jvm.memory.type": {
"description": "The type of memory.",
"type": "string",
"stability": "stable",
"examples": ["heap", "non_heap"]
},
"jvm.memory.pool.name": {
"description": "Name of the memory pool.",
"type": "string",
"note": "Pool names are generally obtained via [MemoryPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/MemoryPoolMXBean.html#getName()).\n",
"stability": "stable",
"examples": ["G1 Old Gen", "G1 Eden space", "G1 Survivor Space"]
},
"jvm.thread.daemon": {
"description": "Whether the thread is daemon or not.",
"type": "boolean",
"stability": "stable"
},
"jvm.thread.state": {
"description": "State of the thread.",
"type": "string",
"stability": "stable",
"examples": [
"new",
"runnable",
"blocked",
"waiting",
"timed_waiting",
"terminated"
]
},
"jvm.buffer.pool.name": {
"description": "Name of the buffer pool.",
"type": "string",
"note": "Pool names are generally obtained via [BufferPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/BufferPoolMXBean.html#getName()).\n",
"stability": "development",
"examples": ["mapped", "direct"]
}
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/index.ts:
--------------------------------------------------------------------------------
```typescript
import * as Sentry from "@sentry/cloudflare";
import OAuthProvider from "@cloudflare/workers-oauth-provider";
import sentryMcpHandler, { SentryMCP } from "./lib/mcp-agent";
import app from "./app";
import { SCOPES } from "../constants";
import type { Env } from "./types";
import getSentryConfig from "./sentry.config";
import { tokenExchangeCallback } from "./oauth";
// required for Durable Objects
export { SentryMCP };
// SentryMCP handles URLPattern-based constraint extraction from request URLs
// and passes context to Durable Objects via headers for org/project scoping.
// Public metadata endpoints that should be accessible from any origin
const PUBLIC_METADATA_PATHS = [
"/.well-known/", // OAuth discovery endpoints
"/robots.txt", // Search engine directives
"/llms.txt", // LLM/AI agent directives
];
const isPublicMetadataEndpoint = (pathname: string): boolean => {
return PUBLIC_METADATA_PATHS.some((path) =>
path.endsWith("/") ? pathname.startsWith(path) : pathname === path,
);
};
const addCorsHeaders = (response: Response): Response => {
const newResponse = new Response(response.body, response);
newResponse.headers.set("Access-Control-Allow-Origin", "*");
newResponse.headers.set("Access-Control-Allow-Methods", "GET, OPTIONS");
newResponse.headers.set("Access-Control-Allow-Headers", "Content-Type");
return newResponse;
};
// Wrap OAuth Provider to add CORS headers for public metadata endpoints
// This is necessary because the OAuth Provider handles some endpoints internally
// (.well-known) without going through our Hono app middleware
const corsWrappedOAuthProvider = {
fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {
// Handle CORS preflight for public metadata endpoints
if (request.method === "OPTIONS") {
const url = new URL(request.url);
if (isPublicMetadataEndpoint(url.pathname)) {
return addCorsHeaders(new Response(null, { status: 204 }));
}
}
const oAuthProvider = new OAuthProvider({
apiRoute: ["/sse", "/mcp"],
apiHandler: sentryMcpHandler,
// @ts-ignore
defaultHandler: app,
// must match the routes registered in `app.ts`
authorizeEndpoint: "/oauth/authorize",
tokenEndpoint: "/oauth/token",
clientRegistrationEndpoint: "/oauth/register",
// @ts-ignore - Environment will be passed as second parameter
tokenExchangeCallback: (options) => tokenExchangeCallback(options, env),
scopesSupported: Object.keys(SCOPES),
});
const response = await oAuthProvider.fetch(request, env, ctx);
// Add CORS headers to public metadata endpoints
const url = new URL(request.url);
if (isPublicMetadataEndpoint(url.pathname)) {
return addCorsHeaders(response);
}
return response;
},
};
export default Sentry.withSentry(
getSentryConfig,
corsWrappedOAuthProvider,
) satisfies ExportedHandler<Env>;
```
--------------------------------------------------------------------------------
/packages/mcp-test-client/src/mcp-test-client.ts:
--------------------------------------------------------------------------------
```typescript
import { experimental_createMCPClient } from "ai";
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { startNewTrace, startSpan } from "@sentry/core";
import { logSuccess } from "./logger.js";
import type { MCPConnection, MCPConfig } from "./types.js";
import { randomUUID } from "node:crypto";
import { LIB_VERSION } from "./version.js";
export async function connectToMCPServer(
config: MCPConfig,
): Promise<MCPConnection> {
const sessionId = randomUUID();
return await startNewTrace(async () => {
return await startSpan(
{
name: "mcp.connect/stdio",
attributes: {
"mcp.transport": "stdio",
"gen_ai.conversation.id": sessionId,
"service.version": LIB_VERSION,
},
},
async (span) => {
try {
const args = [
`--access-token=${config.accessToken}`,
"--all-scopes", // Ensure all tools are available in local stdio runs
];
if (config.host) {
args.push(`--host=${config.host}`);
}
if (config.sentryDsn) {
args.push(`--sentry-dsn=${config.sentryDsn}`);
}
// Resolve the path to the mcp-server binary
const __dirname = dirname(fileURLToPath(import.meta.url));
const mcpServerPath = join(
__dirname,
"../../mcp-server/dist/index.js",
);
const transport = new Experimental_StdioMCPTransport({
command: "node",
args: [mcpServerPath, ...args],
env: {
...process.env,
SENTRY_ACCESS_TOKEN: config.accessToken,
SENTRY_HOST: config.host || "sentry.io",
...(config.sentryDsn && { SENTRY_DSN: config.sentryDsn }),
},
});
const client = await experimental_createMCPClient({
name: "mcp.sentry.dev (test-client)",
transport,
});
// Discover available tools
const toolsMap = await client.tools();
const tools = new Map<string, any>();
for (const [name, tool] of Object.entries(toolsMap)) {
tools.set(name, tool);
}
// Remove custom attributes - let SDK handle standard attributes
span.setStatus({ code: 1 }); // OK status
logSuccess(
"Connected to MCP server (stdio)",
`${tools.size} tools available`,
);
const disconnect = async () => {
await client.close();
};
return {
client,
tools,
disconnect,
sessionId,
transport: "stdio" as const,
};
} catch (error) {
span.setStatus({ code: 2 }); // Error status
throw error;
}
},
);
});
}
```
--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/tool-helpers/api.ts:
--------------------------------------------------------------------------------
```typescript
import {
SentryApiService,
ApiClientError,
ApiNotFoundError,
} from "../../api-client/index";
import { UserInputError } from "../../errors";
import type { ServerContext } from "../../types";
import { validateRegionUrl } from "./validate-region-url";
/**
* Create a Sentry API service from server context with optional region override
* @param context - Server context containing host and access token
* @param opts - Options object containing optional regionUrl override
* @returns Configured SentryApiService instance (always uses HTTPS)
* @throws {UserInputError} When regionUrl is provided but invalid
*/
export function apiServiceFromContext(
context: ServerContext,
opts: { regionUrl?: string } = {},
) {
let host = context.sentryHost;
if (opts.regionUrl?.trim()) {
// Validate the regionUrl against the base host to prevent SSRF
// Use default host if context.sentryHost is not set
const baseHost = context.sentryHost || "sentry.io";
host = validateRegionUrl(opts.regionUrl.trim(), baseHost);
}
return new SentryApiService({
host,
accessToken: context.accessToken,
});
}
/**
* Maps API errors to user-friendly errors based on context
* @param error - The error to handle
* @param params - The parameters that were used in the API call
* @returns Never - always throws an error
* @throws {UserInputError} For 4xx errors that are likely user input issues
* @throws {Error} For other errors
*/
export function handleApiError(
error: unknown,
params?: Record<string, unknown>,
): never {
// Use the new error hierarchy - all 4xx errors extend ApiClientError
if (error instanceof ApiClientError) {
let message = `API error (${error.status}): ${error.message}`;
// Special handling for 404s with parameter context
if (error instanceof ApiNotFoundError && params) {
const paramsList: string[] = [];
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== "") {
paramsList.push(`${key}: '${value}'`);
}
}
if (paramsList.length > 0) {
message = `Resource not found (404): ${error.message}\nPlease verify these parameters are correct:\n${paramsList.map((p) => ` - ${p}`).join("\n")}`;
}
}
throw new UserInputError(message, { cause: error });
}
// All other errors bubble up (including ApiServerError for 5xx)
throw error;
}
/**
* Wraps an async API call with automatic error handling
* @param fn - The async function to execute
* @param params - The parameters that were used in the API call
* @returns The result of the function
* @throws {UserInputError} For user input errors
* @throws {Error} For other errors
*/
export async function withApiErrorHandling<T>(
fn: () => Promise<T>,
params?: Record<string, unknown>,
): Promise<T> {
try {
return await fn();
} catch (error) {
handleApiError(error, params);
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/chat/chat-input.tsx:
--------------------------------------------------------------------------------
```typescript
import { useEffect, useRef } from "react";
import { Send, CircleStop } from "lucide-react";
import { Button } from "../ui/button";
interface ChatInputProps {
input: string;
isLoading: boolean;
isOpen: boolean;
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
onStop: () => void;
onSlashCommand?: (command: string) => void;
}
export function ChatInput({
input,
isLoading,
isOpen,
onInputChange,
onSubmit,
onStop,
onSlashCommand,
}: ChatInputProps) {
const inputRef = useRef<HTMLInputElement>(null);
// Focus when dialog opens (with delay for mobile animation)
useEffect(() => {
if (isOpen) {
// Add delay to ensure the slide-in animation completes on mobile
const timer = setTimeout(() => {
// Use requestAnimationFrame to ensure browser has finished layout
requestAnimationFrame(() => {
if (inputRef.current && !inputRef.current.disabled) {
inputRef.current.focus({ preventScroll: false });
}
});
}, 600); // Delay to account for 500ms animation
return () => clearTimeout(timer);
}
}, [isOpen]);
// Re-focus when loading finishes
useEffect(() => {
if (inputRef.current && !isLoading && isOpen) {
inputRef.current.focus();
}
}, [isLoading, isOpen]);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Check if input is a slash command
if (input.startsWith("/") && onSlashCommand) {
const command = input.slice(1).toLowerCase().trim();
// Pass all slash commands to the handler, let it decide what to do
onSlashCommand(command);
return;
}
// Otherwise, submit normally
onSubmit(e);
};
return (
<form onSubmit={handleSubmit} className="relative flex-1">
<div className="relative">
<input
ref={inputRef}
value={input}
onChange={onInputChange}
placeholder="Ask me anything about your Sentry data..."
disabled={isLoading}
className="w-full p-4 pr-12 rounded bg-slate-800/50 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-300 focus:border-transparent disabled:opacity-50"
/>
<Button
type={isLoading ? "button" : "submit"}
variant="ghost"
onClick={isLoading ? onStop : undefined}
disabled={!isLoading && !input.trim()}
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-slate-400 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:text-slate-400 disabled:hover:bg-transparent transition-colors"
title={isLoading ? "Stop generation" : "Send message"}
>
{isLoading ? (
<CircleStop className="h-4 w-4" />
) : (
<Send className="h-4 w-4" />
)}
</Button>
</div>
</form>
);
}
```