#
tokens: 48553/50000 32/422 files (page 4/15)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 15. Use http://codebase.md/getsentry/sentry-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .claude
│   ├── agents
│   │   └── claude-optimizer.md
│   ├── commands
│   │   ├── gh-pr.md
│   │   └── gh-review.md
│   └── settings.json
├── .craft.yml
├── .cursor
│   └── mcp.json
├── .env.example
├── .github
│   └── workflows
│       ├── deploy.yml
│       ├── eval.yml
│       ├── merge-jobs.yml
│       ├── release.yml
│       ├── smoke-tests.yml
│       ├── test.yml
│       └── token-cost.yml
├── .gitignore
├── .mcp.json
├── .vscode
│   ├── extensions.json
│   ├── mcp.json
│   └── settings.json
├── AGENTS.md
├── benchmark-agent.sh
├── bin
│   └── bump-version.sh
├── biome.json
├── CLAUDE.md
├── codecov.yml
├── core
├── docs
│   ├── adding-tools.mdc
│   ├── api-patterns.mdc
│   ├── architecture.mdc
│   ├── cloudflare
│   │   ├── architecture.md
│   │   ├── oauth-architecture.md
│   │   └── overview.md
│   ├── coding-guidelines.mdc
│   ├── common-patterns.mdc
│   ├── cursor.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
│   ├── releases
│   │   ├── cloudflare.mdc
│   │   └── stdio.mdc
│   ├── search-events-api-patterns.md
│   ├── security.mdc
│   ├── specs
│   │   ├── README.md
│   │   ├── search-events.md
│   │   └── subpath-constraints.md
│   ├── testing-remote.md
│   ├── testing-stdio.md
│   ├── testing.mdc
│   └── token-cost-tracking.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
│   │   │   │   │       ├── badge.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-endpoint-mode.ts
│   │   │   │   │   ├── 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-handler.test.ts
│   │   │   │   │   ├── mcp-handler.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
│   │   │   └── measure-token-cost.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-context.test.ts
│   │   │   ├── server.ts
│   │   │   ├── telem
│   │   │   │   ├── index.ts
│   │   │   │   ├── logging.ts
│   │   │   │   ├── sentry.test.ts
│   │   │   │   └── sentry.ts
│   │   │   ├── test-setup.ts
│   │   │   ├── test-utils
│   │   │   │   └── context.ts
│   │   │   ├── toolDefinitions.ts
│   │   │   ├── tools
│   │   │   │   ├── agent-tools.ts
│   │   │   │   ├── 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
│   │   │   │   ├── use-sentry
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── handler.test.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── tool-wrapper.test.ts
│   │   │   │   │   └── tool-wrapper.ts
│   │   │   │   ├── whoami.test.ts
│   │   │   │   └── whoami.ts
│   │   │   ├── transports
│   │   │   │   └── stdio.ts
│   │   │   ├── types.ts
│   │   │   ├── utils
│   │   │   │   ├── slug-validation.test.ts
│   │   │   │   ├── slug-validation.ts
│   │   │   │   ├── url-utils.test.ts
│   │   │   │   └── url-utils.ts
│   │   │   └── version.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   ├── mcp-server-evals
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── bin
│   │   │   │   └── start-mock-stdio.ts
│   │   │   ├── evals
│   │   │   │   ├── autofix.eval.ts
│   │   │   │   ├── create-dsn.eval.ts
│   │   │   │   ├── create-project.eval.ts
│   │   │   │   ├── create-team.eval.ts
│   │   │   │   ├── get-issue.eval.ts
│   │   │   │   ├── get-trace-details.eval.ts
│   │   │   │   ├── list-dsns.eval.ts
│   │   │   │   ├── list-issues.eval.ts
│   │   │   │   ├── list-organizations.eval.ts
│   │   │   │   ├── list-projects.eval.ts
│   │   │   │   ├── list-releases.eval.ts
│   │   │   │   ├── list-tags.eval.ts
│   │   │   │   ├── list-teams.eval.ts
│   │   │   │   ├── search-docs.eval.ts
│   │   │   │   ├── search-events-agent.eval.ts
│   │   │   │   ├── search-events.eval.ts
│   │   │   │   ├── search-issues-agent.eval.ts
│   │   │   │   ├── search-issues.eval.ts
│   │   │   │   ├── update-issue.eval.ts
│   │   │   │   ├── update-project.eval.ts
│   │   │   │   └── utils
│   │   │   │       ├── fixtures.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── runner.ts
│   │   │   │       ├── structuredOutputScorer.ts
│   │   │   │       └── toolPredictionScorer.ts
│   │   │   └── setup-env.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── mcp-server-mocks
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── fixtures
│   │   │   │   ├── autofix-state.json
│   │   │   │   ├── event-attachments.json
│   │   │   │   ├── event.json
│   │   │   │   ├── issue.json
│   │   │   │   ├── performance-event.json
│   │   │   │   ├── project.json
│   │   │   │   ├── tags.json
│   │   │   │   ├── team.json
│   │   │   │   ├── trace-event.json
│   │   │   │   ├── trace-items-attributes-logs-number.json
│   │   │   │   ├── trace-items-attributes-logs-string.json
│   │   │   │   ├── trace-items-attributes-spans-number.json
│   │   │   │   ├── trace-items-attributes-spans-string.json
│   │   │   │   ├── trace-items-attributes.json
│   │   │   │   ├── trace-meta-with-nulls.json
│   │   │   │   ├── trace-meta.json
│   │   │   │   ├── trace-mixed.json
│   │   │   │   └── trace.json
│   │   │   ├── index.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── mcp-server-tsconfig
│   │   ├── package.json
│   │   ├── tsconfig.base.json
│   │   └── tsconfig.vite.json
│   ├── mcp-test-client
│   │   ├── .env.test
│   │   ├── .gitignore
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── agent.ts
│   │   │   ├── auth
│   │   │   │   ├── config.ts
│   │   │   │   └── oauth.ts
│   │   │   ├── constants.ts
│   │   │   ├── index.ts
│   │   │   ├── logger.test.ts
│   │   │   ├── logger.ts
│   │   │   ├── mcp-test-client-remote.ts
│   │   │   ├── mcp-test-client.ts
│   │   │   ├── types.ts
│   │   │   └── version.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── smoke-tests
│       ├── package.json
│       ├── src
│       │   └── smoke.test.ts
│       └── vitest.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── scripts
│   └── check-doc-links.mjs
├── turbo.json
└── vitest.workspace.ts
```

# Files

--------------------------------------------------------------------------------
/packages/mcp-server/src/telem/sentry.ts:
--------------------------------------------------------------------------------

```typescript
  1 | interface ScrubPattern {
  2 |   pattern: RegExp;
  3 |   replacement: string;
  4 |   description: string;
  5 | }
  6 | 
  7 | // Patterns for sensitive data that should be scrubbed
  8 | // Pre-compile patterns with global flag for replacement
  9 | const SCRUB_PATTERNS: ScrubPattern[] = [
 10 |   {
 11 |     pattern: /\bsk-[a-zA-Z0-9]{48}\b/g,
 12 |     replacement: "[REDACTED_OPENAI_KEY]",
 13 |     description: "OpenAI API key",
 14 |   },
 15 |   {
 16 |     pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+={0,}/g,
 17 |     replacement: "Bearer [REDACTED_TOKEN]",
 18 |     description: "Bearer token",
 19 |   },
 20 |   {
 21 |     pattern: /\bsntrys_[a-zA-Z0-9_]+\b/g,
 22 |     replacement: "[REDACTED_SENTRY_TOKEN]",
 23 |     description: "Sentry access token",
 24 |   },
 25 | ];
 26 | 
 27 | // Maximum depth for recursive scrubbing to prevent stack overflow
 28 | const MAX_SCRUB_DEPTH = 20;
 29 | 
 30 | /**
 31 |  * Recursively scrub sensitive data from any value.
 32 |  * Returns tuple of [scrubbedValue, didScrub, descriptionsOfMatchedPatterns]
 33 |  */
 34 | function scrubValue(value: unknown, depth = 0): [unknown, boolean, string[]] {
 35 |   // Prevent stack overflow by limiting recursion depth
 36 |   if (depth >= MAX_SCRUB_DEPTH) {
 37 |     return ["[MAX_DEPTH_EXCEEDED]", false, []];
 38 |   }
 39 | 
 40 |   if (typeof value === "string") {
 41 |     let scrubbed = value;
 42 |     let didScrub = false;
 43 |     const matchedDescriptions: string[] = [];
 44 | 
 45 |     for (const { pattern, replacement, description } of SCRUB_PATTERNS) {
 46 |       // Reset lastIndex to avoid stateful regex issues
 47 |       pattern.lastIndex = 0;
 48 |       if (pattern.test(scrubbed)) {
 49 |         didScrub = true;
 50 |         matchedDescriptions.push(description);
 51 |         // Reset again before replace
 52 |         pattern.lastIndex = 0;
 53 |         scrubbed = scrubbed.replace(pattern, replacement);
 54 |       }
 55 |     }
 56 |     return [scrubbed, didScrub, matchedDescriptions];
 57 |   }
 58 | 
 59 |   if (Array.isArray(value)) {
 60 |     let arrayDidScrub = false;
 61 |     const arrayDescriptions: string[] = [];
 62 |     const scrubbedArray = value.map((item) => {
 63 |       const [scrubbed, didScrub, descriptions] = scrubValue(item, depth + 1);
 64 |       if (didScrub) {
 65 |         arrayDidScrub = true;
 66 |         arrayDescriptions.push(...descriptions);
 67 |       }
 68 |       return scrubbed;
 69 |     });
 70 |     return [scrubbedArray, arrayDidScrub, arrayDescriptions];
 71 |   }
 72 | 
 73 |   if (value && typeof value === "object") {
 74 |     let objectDidScrub = false;
 75 |     const objectDescriptions: string[] = [];
 76 |     const scrubbed: Record<string, unknown> = {};
 77 |     for (const [key, val] of Object.entries(value)) {
 78 |       const [scrubbedVal, didScrub, descriptions] = scrubValue(val, depth + 1);
 79 |       if (didScrub) {
 80 |         objectDidScrub = true;
 81 |         objectDescriptions.push(...descriptions);
 82 |       }
 83 |       scrubbed[key] = scrubbedVal;
 84 |     }
 85 |     return [scrubbed, objectDidScrub, objectDescriptions];
 86 |   }
 87 | 
 88 |   return [value, false, []];
 89 | }
 90 | 
 91 | /**
 92 |  * Sentry beforeSend hook that scrubs sensitive data from events
 93 |  */
 94 | export function sentryBeforeSend(event: any, hint: any): any {
 95 |   // Always scrub the entire event
 96 |   const [scrubbedEvent, didScrub, descriptions] = scrubValue(event);
 97 | 
 98 |   // Log to console if we found and scrubbed sensitive data
 99 |   // (avoiding LogTape dependency for edge/browser compatibility)
100 |   if (didScrub) {
101 |     const uniqueDescriptions = [...new Set(descriptions)];
102 |     console.warn(
103 |       `[Sentry] Event contained sensitive data: ${uniqueDescriptions.join(", ")}`,
104 |     );
105 |   }
106 | 
107 |   return scrubbedEvent as any;
108 | }
109 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/scripts/generate-definitions.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env tsx
  2 | /**
  3 |  * Generate tool definitions JSON for external consumption.
  4 |  *
  5 |  * Outputs to src/ so they can be bundled and imported by clients and the Cloudflare app.
  6 |  */
  7 | import * as fs from "node:fs";
  8 | import * as path from "node:path";
  9 | import { fileURLToPath } from "node:url";
 10 | import { z, type ZodTypeAny } from "zod";
 11 | import { zodToJsonSchema } from "zod-to-json-schema";
 12 | 
 13 | const __filename = fileURLToPath(import.meta.url);
 14 | const __dirname = path.dirname(__filename);
 15 | 
 16 | // Lazy imports of server modules to avoid type bleed
 17 | const toolsModule = await import("../src/tools/index.ts");
 18 | 
 19 | function writeJson(file: string, data: unknown) {
 20 |   fs.writeFileSync(file, JSON.stringify(data, null, 2));
 21 | }
 22 | 
 23 | function ensureDirExists(dir: string) {
 24 |   if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
 25 | }
 26 | 
 27 | // Shared helpers for Zod parameter maps
 28 | function zodFieldMapToDescriptions(
 29 |   fieldMap: Record<string, ZodTypeAny>,
 30 | ): Record<string, { description: string }> {
 31 |   const out: Record<string, { description: string }> = {};
 32 |   for (const [key, schema] of Object.entries(fieldMap)) {
 33 |     const js = zodToJsonSchema(schema, { $refStrategy: "none" }) as {
 34 |       description?: string;
 35 |     };
 36 |     out[key] = { description: js.description || "" };
 37 |   }
 38 |   return out;
 39 | }
 40 | 
 41 | function zodFieldMapToJsonSchema(
 42 |   fieldMap: Record<string, ZodTypeAny>,
 43 | ): unknown {
 44 |   if (!fieldMap || Object.keys(fieldMap).length === 0) return {};
 45 |   const obj = z.object(fieldMap);
 46 |   return zodToJsonSchema(obj, { $refStrategy: "none" });
 47 | }
 48 | 
 49 | function byName<T extends { name: string }>(a: T, b: T) {
 50 |   return a.name.localeCompare(b.name);
 51 | }
 52 | 
 53 | // Tools
 54 | function generateToolDefinitions() {
 55 |   const toolsDefault = toolsModule.default as
 56 |     | Record<string, unknown>
 57 |     | undefined;
 58 |   if (!toolsDefault || typeof toolsDefault !== "object") {
 59 |     throw new Error("Failed to import tools from src/tools/index.ts");
 60 |   }
 61 | 
 62 |   const defs = Object.entries(toolsDefault).map(([key, tool]) => {
 63 |     if (!tool || typeof tool !== "object")
 64 |       throw new Error(`Invalid tool: ${key}`);
 65 |     const t = tool as {
 66 |       name: string;
 67 |       description: string;
 68 |       inputSchema: Record<string, ZodTypeAny>;
 69 |       requiredScopes: string[]; // must exist on all tools (can be empty)
 70 |     };
 71 |     if (!Array.isArray(t.requiredScopes)) {
 72 |       throw new Error(`Tool '${t.name}' is missing requiredScopes array`);
 73 |     }
 74 |     const jsonSchema = zodFieldMapToJsonSchema(t.inputSchema || {});
 75 |     return {
 76 |       name: t.name,
 77 |       description: t.description,
 78 |       // Export full JSON Schema under inputSchema for external docs
 79 |       inputSchema: jsonSchema,
 80 |       // Preserve tool access requirements for UIs/docs
 81 |       requiredScopes: t.requiredScopes,
 82 |     };
 83 |   });
 84 |   return defs.sort(byName);
 85 | }
 86 | 
 87 | async function main() {
 88 |   try {
 89 |     console.log("Generating tool definitions...");
 90 |     const outDir = path.join(__dirname, "../src");
 91 |     ensureDirExists(outDir);
 92 | 
 93 |     const tools = generateToolDefinitions();
 94 | 
 95 |     writeJson(path.join(outDir, "toolDefinitions.json"), tools);
 96 | 
 97 |     console.log(`✅ Generated: tools(${tools.length})`);
 98 |   } catch (error) {
 99 |     const err = error as Error;
100 |     console.error("[ERROR]", err.message, err.stack);
101 |     process.exit(1);
102 |   }
103 | }
104 | 
105 | if (import.meta.url === `file://${process.argv[1]}`) {
106 |   void main();
107 | }
108 | 
```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/lib/constraint-utils.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import "urlpattern-polyfill";
  3 | import { verifyConstraintsAccess } from "./constraint-utils";
  4 | 
  5 | describe("verifyConstraintsAccess", () => {
  6 |   const token = "test-token";
  7 |   const host = "sentry.io";
  8 | 
  9 |   it("returns ok with empty constraints when no org constraint provided", async () => {
 10 |     const result = await verifyConstraintsAccess(
 11 |       { organizationSlug: null, projectSlug: null },
 12 |       { accessToken: token, sentryHost: host },
 13 |     );
 14 |     expect(result).toEqual({
 15 |       ok: true,
 16 |       constraints: {
 17 |         organizationSlug: null,
 18 |         projectSlug: null,
 19 |         regionUrl: null,
 20 |       },
 21 |     });
 22 |   });
 23 | 
 24 |   it("fails when access token is missing, null, undefined, or empty", async () => {
 25 |     const testCases = [
 26 |       { accessToken: "", label: "empty" },
 27 |       { accessToken: null, label: "null" },
 28 |       { accessToken: undefined, label: "undefined" },
 29 |     ];
 30 | 
 31 |     for (const { accessToken, label } of testCases) {
 32 |       const result = await verifyConstraintsAccess(
 33 |         { organizationSlug: "org", projectSlug: null },
 34 |         { accessToken, sentryHost: host },
 35 |       );
 36 |       expect(result.ok).toBe(false);
 37 |       if (!result.ok) {
 38 |         expect(result.status).toBe(401);
 39 |         expect(result.message).toBe(
 40 |           "Missing access token for constraint verification",
 41 |         );
 42 |       }
 43 |     }
 44 |   });
 45 | 
 46 |   it("successfully verifies org access and returns constraints with regionUrl", async () => {
 47 |     const result = await verifyConstraintsAccess(
 48 |       { organizationSlug: "sentry-mcp-evals", projectSlug: null },
 49 |       { accessToken: token, sentryHost: host },
 50 |     );
 51 |     expect(result.ok).toBe(true);
 52 |     if (result.ok) {
 53 |       expect(result.constraints).toEqual({
 54 |         organizationSlug: "sentry-mcp-evals",
 55 |         projectSlug: null,
 56 |         regionUrl: "https://us.sentry.io",
 57 |       });
 58 |     }
 59 |   });
 60 | 
 61 |   it("successfully verifies org and project access", async () => {
 62 |     const result = await verifyConstraintsAccess(
 63 |       { organizationSlug: "sentry-mcp-evals", projectSlug: "cloudflare-mcp" },
 64 |       { accessToken: token, sentryHost: host },
 65 |     );
 66 |     expect(result.ok).toBe(true);
 67 |     if (result.ok) {
 68 |       expect(result.constraints).toEqual({
 69 |         organizationSlug: "sentry-mcp-evals",
 70 |         projectSlug: "cloudflare-mcp",
 71 |         regionUrl: "https://us.sentry.io",
 72 |       });
 73 |     }
 74 |   });
 75 | 
 76 |   it("fails when org does not exist", async () => {
 77 |     const result = await verifyConstraintsAccess(
 78 |       { organizationSlug: "nonexistent-org", projectSlug: null },
 79 |       { accessToken: token, sentryHost: host },
 80 |     );
 81 |     expect(result.ok).toBe(false);
 82 |     if (!result.ok) {
 83 |       expect(result.status).toBe(404);
 84 |       expect(result.message).toBe("Organization 'nonexistent-org' not found");
 85 |     }
 86 |   });
 87 | 
 88 |   it("fails when project does not exist", async () => {
 89 |     const result = await verifyConstraintsAccess(
 90 |       {
 91 |         organizationSlug: "sentry-mcp-evals",
 92 |         projectSlug: "nonexistent-project",
 93 |       },
 94 |       { accessToken: token, sentryHost: host },
 95 |     );
 96 |     expect(result.ok).toBe(false);
 97 |     if (!result.ok) {
 98 |       expect(result.status).toBe(404);
 99 |       expect(result.message).toBe(
100 |         "Project 'nonexistent-project' not found in organization 'sentry-mcp-evals'",
101 |       );
102 |     }
103 |   });
104 | });
105 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server-evals/src/evals/search-issues-agent.eval.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describeEval } from "vitest-evals";
  2 | import { ToolCallScorer } from "vitest-evals";
  3 | import { searchIssuesAgent } from "@sentry/mcp-server/tools/search-issues/agent";
  4 | import { SentryApiService } from "@sentry/mcp-server/api-client";
  5 | import { StructuredOutputScorer } from "./utils/structuredOutputScorer";
  6 | import "../setup-env";
  7 | 
  8 | // The shared MSW server is already started in setup-env.ts
  9 | 
 10 | describeEval("search-issues-agent", {
 11 |   data: async () => {
 12 |     return [
 13 |       {
 14 |         // Simple query with common fields - should NOT require tool calls
 15 |         input: "Show me unresolved issues",
 16 |         expectedTools: [],
 17 |         expected: {
 18 |           query: "is:unresolved",
 19 |           sort: "date", // Agent uses "date" as default
 20 |         },
 21 |       },
 22 |       {
 23 |         // Query with "me" reference - should only require whoami
 24 |         input: "Show me issues assigned to me",
 25 |         expectedTools: [
 26 |           {
 27 |             name: "whoami",
 28 |             arguments: {},
 29 |           },
 30 |         ],
 31 |         expected: {
 32 |           query:
 33 |             /assignedOrSuggested:test@example\.com|assigned:test@example\.com|assigned:me/, // Various valid forms
 34 |           sort: "date",
 35 |         },
 36 |       },
 37 |       {
 38 |         // Complex query but with common fields - should NOT require tool calls
 39 |         // NOTE: AI often incorrectly uses firstSeen instead of lastSeen - known limitation
 40 |         input: "Show me critical unhandled errors from the last 24 hours",
 41 |         expectedTools: [],
 42 |         expected: {
 43 |           query: /level:error.*is:unresolved.*lastSeen:-24h/,
 44 |           sort: "date",
 45 |         },
 46 |       },
 47 |       {
 48 |         // Query with custom/uncommon field that would require discovery
 49 |         input: "Show me issues with custom.payment.failed tag",
 50 |         expectedTools: [
 51 |           {
 52 |             name: "issueFields",
 53 |             arguments: {}, // No arguments needed anymore
 54 |           },
 55 |         ],
 56 |         expected: {
 57 |           query: /custom\.payment\.failed|tags\[custom\.payment\.failed\]/, // Both syntaxes are valid for tags
 58 |           sort: "date", // Agent should always return a sort value
 59 |         },
 60 |       },
 61 |       {
 62 |         // Another query requiring field discovery
 63 |         input: "Find issues where the kafka.consumer.group is orders-processor",
 64 |         expectedTools: [
 65 |           {
 66 |             name: "issueFields",
 67 |             arguments: {}, // No arguments needed anymore
 68 |           },
 69 |         ],
 70 |         expected: {
 71 |           query: "kafka.consumer.group:orders-processor",
 72 |           sort: "date", // Agent should always return a sort value
 73 |         },
 74 |       },
 75 |     ];
 76 |   },
 77 |   task: async (input) => {
 78 |     // Create a real API service that will use MSW mocks
 79 |     const apiService = new SentryApiService({
 80 |       accessToken: "test-token",
 81 |     });
 82 | 
 83 |     const agentResult = await searchIssuesAgent({
 84 |       query: input,
 85 |       organizationSlug: "sentry-mcp-evals",
 86 |       apiService,
 87 |     });
 88 | 
 89 |     // Return in the format expected by ToolCallScorer
 90 |     return {
 91 |       result: JSON.stringify(agentResult.result),
 92 |       toolCalls: agentResult.toolCalls.map((call: any) => ({
 93 |         name: call.toolName,
 94 |         arguments: call.args,
 95 |       })),
 96 |     };
 97 |   },
 98 |   scorers: [
 99 |     ToolCallScorer(), // Validates tool calls
100 |     StructuredOutputScorer({ match: "fuzzy" }), // Validates the structured query output with flexible matching
101 |   ],
102 | });
103 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/search-issues/config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Configuration for the search-issues agent
 3 |  */
 4 | 
 5 | export const systemPrompt = `You are a Sentry issue search query translator. Convert natural language queries to Sentry issue search syntax.
 6 | 
 7 | IMPORTANT RULES:
 8 | 1. Use Sentry issue search syntax, NOT SQL
 9 | 2. Time ranges use relative notation: -24h, -7d, -30d
10 | 3. Comparisons: >, <, >=, <=
11 | 4. Boolean operators: AND, OR, NOT (or !)
12 | 5. Field values with spaces need quotes: environment:"dev server"
13 | 
14 | BUILT-IN FIELDS:
15 | - is: Issue status (unresolved, resolved, ignored, archived)
16 | - level: Severity level (error, warning, info, debug, fatal)
17 |   IMPORTANT: Almost NEVER use this field. Terms like "critical", "important", "severe" refer to IMPACT not level.
18 |   Only use if user explicitly says "error level", "warning level", etc.
19 | - environment: Deployment environment (production, staging, development)
20 | - release: Version/release identifier
21 | - firstSeen: When the issue was FIRST encountered (use for "new issues", "started", "began")
22 |   WARNING: Excludes ongoing issues that started before the time window
23 | - lastSeen: When the issue was LAST encountered (use for "from the last", "recent", "active")
24 |   This includes ALL issues seen during the time window, regardless of when they started
25 | - assigned: Issues explicitly assigned to a user (email or "me")  
26 | - assignedOrSuggested: Issues assigned to OR suggested for a user (broader match)
27 | - userCount: Number of unique users affected
28 | - eventCount: Total number of events
29 | 
30 | COMMON QUERY PATTERNS:
31 | - Unresolved issues: is:unresolved (NO level filter unless explicitly requested)
32 | - Critical/important issues: is:unresolved with sort:freq or sort:user (NOT level:error)
33 | - Recent activity: lastSeen:-24h
34 | - New issues: firstSeen:-7d
35 | - High impact: userCount:>100
36 | - My work: assignedOrSuggested:me
37 | 
38 | SORTING RULES:
39 | 1. CRITICAL: Sort MUST go in the separate "sort" field, NEVER in the "query" field
40 |    - WRONG: query: "is:unresolved sort:user" ← Sort syntax in query field is FORBIDDEN
41 |    - CORRECT: query: "is:unresolved", sort: "user" ← Sort in separate field
42 | 
43 | 2. AVAILABLE SORT OPTIONS:
44 |    - date: Last seen (default)
45 |    - freq: Event frequency  
46 |    - new: First seen
47 |    - user: User count
48 | 
49 | 3. IMPORTANT: Query field is for filtering only (is:, level:, environment:, etc.)
50 | 
51 | 'ME' REFERENCES:
52 | - When the user says "assigned to me" or similar, you MUST use the whoami tool to get the current user's email
53 | - Replace "me" with the actual email address in the query
54 | - Example: "assigned to me" → use whoami tool → assignedOrSuggested:[email protected]
55 | 
56 | EXAMPLES:
57 | "critical bugs" → query: "level:error is:unresolved", sort: "date"
58 | "worst issues affecting the most users" → query: "is:unresolved", sort: "user"
59 | "assigned to [email protected]" → query: "assignedOrSuggested:[email protected]", sort: "date"
60 | 
61 | NEVER: query: "is:unresolved sort:user" ← Sort goes in separate field!
62 | 
63 | CRITICAL - TOOL RESPONSE HANDLING:
64 | All tools return responses in this format: {error?: string, result?: data}
65 | - If 'error' is present: The tool failed - analyze the error message and potentially retry with corrections
66 | - If 'result' is present: The tool succeeded - use the result data for your query construction
67 | - Always check for errors before using results
68 | 
69 | Always use the issueFields tool to discover available fields when needed.
70 | Use the whoami tool when you need to resolve 'me' references.`;
71 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/create-project.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import { setTag } from "@sentry/core";
  3 | import { defineTool } from "../internal/tool-helpers/define";
  4 | import { apiServiceFromContext } from "../internal/tool-helpers/api";
  5 | import { logIssue } from "../telem/logging";
  6 | import type { ServerContext } from "../types";
  7 | import type { ClientKey } from "../api-client/index";
  8 | import {
  9 |   ParamOrganizationSlug,
 10 |   ParamRegionUrl,
 11 |   ParamTeamSlug,
 12 |   ParamPlatform,
 13 | } from "../schema";
 14 | 
 15 | export default defineTool({
 16 |   name: "create_project",
 17 |   requiredScopes: ["project:write", "team:read"],
 18 |   description: [
 19 |     "Create a new project in Sentry (includes DSN automatically).",
 20 |     "",
 21 |     "🔍 USE THIS TOOL WHEN USERS WANT TO:",
 22 |     "- 'Create a new project'",
 23 |     "- 'Set up a project for [app/service] with team [X]'",
 24 |     "- 'I need a new Sentry project'",
 25 |     "- Create project AND need DSN in one step",
 26 |     "",
 27 |     "❌ DO NOT USE create_dsn after this - DSN is included in output.",
 28 |     "",
 29 |     "Be careful when using this tool!",
 30 |     "",
 31 |     "<examples>",
 32 |     "### Create new project with team",
 33 |     "```",
 34 |     "create_project(organizationSlug='my-organization', teamSlug='my-team', name='my-project', platform='javascript')",
 35 |     "```",
 36 |     "</examples>",
 37 |     "",
 38 |     "<hints>",
 39 |     "- If the user passes a parameter in the form of name/otherName, its likely in the format of <organizationSlug>/<teamSlug>.",
 40 |     "- If any parameter is ambiguous, you should clarify with the user what they meant.",
 41 |     "</hints>",
 42 |   ].join("\n"),
 43 |   inputSchema: {
 44 |     organizationSlug: ParamOrganizationSlug,
 45 |     regionUrl: ParamRegionUrl.optional(),
 46 |     teamSlug: ParamTeamSlug,
 47 |     name: z
 48 |       .string()
 49 |       .trim()
 50 |       .describe(
 51 |         "The name of the project to create. Typically this is commonly the name of the repository or service. It is only used as a visual label in Sentry.",
 52 |       ),
 53 |     platform: ParamPlatform.optional(),
 54 |   },
 55 |   annotations: {
 56 |     readOnlyHint: false,
 57 |     destructiveHint: false,
 58 |     openWorldHint: true,
 59 |   },
 60 |   async handler(params, context: ServerContext) {
 61 |     const apiService = apiServiceFromContext(context, {
 62 |       regionUrl: params.regionUrl,
 63 |     });
 64 |     const organizationSlug = params.organizationSlug;
 65 | 
 66 |     setTag("organization.slug", organizationSlug);
 67 |     setTag("team.slug", params.teamSlug);
 68 | 
 69 |     const project = await apiService.createProject({
 70 |       organizationSlug,
 71 |       teamSlug: params.teamSlug,
 72 |       name: params.name,
 73 |       platform: params.platform,
 74 |     });
 75 |     let clientKey: ClientKey | null = null;
 76 |     try {
 77 |       clientKey = await apiService.createClientKey({
 78 |         organizationSlug,
 79 |         projectSlug: project.slug,
 80 |         name: "Default",
 81 |       });
 82 |     } catch (err) {
 83 |       logIssue(err);
 84 |     }
 85 |     let output = `# New Project in **${organizationSlug}**\n\n`;
 86 |     output += `**ID**: ${project.id}\n`;
 87 |     output += `**Slug**: ${project.slug}\n`;
 88 |     output += `**Name**: ${project.name}\n`;
 89 |     if (clientKey) {
 90 |       output += `**SENTRY_DSN**: ${clientKey?.dsn.public}\n\n`;
 91 |     } else {
 92 |       output += "**SENTRY_DSN**: There was an error fetching this value.\n\n";
 93 |     }
 94 |     output += "# Using this information\n\n";
 95 |     output += `- You can reference the **SENTRY_DSN** value to initialize Sentry's SDKs.\n`;
 96 |     output += `- You should always inform the user of the **SENTRY_DSN** and Project Slug values.\n`;
 97 |     return output;
 98 |   },
 99 | });
100 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/constants.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Constants for Sentry MCP server.
  3 |  *
  4 |  * Defines platform and framework combinations available in Sentry documentation.
  5 |  */
  6 | 
  7 | /**
  8 |  * MCP Server identification
  9 |  */
 10 | export const MCP_SERVER_NAME = "Sentry MCP" as const;
 11 | 
 12 | /**
 13 |  * Allowed region domains for sentry.io
 14 |  * Only these specific domains are permitted when using Sentry's cloud service
 15 |  * This is used to prevent SSRF attacks by restricting regionUrl to known domains
 16 |  */
 17 | export const SENTRY_ALLOWED_REGION_DOMAINS = new Set([
 18 |   "sentry.io",
 19 |   "us.sentry.io",
 20 |   "de.sentry.io",
 21 | ]);
 22 | 
 23 | /**
 24 |  * Common Sentry platforms that have documentation available
 25 |  */
 26 | export const SENTRY_PLATFORMS_BASE = [
 27 |   "javascript",
 28 |   "python",
 29 |   "java",
 30 |   "dotnet",
 31 |   "go",
 32 |   "php",
 33 |   "ruby",
 34 |   "android",
 35 |   "apple",
 36 |   "unity",
 37 |   "unreal",
 38 |   "rust",
 39 |   "elixir",
 40 |   "kotlin",
 41 |   "native",
 42 |   "dart",
 43 |   "godot",
 44 |   "nintendo-switch",
 45 |   "playstation",
 46 |   "powershell",
 47 |   "react-native",
 48 |   "xbox",
 49 | ] as const;
 50 | 
 51 | /**
 52 |  * Platform-specific frameworks that have Sentry guides
 53 |  */
 54 | export const SENTRY_FRAMEWORKS: Record<string, string[]> = {
 55 |   javascript: [
 56 |     "nextjs",
 57 |     "react",
 58 |     "gatsby",
 59 |     "remix",
 60 |     "vue",
 61 |     "angular",
 62 |     "hono",
 63 |     "svelte",
 64 |     "express",
 65 |     "fastify",
 66 |     "astro",
 67 |     "bun",
 68 |     "capacitor",
 69 |     "cloudflare",
 70 |     "connect",
 71 |     "cordova",
 72 |     "deno",
 73 |     "electron",
 74 |     "ember",
 75 |     "nuxt",
 76 |     "solid",
 77 |     "solidstart",
 78 |     "sveltekit",
 79 |     "tanstack-react",
 80 |     "wasm",
 81 |     "node",
 82 |     "koa",
 83 |     "nestjs",
 84 |     "hapi",
 85 |   ],
 86 |   python: [
 87 |     "django",
 88 |     "flask",
 89 |     "fastapi",
 90 |     "celery",
 91 |     "tornado",
 92 |     "pyramid",
 93 |     "aiohttp",
 94 |     "anthropic",
 95 |     "airflow",
 96 |     "aws-lambda",
 97 |     "boto3",
 98 |     "bottle",
 99 |     "chalice",
100 |     "dramatiq",
101 |     "falcon",
102 |     "langchain",
103 |     "litestar",
104 |     "logging",
105 |     "loguru",
106 |     "openai",
107 |     "quart",
108 |     "ray",
109 |     "redis",
110 |     "rq",
111 |     "sanic",
112 |     "sqlalchemy",
113 |     "starlette",
114 |   ],
115 |   dart: ["flutter"],
116 |   dotnet: [
117 |     "aspnetcore",
118 |     "maui",
119 |     "wpf",
120 |     "winforms",
121 |     "aspnet",
122 |     "aws-lambda",
123 |     "azure-functions",
124 |     "blazor-webassembly",
125 |     "entityframework",
126 |     "google-cloud-functions",
127 |     "extensions-logging",
128 |     "log4net",
129 |     "nlog",
130 |     "serilog",
131 |     "uwp",
132 |     "xamarin",
133 |   ],
134 |   java: [
135 |     "spring",
136 |     "spring-boot",
137 |     "android",
138 |     "jul",
139 |     "log4j2",
140 |     "logback",
141 |     "servlet",
142 |   ],
143 |   go: [
144 |     "echo",
145 |     "fasthttp",
146 |     "fiber",
147 |     "gin",
148 |     "http",
149 |     "iris",
150 |     "logrus",
151 |     "negroni",
152 |     "slog",
153 |     "zerolog",
154 |   ],
155 |   php: ["laravel", "symfony"],
156 |   ruby: ["delayed_job", "rack", "rails", "resque", "sidekiq"],
157 |   android: ["kotlin"],
158 |   apple: ["ios", "macos", "watchos", "tvos", "visionos"],
159 |   kotlin: ["multiplatform"],
160 | } as const;
161 | 
162 | /**
163 |  * All valid guides for Sentry docs search filtering.
164 |  * A guide can be either a platform (e.g., 'javascript') or a platform/framework combination (e.g., 'javascript/nextjs').
165 |  */
166 | export const SENTRY_GUIDES = [
167 |   // Base platforms
168 |   ...SENTRY_PLATFORMS_BASE,
169 |   // Platform/guide combinations
170 |   ...Object.entries(SENTRY_FRAMEWORKS).flatMap(([platform, guides]) =>
171 |     guides.map((guide) => `${platform}/${guide}`),
172 |   ),
173 | ] as const;
174 | 
175 | export const DEFAULT_SCOPES = [
176 |   "org:read",
177 |   "project:read",
178 |   "team:read",
179 |   "event:read",
180 | ] as const;
181 | 
182 | // Note: All scopes are now exported from permissions.ts to avoid pulling this
183 | // heavy constants module into scope-only consumers.
184 | 
```

--------------------------------------------------------------------------------
/packages/mcp-test-client/src/mcp-test-client-remote.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { experimental_createMCPClient } from "ai";
  2 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
  3 | import { startNewTrace, startSpan } from "@sentry/core";
  4 | import { OAuthClient } from "./auth/oauth.js";
  5 | import { DEFAULT_MCP_URL } from "./constants.js";
  6 | import { logError, logSuccess } from "./logger.js";
  7 | import type { MCPConnection, RemoteMCPConfig } from "./types.js";
  8 | import { randomUUID } from "node:crypto";
  9 | import { LIB_VERSION } from "./version.js";
 10 | 
 11 | export async function connectToRemoteMCPServer(
 12 |   config: RemoteMCPConfig,
 13 | ): Promise<MCPConnection> {
 14 |   const sessionId = randomUUID();
 15 | 
 16 |   return await startNewTrace(async () => {
 17 |     return await startSpan(
 18 |       {
 19 |         name: "mcp.connect/http",
 20 |         attributes: {
 21 |           "mcp.transport": "http",
 22 |           "gen_ai.conversation.id": sessionId,
 23 |           "service.version": LIB_VERSION,
 24 |         },
 25 |       },
 26 |       async (span) => {
 27 |         try {
 28 |           const mcpHost = config.mcpHost || DEFAULT_MCP_URL;
 29 | 
 30 |           // Remove custom attributes - let SDK handle standard attributes
 31 |           let accessToken = config.accessToken;
 32 | 
 33 |           // If no access token provided, we need to authenticate
 34 |           if (!accessToken) {
 35 |             await startSpan(
 36 |               {
 37 |                 name: "mcp.auth/oauth",
 38 |               },
 39 |               async (authSpan) => {
 40 |                 try {
 41 |                   const oauthClient = new OAuthClient({
 42 |                     mcpHost: mcpHost,
 43 |                   });
 44 |                   accessToken = await oauthClient.getAccessToken();
 45 |                   authSpan.setStatus({ code: 1 });
 46 |                 } catch (error) {
 47 |                   authSpan.setStatus({ code: 2 });
 48 |                   logError(
 49 |                     "OAuth authentication failed",
 50 |                     error instanceof Error ? error : String(error),
 51 |                   );
 52 |                   throw error;
 53 |                 }
 54 |               },
 55 |             );
 56 |           }
 57 | 
 58 |           // Create HTTP streaming client with authentication
 59 |           // Use ?agent=1 query param for agent mode, otherwise standard /mcp
 60 |           const mcpUrl = new URL(`${mcpHost}/mcp`);
 61 |           if (config.useAgentEndpoint) {
 62 |             mcpUrl.searchParams.set("agent", "1");
 63 |           }
 64 |           const httpTransport = new StreamableHTTPClientTransport(mcpUrl, {
 65 |             requestInit: {
 66 |               headers: {
 67 |                 Authorization: `Bearer ${accessToken}`,
 68 |               },
 69 |             },
 70 |           });
 71 | 
 72 |           const client = await experimental_createMCPClient({
 73 |             name: "mcp.sentry.dev (test-client)",
 74 |             transport: httpTransport,
 75 |           });
 76 | 
 77 |           // Discover available tools
 78 |           const toolsMap = await client.tools();
 79 |           const tools = new Map<string, any>();
 80 | 
 81 |           for (const [name, tool] of Object.entries(toolsMap)) {
 82 |             tools.set(name, tool);
 83 |           }
 84 | 
 85 |           // Remove custom attributes - let SDK handle standard attributes
 86 |           span.setStatus({ code: 1 });
 87 | 
 88 |           logSuccess(
 89 |             `Connected to MCP server (${mcpHost})`,
 90 |             `${tools.size} tools available`,
 91 |           );
 92 | 
 93 |           const disconnect = async () => {
 94 |             await client.close();
 95 |           };
 96 | 
 97 |           return {
 98 |             client,
 99 |             tools,
100 |             disconnect,
101 |             sessionId,
102 |             transport: "http" as const,
103 |           };
104 |         } catch (error) {
105 |           span.setStatus({ code: 2 });
106 |           throw error;
107 |         }
108 |       },
109 |     );
110 |   });
111 | }
112 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/get-event-attachment.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import getEventAttachment from "./get-event-attachment.js";
  3 | 
  4 | describe("get_event_attachment", () => {
  5 |   it("lists attachments for an event", async () => {
  6 |     const result = await getEventAttachment.handler(
  7 |       {
  8 |         organizationSlug: "sentry-mcp-evals",
  9 |         projectSlug: "cloudflare-mcp",
 10 |         eventId: "7ca573c0f4814912aaa9bdc77d1a7d51",
 11 |         attachmentId: undefined,
 12 |         regionUrl: undefined,
 13 |       },
 14 |       {
 15 |         constraints: {
 16 |           organizationSlug: null,
 17 |           projectSlug: null,
 18 |         },
 19 |         accessToken: "access-token",
 20 |         userId: "1",
 21 |       },
 22 |     );
 23 |     expect(result).toMatchInlineSnapshot(`
 24 |       "# Event Attachments
 25 | 
 26 |       **Event ID:** 7ca573c0f4814912aaa9bdc77d1a7d51
 27 |       **Project:** cloudflare-mcp
 28 | 
 29 |       Found 1 attachment(s):
 30 | 
 31 |       ## Attachment 1
 32 | 
 33 |       **ID:** 123
 34 |       **Name:** screenshot.png
 35 |       **Type:** event.attachment
 36 |       **Size:** 1024 bytes
 37 |       **MIME Type:** image/png
 38 |       **Created:** 2025-04-08T21:15:04.000Z
 39 |       **SHA1:** abc123def456
 40 | 
 41 |       To download this attachment, use the "get_event_attachment" tool with the attachmentId provided:
 42 |       \`get_event_attachment(organizationSlug="sentry-mcp-evals", projectSlug="cloudflare-mcp", eventId="7ca573c0f4814912aaa9bdc77d1a7d51", attachmentId="123")\`
 43 | 
 44 |       "
 45 |     `);
 46 |   });
 47 | 
 48 |   it("downloads a specific attachment by ID", async () => {
 49 |     const result = await getEventAttachment.handler(
 50 |       {
 51 |         organizationSlug: "sentry-mcp-evals",
 52 |         projectSlug: "cloudflare-mcp",
 53 |         eventId: "7ca573c0f4814912aaa9bdc77d1a7d51",
 54 |         attachmentId: "123",
 55 |         regionUrl: undefined,
 56 |       },
 57 |       {
 58 |         constraints: {
 59 |           organizationSlug: null,
 60 |           projectSlug: null,
 61 |         },
 62 |         accessToken: "access-token",
 63 |         userId: "1",
 64 |       },
 65 |     );
 66 | 
 67 |     // Should return an array with both text description and image content
 68 |     expect(Array.isArray(result)).toBe(true);
 69 |     expect(result).toHaveLength(2);
 70 | 
 71 |     // First item should be the image content
 72 |     expect(result[0]).toMatchObject({
 73 |       type: "image",
 74 |       mimeType: "image/png",
 75 |       data: expect.any(String), // base64 encoded data
 76 |     });
 77 | 
 78 |     // Second item should be the text description
 79 |     expect(result[1]).toMatchInlineSnapshot(`
 80 |       {
 81 |         "text": "# Event Attachment Download
 82 | 
 83 |       **Event ID:** 7ca573c0f4814912aaa9bdc77d1a7d51
 84 |       **Attachment ID:** 123
 85 |       **Filename:** screenshot.png
 86 |       **Type:** event.attachment
 87 |       **Size:** 1024 bytes
 88 |       **MIME Type:** image/png
 89 |       **Created:** 2025-04-08T21:15:04.000Z
 90 |       **SHA1:** abc123def456
 91 | 
 92 |       **Download URL:** https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/events/7ca573c0f4814912aaa9bdc77d1a7d51/attachments/123/?download=1
 93 | 
 94 |       ## Binary Content
 95 | 
 96 |       The attachment is included as a resource and accessible through your client.
 97 |       ",
 98 |         "type": "text",
 99 |       }
100 |     `);
101 |   });
102 | 
103 |   it("throws error for malformed regionUrl", async () => {
104 |     await expect(
105 |       getEventAttachment.handler(
106 |         {
107 |           organizationSlug: "sentry-mcp-evals",
108 |           projectSlug: "cloudflare-mcp",
109 |           eventId: "7ca573c0f4814912aaa9bdc77d1a7d51",
110 |           attachmentId: undefined,
111 |           regionUrl: "https",
112 |         },
113 |         {
114 |           constraints: {
115 |             organizationSlug: null,
116 |             projectSlug: null,
117 |           },
118 |           accessToken: "access-token",
119 |           userId: "1",
120 |         },
121 |       ),
122 |     ).rejects.toThrow(
123 |       "Invalid regionUrl provided: https. Must be a valid URL.",
124 |     );
125 |   });
126 | });
127 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/code.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "namespace": "code",
 3 |   "description": "These attributes provide context about source code\n",
 4 |   "attributes": {
 5 |     "code.function.name": {
 6 |       "description": "The method or function fully-qualified name without arguments. The value should fit the natural representation of the language runtime, which is also likely the same used within `code.stacktrace` attribute value. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Function'. This constraint is imposed to prevent redundancy and maintain data integrity.\n",
 7 |       "type": "string",
 8 |       "note": "Values and format depends on each language runtime, thus it is impossible to provide an exhaustive list of examples.\nThe values are usually the same (or prefixes of) the ones found in native stack trace representation stored in\n`code.stacktrace` without information on arguments.\n\nExamples:\n\n* Java method: `com.example.MyHttpService.serveRequest`\n* Java anonymous class method: `com.mycompany.Main$1.myMethod`\n* Java lambda method: `com.mycompany.Main$$Lambda/0x0000748ae4149c00.myMethod`\n* PHP function: `GuzzleHttp\\Client::transfer`\n* Go function: `github.com/my/repo/pkg.foo.func5`\n* Elixir: `OpenTelemetry.Ctx.new`\n* Erlang: `opentelemetry_ctx:new`\n* Rust: `playground::my_module::my_cool_func`\n* C function: `fopen`\n",
 9 |       "stability": "stable",
10 |       "examples": [
11 |         "com.example.MyHttpService.serveRequest",
12 |         "GuzzleHttp\\Client::transfer",
13 |         "fopen"
14 |       ]
15 |     },
16 |     "code.file.path": {
17 |       "description": "The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Function'. This constraint is imposed to prevent redundancy and maintain data integrity.\n",
18 |       "type": "string",
19 |       "stability": "stable",
20 |       "examples": ["/usr/local/MyApplication/content_root/app/index.php"]
21 |     },
22 |     "code.line.number": {
23 |       "description": "The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Line'. This constraint is imposed to prevent redundancy and maintain data integrity.\n",
24 |       "type": "number",
25 |       "stability": "stable",
26 |       "examples": ["42"]
27 |     },
28 |     "code.column.number": {
29 |       "description": "The column number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Line'. This constraint is imposed to prevent redundancy and maintain data integrity.\n",
30 |       "type": "number",
31 |       "stability": "stable",
32 |       "examples": ["16"]
33 |     },
34 |     "code.stacktrace": {
35 |       "description": "A stacktrace as a string in the natural representation for the language runtime. The representation is identical to [`exception.stacktrace`](/docs/exceptions/exceptions-spans.md#stacktrace-representation). This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Location'. This constraint is imposed to prevent redundancy and maintain data integrity.\n",
36 |       "type": "string",
37 |       "stability": "stable",
38 |       "examples": [
39 |         "at com.example.GenerateTrace.methodB(GenerateTrace.java:13)\\n at com.example.GenerateTrace.methodA(GenerateTrace.java:9)\\n at com.example.GenerateTrace.main(GenerateTrace.java:5)\n"
40 |       ]
41 |     }
42 |   }
43 | }
44 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/use-sentry/handler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
  3 | import { experimental_createMCPClient } from "ai";
  4 | import { defineTool } from "../../internal/tool-helpers/define";
  5 | import type { ServerContext } from "../../types";
  6 | import { useSentryAgent } from "./agent";
  7 | import { buildServer } from "../../server";
  8 | import tools from "../index";
  9 | import type { ToolCall } from "../../internal/agents/callEmbeddedAgent";
 10 | 
 11 | /**
 12 |  * Format tool calls into a readable trace
 13 |  */
 14 | function formatToolCallTrace(toolCalls: ToolCall[]): string {
 15 |   let trace = "";
 16 | 
 17 |   for (let i = 0; i < toolCalls.length; i++) {
 18 |     const call = toolCalls[i];
 19 |     trace += `### ${i + 1}. ${call.toolName}\n\n`;
 20 | 
 21 |     // Type assertion is safe: AI SDK guarantees args is always a JSON-serializable object
 22 |     const args = call.args as Record<string, unknown>;
 23 | 
 24 |     // Format arguments
 25 |     if (Object.keys(args).length === 0) {
 26 |       trace += "_No arguments_\n\n";
 27 |     } else {
 28 |       trace += "**Arguments:**\n```json\n";
 29 |       trace += JSON.stringify(args, null, 2);
 30 |       trace += "\n```\n\n";
 31 |     }
 32 |   }
 33 | 
 34 |   return trace;
 35 | }
 36 | 
 37 | export default defineTool({
 38 |   name: "use_sentry",
 39 |   requiredScopes: [], // No specific scopes - uses authentication token
 40 |   description: [
 41 |     "Use Sentry's MCP Agent to answer questions related to Sentry (sentry.io).",
 42 |     "",
 43 |     "You should pass the entirety of the user's prompt to the agent. Do not interpret the prompt in any way. Just pass it directly to the agent.",
 44 |     "",
 45 |   ].join("\n"),
 46 |   inputSchema: {
 47 |     request: z
 48 |       .string()
 49 |       .trim()
 50 |       .min(1)
 51 |       .describe(
 52 |         "The user's raw input. Do not interpret the prompt in any way. Do not add any additional information to the prompt.",
 53 |       ),
 54 |     trace: z
 55 |       .boolean()
 56 |       .optional()
 57 |       .describe(
 58 |         "Enable tracing to see all tool calls made by the agent. Useful for debugging.",
 59 |       ),
 60 |   },
 61 |   annotations: {
 62 |     readOnlyHint: true, // Will be adjusted based on actual implementation
 63 |     openWorldHint: true,
 64 |   },
 65 |   async handler(params, context: ServerContext) {
 66 |     // Create linked pair of in-memory transports for client-server communication
 67 |     const [clientTransport, serverTransport] =
 68 |       InMemoryTransport.createLinkedPair();
 69 | 
 70 |     // Filter out use_sentry from tools to prevent recursion and circular dependency
 71 |     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 72 |     const { use_sentry, ...toolsForAgent } = tools;
 73 | 
 74 |     // Build internal MCP server with the provided context
 75 |     // Context is captured in tool handler closures during buildServer()
 76 |     const server = buildServer({
 77 |       context,
 78 |       tools: toolsForAgent,
 79 |     });
 80 | 
 81 |     // Connect server to its transport
 82 |     await server.server.connect(serverTransport);
 83 | 
 84 |     // Create MCP client with the other end of the transport
 85 |     const mcpClient = await experimental_createMCPClient({
 86 |       name: "mcp.sentry.dev (use-sentry)",
 87 |       transport: clientTransport,
 88 |     });
 89 | 
 90 |     try {
 91 |       // Get tools from MCP server (returns Vercel AI SDK compatible tools)
 92 |       const mcpTools = await mcpClient.tools();
 93 | 
 94 |       // Call the embedded agent with MCP tools and the user's request
 95 |       const agentResult = await useSentryAgent({
 96 |         request: params.request,
 97 |         tools: mcpTools,
 98 |       });
 99 | 
100 |       let output = agentResult.result.result;
101 | 
102 |       // If tracing is enabled, append the tool call trace
103 |       if (params.trace && agentResult.toolCalls.length > 0) {
104 |         output += "\n\n---\n\n## Tool Call Trace\n\n";
105 |         output += formatToolCallTrace(agentResult.toolCalls);
106 |       }
107 | 
108 |       return output;
109 |     } finally {
110 |       // Clean up connections
111 |       await mcpClient.close();
112 |       await server.server.close();
113 |     }
114 |   },
115 | });
116 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/search-events/agent.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import { ConfigurationError } from "../../errors";
  3 | import { callEmbeddedAgent } from "../../internal/agents/callEmbeddedAgent";
  4 | import type { SentryApiService } from "../../api-client";
  5 | import { createOtelLookupTool } from "../../internal/agents/tools/otel-semantics";
  6 | import { createWhoamiTool } from "../../internal/agents/tools/whoami";
  7 | import { createDatasetAttributesTool } from "./utils";
  8 | import { systemPrompt } from "./config";
  9 | 
 10 | const outputSchema = z
 11 |   .object({
 12 |     dataset: z
 13 |       .enum(["spans", "errors", "logs"])
 14 |       .describe("Which dataset to use for the query"),
 15 |     query: z
 16 |       .string()
 17 |       .default("")
 18 |       .nullish()
 19 |       .describe("The Sentry query string for filtering results"),
 20 |     fields: z
 21 |       .array(z.string())
 22 |       .describe("Array of field names to return in results."),
 23 |     sort: z.string().describe("Sort parameter for results."),
 24 |     timeRange: z
 25 |       .union([
 26 |         z.object({
 27 |           statsPeriod: z
 28 |             .string()
 29 |             .describe("Relative time period like '1h', '24h', '7d'"),
 30 |         }),
 31 |         z.object({
 32 |           start: z.string().describe("ISO 8601 start time"),
 33 |           end: z.string().describe("ISO 8601 end time"),
 34 |         }),
 35 |       ])
 36 |       .nullish()
 37 |       .describe(
 38 |         "Time range for filtering events. Use either statsPeriod for relative time or start/end for absolute time.",
 39 |       ),
 40 |     explanation: z
 41 |       .string()
 42 |       .describe("Brief explanation of how you translated this query."),
 43 |   })
 44 |   .refine(
 45 |     (data) => {
 46 |       // Only validate if both sort and fields are present
 47 |       if (!data.sort || !data.fields || data.fields.length === 0) {
 48 |         return true;
 49 |       }
 50 | 
 51 |       // Extract the field name from sort parameter (e.g., "-timestamp" -> "timestamp", "-count()" -> "count()")
 52 |       const sortField = data.sort.startsWith("-")
 53 |         ? data.sort.substring(1)
 54 |         : data.sort;
 55 | 
 56 |       // Check if sort field is in fields array
 57 |       return data.fields.includes(sortField);
 58 |     },
 59 |     {
 60 |       message:
 61 |         "Sort field must be included in the fields array. Sentry requires that any field used for sorting must also be explicitly selected. Add the sort field to the fields array or choose a different sort field that's already included.",
 62 |     },
 63 |   );
 64 | 
 65 | export interface SearchEventsAgentOptions {
 66 |   query: string;
 67 |   organizationSlug: string;
 68 |   apiService: SentryApiService;
 69 |   projectId?: string;
 70 | }
 71 | 
 72 | /**
 73 |  * Search events agent - single entry point for translating natural language queries to Sentry search syntax
 74 |  * This returns both the translated query result AND the tool calls made by the agent
 75 |  */
 76 | export async function searchEventsAgent(
 77 |   options: SearchEventsAgentOptions,
 78 | ): Promise<{
 79 |   result: z.infer<typeof outputSchema>;
 80 |   toolCalls: any[];
 81 | }> {
 82 |   if (!process.env.OPENAI_API_KEY) {
 83 |     throw new ConfigurationError(
 84 |       "OPENAI_API_KEY environment variable is required for semantic search",
 85 |     );
 86 |   }
 87 | 
 88 |   // Create tools pre-bound with the provided API service and organization
 89 |   const datasetAttributesTool = createDatasetAttributesTool({
 90 |     apiService: options.apiService,
 91 |     organizationSlug: options.organizationSlug,
 92 |     projectId: options.projectId,
 93 |   });
 94 |   const otelLookupTool = createOtelLookupTool({
 95 |     apiService: options.apiService,
 96 |     organizationSlug: options.organizationSlug,
 97 |     projectId: options.projectId,
 98 |   });
 99 |   const whoamiTool = createWhoamiTool({ apiService: options.apiService });
100 | 
101 |   // Use callEmbeddedAgent to translate the query with tool call capture
102 |   return await callEmbeddedAgent({
103 |     system: systemPrompt,
104 |     prompt: options.query,
105 |     tools: {
106 |       datasetAttributes: datasetAttributesTool,
107 |       otelSemantics: otelLookupTool,
108 |       whoami: whoamiTool,
109 |     },
110 |     schema: outputSchema,
111 |   });
112 | }
113 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/api-client/types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * TypeScript type definitions derived from Zod schemas.
  3 |  *
  4 |  * This module provides strongly-typed interfaces for all Sentry API data
  5 |  * structures. Types are automatically derived from their corresponding
  6 |  * Zod schemas using `z.infer<>`, ensuring perfect synchronization between
  7 |  * runtime validation and compile-time type checking.
  8 |  *
  9 |  * Type Categories:
 10 |  * - **Core Resources**: User, Organization, Team, Project
 11 |  * - **Issue Management**: Issue, Event, AssignedTo
 12 |  * - **Release Management**: Release
 13 |  * - **Search & Discovery**: Tag
 14 |  * - **Integrations**: ClientKey, AutofixRun, AutofixRunState
 15 |  *
 16 |  * Array Types:
 17 |  * All list types follow the pattern `ResourceList = Resource[]` for consistency.
 18 |  *
 19 |  * @example Type Usage
 20 |  * ```typescript
 21 |  * import type { Issue, IssueList } from "./types";
 22 |  *
 23 |  * function processIssues(issues: IssueList): void {
 24 |  *   issues.forEach((issue: Issue) => {
 25 |  *     console.log(`${issue.shortId}: ${issue.title}`);
 26 |  *   });
 27 |  * }
 28 |  * ```
 29 |  *
 30 |  * @example API Response Typing
 31 |  * ```typescript
 32 |  * async function getIssue(id: string): Promise<Issue> {
 33 |  *   const response = await apiService.getIssue({
 34 |  *     organizationSlug: "my-org",
 35 |  *     issueId: id
 36 |  *   });
 37 |  *   return response; // Already typed as Issue from schema validation
 38 |  * }
 39 |  * ```
 40 |  */
 41 | import type { z } from "zod";
 42 | import type {
 43 |   AssignedToSchema,
 44 |   AutofixRunSchema,
 45 |   AutofixRunStateSchema,
 46 |   ClientKeyListSchema,
 47 |   ClientKeySchema,
 48 |   ErrorEventSchema,
 49 |   DefaultEventSchema,
 50 |   TransactionEventSchema,
 51 |   UnknownEventSchema,
 52 |   EventSchema,
 53 |   EventAttachmentSchema,
 54 |   EventAttachmentListSchema,
 55 |   IssueListSchema,
 56 |   IssueSchema,
 57 |   OrganizationListSchema,
 58 |   OrganizationSchema,
 59 |   ProjectListSchema,
 60 |   ProjectSchema,
 61 |   ReleaseListSchema,
 62 |   ReleaseSchema,
 63 |   TagListSchema,
 64 |   TagSchema,
 65 |   TeamListSchema,
 66 |   TeamSchema,
 67 |   TraceMetaSchema,
 68 |   TraceSchema,
 69 |   TraceSpanSchema,
 70 |   TraceIssueSchema,
 71 |   UserSchema,
 72 | } from "./schema";
 73 | 
 74 | export type User = z.infer<typeof UserSchema>;
 75 | export type Organization = z.infer<typeof OrganizationSchema>;
 76 | export type Team = z.infer<typeof TeamSchema>;
 77 | export type Project = z.infer<typeof ProjectSchema>;
 78 | export type ClientKey = z.infer<typeof ClientKeySchema>;
 79 | export type Release = z.infer<typeof ReleaseSchema>;
 80 | export type Issue = z.infer<typeof IssueSchema>;
 81 | 
 82 | // Individual event types
 83 | export type ErrorEvent = z.infer<typeof ErrorEventSchema>;
 84 | export type DefaultEvent = z.infer<typeof DefaultEventSchema>;
 85 | export type TransactionEvent = z.infer<typeof TransactionEventSchema>;
 86 | export type UnknownEvent = z.infer<typeof UnknownEventSchema>;
 87 | 
 88 | // Event union - use RawEvent for parsing, Event for known types only
 89 | export type RawEvent = z.infer<typeof EventSchema>;
 90 | export type Event = ErrorEvent | DefaultEvent | TransactionEvent;
 91 | 
 92 | export type EventAttachment = z.infer<typeof EventAttachmentSchema>;
 93 | export type Tag = z.infer<typeof TagSchema>;
 94 | export type AutofixRun = z.infer<typeof AutofixRunSchema>;
 95 | export type AutofixRunState = z.infer<typeof AutofixRunStateSchema>;
 96 | export type AssignedTo = z.infer<typeof AssignedToSchema>;
 97 | 
 98 | export type OrganizationList = z.infer<typeof OrganizationListSchema>;
 99 | export type TeamList = z.infer<typeof TeamListSchema>;
100 | export type ProjectList = z.infer<typeof ProjectListSchema>;
101 | export type ReleaseList = z.infer<typeof ReleaseListSchema>;
102 | export type IssueList = z.infer<typeof IssueListSchema>;
103 | export type EventAttachmentList = z.infer<typeof EventAttachmentListSchema>;
104 | export type TagList = z.infer<typeof TagListSchema>;
105 | export type ClientKeyList = z.infer<typeof ClientKeyListSchema>;
106 | 
107 | // Trace types
108 | export type TraceMeta = z.infer<typeof TraceMetaSchema>;
109 | export type TraceSpan = z.infer<typeof TraceSpanSchema>;
110 | export type TraceIssue = z.infer<typeof TraceIssueSchema>;
111 | export type Trace = z.infer<typeof TraceSchema>;
112 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/service.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "namespace": "service",
 3 |   "description": "A service instance.\n",
 4 |   "attributes": {
 5 |     "service.name": {
 6 |       "description": "Logical name of the service.\n",
 7 |       "type": "string",
 8 |       "note": "MUST be the same for all instances of horizontally scaled services. If the value was not specified, SDKs MUST fallback to `unknown_service:` concatenated with [`process.executable.name`](process.md), e.g. `unknown_service:bash`. If `process.executable.name` is not available, the value MUST be set to `unknown_service`.\n",
 9 |       "stability": "stable",
10 |       "examples": ["shoppingcart"]
11 |     },
12 |     "service.version": {
13 |       "description": "The version string of the service API or implementation. The format is not defined by these conventions.\n",
14 |       "type": "string",
15 |       "stability": "stable",
16 |       "examples": ["2.0.0", "a01dbef8a"]
17 |     },
18 |     "service.namespace": {
19 |       "description": "A namespace for `service.name`.\n",
20 |       "type": "string",
21 |       "note": "A string value having a meaning that helps to distinguish a group of services, for example the team name that owns a group of services. `service.name` is expected to be unique within the same namespace. If `service.namespace` is not specified in the Resource then `service.name` is expected to be unique for all services that have no explicit namespace defined (so the empty/unspecified namespace is simply one more valid namespace). Zero-length namespace string is assumed equal to unspecified namespace.\n",
22 |       "stability": "development",
23 |       "examples": ["Shop"]
24 |     },
25 |     "service.instance.id": {
26 |       "description": "The string ID of the service instance.\n",
27 |       "type": "string",
28 |       "note": "MUST be unique for each instance of the same `service.namespace,service.name` pair (in other words\n`service.namespace,service.name,service.instance.id` triplet MUST be globally unique). The ID helps to\ndistinguish instances of the same service that exist at the same time (e.g. instances of a horizontally scaled\nservice).\n\nImplementations, such as SDKs, are recommended to generate a random Version 1 or Version 4 [RFC\n4122](https://www.ietf.org/rfc/rfc4122.txt) UUID, but are free to use an inherent unique ID as the source of\nthis value if stability is desirable. In that case, the ID SHOULD be used as source of a UUID Version 5 and\nSHOULD use the following UUID as the namespace: `4d63009a-8d0f-11ee-aad7-4c796ed8e320`.\n\nUUIDs are typically recommended, as only an opaque value for the purposes of identifying a service instance is\nneeded. Similar to what can be seen in the man page for the\n[`/etc/machine-id`](https://www.freedesktop.org/software/systemd/man/latest/machine-id.html) file, the underlying\ndata, such as pod name and namespace should be treated as confidential, being the user's choice to expose it\nor not via another resource attribute.\n\nFor applications running behind an application server (like unicorn), we do not recommend using one identifier\nfor all processes participating in the application. Instead, it's recommended each division (e.g. a worker\nthread in unicorn) to have its own instance.id.\n\nIt's not recommended for a Collector to set `service.instance.id` if it can't unambiguously determine the\nservice instance that is generating that telemetry. For instance, creating an UUID based on `pod.name` will\nlikely be wrong, as the Collector might not know from which container within that pod the telemetry originated.\nHowever, Collectors can set the `service.instance.id` if they can unambiguously determine the service instance\nfor that telemetry. This is typically the case for scraping receivers, as they know the target address and\nport.\n",
29 |       "stability": "development",
30 |       "examples": ["627cc493-f310-47de-96bd-71410b7dec09"]
31 |     }
32 |   }
33 | }
34 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/tool-helpers/seer.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { z } from "zod";
  2 | import type {
  3 |   AutofixRunStepSchema,
  4 |   AutofixRunStepRootCauseAnalysisSchema,
  5 |   AutofixRunStepSolutionSchema,
  6 |   AutofixRunStepDefaultSchema,
  7 | } from "../../api-client/index";
  8 | 
  9 | export const SEER_POLLING_INTERVAL = 5000; // 5 seconds
 10 | export const SEER_TIMEOUT = 5 * 60 * 1000; // 5 minutes
 11 | export const SEER_MAX_RETRIES = 3; // Maximum retries for transient failures
 12 | export const SEER_INITIAL_RETRY_DELAY = 1000; // 1 second initial retry delay
 13 | 
 14 | export function getStatusDisplayName(status: string): string {
 15 |   switch (status) {
 16 |     case "COMPLETED":
 17 |       return "Complete";
 18 |     case "FAILED":
 19 |     case "ERROR":
 20 |       return "Failed";
 21 |     case "CANCELLED":
 22 |       return "Cancelled";
 23 |     case "NEED_MORE_INFORMATION":
 24 |       return "Needs More Information";
 25 |     case "WAITING_FOR_USER_RESPONSE":
 26 |       return "Waiting for Response";
 27 |     case "PROCESSING":
 28 |       return "Processing";
 29 |     case "IN_PROGRESS":
 30 |       return "In Progress";
 31 |     default:
 32 |       return status;
 33 |   }
 34 | }
 35 | 
 36 | /**
 37 |  * Check if an autofix status is terminal (no more updates expected)
 38 |  */
 39 | export function isTerminalStatus(status: string): boolean {
 40 |   return [
 41 |     "COMPLETED",
 42 |     "FAILED",
 43 |     "ERROR",
 44 |     "CANCELLED",
 45 |     "NEED_MORE_INFORMATION",
 46 |     "WAITING_FOR_USER_RESPONSE",
 47 |   ].includes(status);
 48 | }
 49 | 
 50 | /**
 51 |  * Check if an autofix status requires human intervention
 52 |  */
 53 | export function isHumanInterventionStatus(status: string): boolean {
 54 |   return (
 55 |     status === "NEED_MORE_INFORMATION" || status === "WAITING_FOR_USER_RESPONSE"
 56 |   );
 57 | }
 58 | 
 59 | /**
 60 |  * Get guidance message for human intervention states
 61 |  */
 62 | export function getHumanInterventionGuidance(status: string): string {
 63 |   if (status === "NEED_MORE_INFORMATION") {
 64 |     return "\nSeer needs additional information to continue the analysis. Please review the insights above and consider providing more context.\n";
 65 |   }
 66 |   if (status === "WAITING_FOR_USER_RESPONSE") {
 67 |     return "\nSeer is waiting for your response to proceed. Please review the analysis and provide feedback.\n";
 68 |   }
 69 |   return "";
 70 | }
 71 | 
 72 | export function getOutputForAutofixStep(
 73 |   step: z.infer<typeof AutofixRunStepSchema>,
 74 | ) {
 75 |   let output = `## ${step.title}\n\n`;
 76 | 
 77 |   if (step.status === "FAILED") {
 78 |     output += `**Sentry hit an error completing this step.\n\n`;
 79 |     return output;
 80 |   }
 81 | 
 82 |   if (step.status !== "COMPLETED") {
 83 |     output += `**Sentry is still working on this step. Please check back in a minute.**\n\n`;
 84 |     return output;
 85 |   }
 86 | 
 87 |   if (step.type === "root_cause_analysis") {
 88 |     const typedStep = step as z.infer<
 89 |       typeof AutofixRunStepRootCauseAnalysisSchema
 90 |     >;
 91 | 
 92 |     for (const cause of typedStep.causes) {
 93 |       if (cause.description) {
 94 |         output += `${cause.description}\n\n`;
 95 |       }
 96 |       for (const entry of cause.root_cause_reproduction) {
 97 |         output += `**${entry.title}**\n\n`;
 98 |         output += `${entry.code_snippet_and_analysis}\n\n`;
 99 |       }
100 |     }
101 |     return output;
102 |   }
103 | 
104 |   if (step.type === "solution") {
105 |     const typedStep = step as z.infer<typeof AutofixRunStepSolutionSchema>;
106 |     output += `${typedStep.description}\n\n`;
107 |     for (const entry of typedStep.solution) {
108 |       output += `**${entry.title}**\n`;
109 |       output += `${entry.code_snippet_and_analysis}\n\n`;
110 |     }
111 | 
112 |     if (typedStep.status === "FAILED") {
113 |       output += `**Sentry hit an error completing this step.\n\n`;
114 |     } else if (typedStep.status !== "COMPLETED") {
115 |       output += `**Sentry is still working on this step.**\n\n`;
116 |     }
117 | 
118 |     return output;
119 |   }
120 | 
121 |   const typedStep = step as z.infer<typeof AutofixRunStepDefaultSchema>;
122 |   if (typedStep.insights && typedStep.insights.length > 0) {
123 |     for (const entry of typedStep.insights) {
124 |       output += `**${entry.insight}**\n`;
125 |       output += `${entry.justification}\n\n`;
126 |     }
127 |   } else if (step.output_stream) {
128 |     output += `${step.output_stream}\n`;
129 |   }
130 | 
131 |   return output;
132 | }
133 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/search-docs.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi } from "vitest";
  2 | import searchDocs from "./search-docs.js";
  3 | 
  4 | describe("search_docs", () => {
  5 |   // Note: Query validation (empty, too short, too long) is now handled by Zod schema
  6 |   // These validation tests are no longer needed as they test framework behavior, not our tool logic
  7 | 
  8 |   it("returns results from the API", async () => {
  9 |     const result = await searchDocs.handler(
 10 |       {
 11 |         query: "How do I configure rate limiting?",
 12 |         maxResults: 5,
 13 |         guide: undefined,
 14 |       },
 15 |       {
 16 |         constraints: {
 17 |           organizationSlug: null,
 18 |         },
 19 |         accessToken: "access-token",
 20 |         userId: "1",
 21 |         mcpUrl: "https://mcp.sentry.dev",
 22 |       },
 23 |     );
 24 |     expect(result).toMatchInlineSnapshot(`
 25 |       "# Documentation Search Results
 26 | 
 27 |       **Query**: "How do I configure rate limiting?"
 28 | 
 29 |       Found 2 matches
 30 | 
 31 |       These are just snippets. Use \`get_doc(path='...')\` to fetch the full content.
 32 | 
 33 |       ## 1. https://docs.sentry.io/product/rate-limiting
 34 | 
 35 |       **Path**: product/rate-limiting.md
 36 |       **Relevance**: 95.0%
 37 | 
 38 |       **Matching Context**
 39 |       > Learn how to configure rate limiting in Sentry to prevent quota exhaustion and control event ingestion.
 40 | 
 41 |       ## 2. https://docs.sentry.io/product/accounts/quotas/spike-protection
 42 | 
 43 |       **Path**: product/accounts/quotas/spike-protection.md
 44 |       **Relevance**: 87.0%
 45 | 
 46 |       **Matching Context**
 47 |       > Spike protection helps prevent unexpected spikes in event volume from consuming your quota.
 48 | 
 49 |       "
 50 |     `);
 51 |   });
 52 | 
 53 |   it("handles API errors", async () => {
 54 |     vi.spyOn(global, "fetch").mockResolvedValueOnce({
 55 |       ok: false,
 56 |       status: 500,
 57 |       statusText: "Internal Server Error",
 58 |       json: async () => ({ error: "Internal server error" }),
 59 |     } as Response);
 60 | 
 61 |     await expect(
 62 |       searchDocs.handler(
 63 |         {
 64 |           query: "test query",
 65 |           maxResults: undefined,
 66 |           guide: undefined,
 67 |         },
 68 |         {
 69 |           constraints: {
 70 |             organizationSlug: null,
 71 |           },
 72 |           accessToken: "access-token",
 73 |           userId: "1",
 74 |         },
 75 |       ),
 76 |     ).rejects.toThrow();
 77 |   });
 78 | 
 79 |   it("handles timeout errors", async () => {
 80 |     // Mock fetch to simulate a timeout by throwing an AbortError
 81 |     vi.spyOn(global, "fetch").mockImplementationOnce(() => {
 82 |       const error = new Error("The operation was aborted");
 83 |       error.name = "AbortError";
 84 |       return Promise.reject(error);
 85 |     });
 86 | 
 87 |     await expect(
 88 |       searchDocs.handler(
 89 |         {
 90 |           query: "test query",
 91 |           maxResults: undefined,
 92 |           guide: undefined,
 93 |         },
 94 |         {
 95 |           constraints: {
 96 |             organizationSlug: null,
 97 |           },
 98 |           accessToken: "access-token",
 99 |           userId: "1",
100 |         },
101 |       ),
102 |     ).rejects.toThrow("Request timeout after 15000ms");
103 |   });
104 | 
105 |   it("includes platform in output and request", async () => {
106 |     const mockFetch = vi.spyOn(global, "fetch");
107 | 
108 |     const result = await searchDocs.handler(
109 |       {
110 |         query: "test query",
111 |         maxResults: 5,
112 |         guide: "javascript/nextjs",
113 |       },
114 |       {
115 |         constraints: {
116 |           organizationSlug: null,
117 |         },
118 |         accessToken: "access-token",
119 |         userId: "1",
120 |         mcpUrl: "https://mcp.sentry.dev",
121 |       },
122 |     );
123 | 
124 |     // Check that platform is included in the output
125 |     expect(result).toContain("**Guide**: javascript/nextjs");
126 | 
127 |     // Check that platform is included in the request
128 |     expect(mockFetch).toHaveBeenCalledWith(
129 |       "https://mcp.sentry.dev/api/search",
130 |       expect.objectContaining({
131 |         method: "POST",
132 |         headers: {
133 |           "Content-Type": "application/json",
134 |         },
135 |         body: JSON.stringify({
136 |           query: "test query",
137 |           maxResults: 5,
138 |           guide: "javascript/nextjs",
139 |         }),
140 |       }),
141 |     );
142 |   });
143 | });
144 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/error-handling.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { UserInputError, ConfigurationError } from "../errors";
  2 | import { ApiError, ApiClientError, ApiServerError } from "../api-client";
  3 | import { logIssue } from "../telem/logging";
  4 | 
  5 | /**
  6 |  * Type guard to identify user input validation errors.
  7 |  */
  8 | export function isUserInputError(error: unknown): error is UserInputError {
  9 |   return error instanceof UserInputError;
 10 | }
 11 | 
 12 | /**
 13 |  * Type guard to identify configuration errors.
 14 |  */
 15 | export function isConfigurationError(
 16 |   error: unknown,
 17 | ): error is ConfigurationError {
 18 |   return error instanceof ConfigurationError;
 19 | }
 20 | 
 21 | /**
 22 |  * Type guard to identify API errors.
 23 |  */
 24 | export function isApiError(error: unknown): error is ApiError {
 25 |   return error instanceof ApiError;
 26 | }
 27 | 
 28 | /**
 29 |  * Type guard to identify API client errors (4xx).
 30 |  */
 31 | export function isApiClientError(error: unknown): error is ApiClientError {
 32 |   return error instanceof ApiClientError;
 33 | }
 34 | 
 35 | /**
 36 |  * Type guard to identify API server errors (5xx).
 37 |  */
 38 | export function isApiServerError(error: unknown): error is ApiServerError {
 39 |   return error instanceof ApiServerError;
 40 | }
 41 | 
 42 | /**
 43 |  * Format an error for user display with markdown formatting.
 44 |  * This is used by tool handlers to format errors for MCP responses.
 45 |  *
 46 |  * SECURITY: Only return trusted error messages to prevent prompt injection vulnerabilities.
 47 |  * We trust: Sentry API errors, our own UserInputError/ConfigurationError messages, and system templates.
 48 |  */
 49 | export async function formatErrorForUser(error: unknown): Promise<string> {
 50 |   if (isUserInputError(error)) {
 51 |     return [
 52 |       "**Input Error**",
 53 |       "It looks like there was a problem with the input you provided.",
 54 |       error.message,
 55 |       `You may be able to resolve the issue by addressing the concern and trying again.`,
 56 |     ].join("\n\n");
 57 |   }
 58 | 
 59 |   if (isConfigurationError(error)) {
 60 |     return [
 61 |       "**Configuration Error**",
 62 |       "There appears to be a configuration issue with your setup.",
 63 |       error.message,
 64 |       `Please check your environment configuration and try again.`,
 65 |     ].join("\n\n");
 66 |   }
 67 | 
 68 |   // Handle ApiClientError (4xx) - user input errors, should NOT be logged to Sentry
 69 |   if (isApiClientError(error)) {
 70 |     const statusText = error.status
 71 |       ? `There was an HTTP ${error.status} error with your request to the Sentry API.`
 72 |       : "There was an error with your request.";
 73 | 
 74 |     return [
 75 |       "**Input Error**",
 76 |       statusText,
 77 |       error.toUserMessage(),
 78 |       `You may be able to resolve the issue by addressing the concern and trying again.`,
 79 |     ].join("\n\n");
 80 |   }
 81 | 
 82 |   // Handle ApiServerError (5xx) - system errors, SHOULD be logged to Sentry
 83 |   if (isApiServerError(error)) {
 84 |     const eventId = logIssue(error);
 85 |     const statusText = error.status
 86 |       ? `There was an HTTP ${error.status} server error with the Sentry API.`
 87 |       : "There was a server error.";
 88 | 
 89 |     return [
 90 |       "**Error**",
 91 |       statusText,
 92 |       `${error.message}`,
 93 |       `**Event ID**: ${eventId}`,
 94 |       `Please contact support with this Event ID if the problem persists.`,
 95 |     ].join("\n\n");
 96 |   }
 97 | 
 98 |   // Handle generic ApiError (shouldn't happen with new hierarchy, but just in case)
 99 |   if (isApiError(error)) {
100 |     const statusText = error.status
101 |       ? `There was an HTTP ${error.status} error with your request to the Sentry API.`
102 |       : "There was an error with your request.";
103 | 
104 |     return [
105 |       "**Error**",
106 |       statusText,
107 |       `${error.message}`,
108 |       `You may be able to resolve the issue by addressing the concern and trying again.`,
109 |     ].join("\n\n");
110 |   }
111 | 
112 |   const eventId = logIssue(error);
113 | 
114 |   return [
115 |     "**Error**",
116 |     "It looks like there was a problem communicating with the Sentry API.",
117 |     "Please report the following to the user for the Sentry team:",
118 |     `**Event ID**: ${eventId}`,
119 |     process.env.NODE_ENV !== "production"
120 |       ? error instanceof Error
121 |         ? error.message
122 |         : String(error)
123 |       : "",
124 |   ]
125 |     .filter(Boolean)
126 |     .join("\n\n");
127 | }
128 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/use-sentry/handler.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
  2 | import useSentry from "./handler";
  3 | import type { ServerContext } from "../../types";
  4 | import type { Scope } from "../../permissions";
  5 | 
  6 | // Mock the embedded agent
  7 | vi.mock("./agent", () => ({
  8 |   useSentryAgent: vi.fn(),
  9 | }));
 10 | 
 11 | // Import the mocked module to get access to the mock function
 12 | import { useSentryAgent } from "./agent";
 13 | const mockUseSentryAgent = useSentryAgent as Mock;
 14 | 
 15 | // Use all scopes for testing to ensure all tools are available
 16 | const ALL_SCOPES: Scope[] = [
 17 |   "org:read",
 18 |   "org:write",
 19 |   "project:read",
 20 |   "project:write",
 21 |   "team:read",
 22 |   "team:write",
 23 |   "event:read",
 24 |   "event:write",
 25 |   "project:releases",
 26 |   "seer",
 27 |   "docs",
 28 | ];
 29 | 
 30 | const mockContext: ServerContext = {
 31 |   accessToken: "test-token",
 32 |   sentryHost: "sentry.io",
 33 |   userId: "1",
 34 |   clientId: "test-client",
 35 |   constraints: {},
 36 |   grantedScopes: new Set(ALL_SCOPES),
 37 | };
 38 | 
 39 | describe("use_sentry handler", () => {
 40 |   beforeEach(() => {
 41 |     mockUseSentryAgent.mockClear();
 42 |   });
 43 | 
 44 |   it("calls embedded agent with request and wrapped tools", async () => {
 45 |     mockUseSentryAgent.mockResolvedValue({
 46 |       result: {
 47 |         result: "Agent executed tools successfully",
 48 |       },
 49 |       toolCalls: [{ toolName: "whoami", args: {} }],
 50 |     });
 51 | 
 52 |     const result = await useSentry.handler(
 53 |       { request: "Show me unresolved issues" },
 54 |       mockContext,
 55 |     );
 56 | 
 57 |     // Verify agent was called
 58 |     expect(mockUseSentryAgent).toHaveBeenCalledWith({
 59 |       request: "Show me unresolved issues",
 60 |       tools: expect.objectContaining({
 61 |         whoami: expect.any(Object),
 62 |         find_organizations: expect.any(Object),
 63 |         search_issues: expect.any(Object),
 64 |       }),
 65 |     });
 66 | 
 67 |     // Verify all 19 tools were provided (20 total - use_sentry itself)
 68 |     const toolsArg = mockUseSentryAgent.mock.calls[0][0].tools;
 69 |     expect(Object.keys(toolsArg)).toHaveLength(19);
 70 | 
 71 |     // Verify result is returned
 72 |     expect(result).toBe("Agent executed tools successfully");
 73 |   });
 74 | 
 75 |   it("provides wrapped tools with ServerContext", async () => {
 76 |     mockUseSentryAgent.mockResolvedValue({
 77 |       result: {
 78 |         result: "Success",
 79 |       },
 80 |       toolCalls: [],
 81 |     });
 82 | 
 83 |     await useSentry.handler({ request: "test request" }, mockContext);
 84 | 
 85 |     // Verify tools were provided to agent
 86 |     const toolsArg = mockUseSentryAgent.mock.calls[0][0].tools;
 87 |     expect(toolsArg).toBeDefined();
 88 | 
 89 |     // Verify key tools are present
 90 |     expect(toolsArg.whoami).toBeDefined();
 91 |     expect(toolsArg.find_organizations).toBeDefined();
 92 |     expect(toolsArg.search_events).toBeDefined();
 93 |     expect(toolsArg.search_issues).toBeDefined();
 94 |     expect(toolsArg.get_issue_details).toBeDefined();
 95 |   });
 96 | 
 97 |   it("excludes use_sentry from available tools to prevent recursion", async () => {
 98 |     mockUseSentryAgent.mockResolvedValue({
 99 |       result: {
100 |         result: "Success",
101 |       },
102 |       toolCalls: [],
103 |     });
104 | 
105 |     await useSentry.handler({ request: "test" }, mockContext);
106 | 
107 |     const toolsArg = mockUseSentryAgent.mock.calls[0][0].tools;
108 |     const toolNames = Object.keys(toolsArg);
109 | 
110 |     // Verify use_sentry is NOT in the list
111 |     expect(toolNames).not.toContain("use_sentry");
112 | 
113 |     // Verify we have exactly 19 tools (20 total - 1 use_sentry)
114 |     expect(toolNames).toHaveLength(19);
115 |   });
116 | 
117 |   it("wraps tools with session constraints", async () => {
118 |     const constrainedContext: ServerContext = {
119 |       ...mockContext,
120 |       constraints: {
121 |         organizationSlug: "constrained-org",
122 |         projectSlug: "constrained-project",
123 |       },
124 |     };
125 | 
126 |     mockUseSentryAgent.mockResolvedValue({
127 |       result: {
128 |         result: "Success with constraints",
129 |       },
130 |       toolCalls: [],
131 |     });
132 | 
133 |     await useSentry.handler(
134 |       { request: "test with constraints" },
135 |       constrainedContext,
136 |     );
137 | 
138 |     // Verify agent was called with tools
139 |     expect(mockUseSentryAgent).toHaveBeenCalled();
140 |     const toolsArg = mockUseSentryAgent.mock.calls[0][0].tools;
141 |     expect(toolsArg).toBeDefined();
142 |     expect(Object.keys(toolsArg)).toHaveLength(19);
143 |   });
144 | });
145 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/data/artifact.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "namespace": "artifact",
 3 |   "description": "This group describes attributes specific to artifacts. Artifacts are files or other immutable objects that are intended for distribution. This definition aligns directly with the [SLSA](https://slsa.dev/spec/v1.0/terminology#package-model) package model.\n",
 4 |   "attributes": {
 5 |     "artifact.filename": {
 6 |       "description": "The human readable file name of the artifact, typically generated during build and release processes. Often includes the package name and version in the file name.\n",
 7 |       "type": "string",
 8 |       "note": "This file name can also act as the [Package Name](https://slsa.dev/spec/v1.0/terminology#package-model)\nin cases where the package ecosystem maps accordingly.\nAdditionally, the artifact [can be published](https://slsa.dev/spec/v1.0/terminology#software-supply-chain)\nfor others, but that is not a guarantee.\n",
 9 |       "stability": "development",
10 |       "examples": [
11 |         "golang-binary-amd64-v0.1.0",
12 |         "docker-image-amd64-v0.1.0",
13 |         "release-1.tar.gz",
14 |         "file-name-package.tar.gz"
15 |       ]
16 |     },
17 |     "artifact.version": {
18 |       "description": "The version of the artifact.\n",
19 |       "type": "string",
20 |       "stability": "development",
21 |       "examples": ["v0.1.0", "1.2.1", "122691-build"]
22 |     },
23 |     "artifact.purl": {
24 |       "description": "The [Package URL](https://github.com/package-url/purl-spec) of the [package artifact](https://slsa.dev/spec/v1.0/terminology#package-model) provides a standard way to identify and locate the packaged artifact.\n",
25 |       "type": "string",
26 |       "stability": "development",
27 |       "examples": [
28 |         "pkg:github/package-url/purl-spec@1209109710924",
29 |         "pkg:npm/[email protected]"
30 |       ]
31 |     },
32 |     "artifact.hash": {
33 |       "description": "The full [hash value (see glossary)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf), often found in checksum.txt on a release of the artifact and used to verify package integrity.\n",
34 |       "type": "string",
35 |       "note": "The specific algorithm used to create the cryptographic hash value is\nnot defined. In situations where an artifact has multiple\ncryptographic hashes, it is up to the implementer to choose which\nhash value to set here; this should be the most secure hash algorithm\nthat is suitable for the situation and consistent with the\ncorresponding attestation. The implementer can then provide the other\nhash values through an additional set of attribute extensions as they\ndeem necessary.\n",
36 |       "stability": "development",
37 |       "examples": [
38 |         "9ff4c52759e2c4ac70b7d517bc7fcdc1cda631ca0045271ddd1b192544f8a3e9"
39 |       ]
40 |     },
41 |     "artifact.attestation.id": {
42 |       "description": "The id of the build [software attestation](https://slsa.dev/attestation-model).\n",
43 |       "type": "string",
44 |       "stability": "development",
45 |       "examples": ["123"]
46 |     },
47 |     "artifact.attestation.filename": {
48 |       "description": "The provenance filename of the built attestation which directly relates to the build artifact filename. This filename SHOULD accompany the artifact at publish time. See the [SLSA Relationship](https://slsa.dev/spec/v1.0/distributing-provenance#relationship-between-artifacts-and-attestations) specification for more information.\n",
49 |       "type": "string",
50 |       "stability": "development",
51 |       "examples": [
52 |         "golang-binary-amd64-v0.1.0.attestation",
53 |         "docker-image-amd64-v0.1.0.intoto.json1",
54 |         "release-1.tar.gz.attestation",
55 |         "file-name-package.tar.gz.intoto.json1"
56 |       ]
57 |     },
58 |     "artifact.attestation.hash": {
59 |       "description": "The full [hash value (see glossary)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf), of the built attestation. Some envelopes in the [software attestation space](https://github.com/in-toto/attestation/tree/main/spec) also refer to this as the **digest**.\n",
60 |       "type": "string",
61 |       "stability": "development",
62 |       "examples": [
63 |         "1b31dfcd5b7f9267bf2ff47651df1cfb9147b9e4df1f335accf65b4cda498408"
64 |       ]
65 |     }
66 |   }
67 | }
68 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/constraint-helpers.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Constraint application helpers for MCP server configuration.
  3 |  *
  4 |  * These functions handle the logic for filtering tool schemas and injecting
  5 |  * constraint parameters, including support for parameter aliases (e.g., projectSlug → projectSlugOrId).
  6 |  */
  7 | import type { Constraints } from "../types";
  8 | import type { z } from "zod";
  9 | 
 10 | /**
 11 |  * Determines which tool parameter keys should be filtered out of the schema
 12 |  * because they will be injected from constraints.
 13 |  *
 14 |  * Handles parameter aliases: when a projectSlug constraint exists and the tool
 15 |  * has a projectSlugOrId parameter, the alias will be applied UNLESS projectSlugOrId
 16 |  * is explicitly constrained with a truthy value.
 17 |  *
 18 |  * @param constraints - The active constraints (org, project, region)
 19 |  * @param toolInputSchema - The tool's input schema definition
 20 |  * @returns Array of parameter keys that should be filtered from the schema
 21 |  *
 22 |  * @example
 23 |  * ```typescript
 24 |  * const constraints = { projectSlug: "my-project", organizationSlug: "my-org" };
 25 |  * const schema = { organizationSlug: z.string(), projectSlugOrId: z.string() };
 26 |  * const keys = getConstraintKeysToFilter(constraints, schema);
 27 |  * // Returns: ["organizationSlug", "projectSlugOrId"]
 28 |  * // projectSlugOrId is included because projectSlug constraint will map to it
 29 |  * ```
 30 |  */
 31 | export function getConstraintKeysToFilter(
 32 |   constraints: Constraints & Record<string, string | null | undefined>,
 33 |   toolInputSchema: Record<string, z.ZodType>,
 34 | ): string[] {
 35 |   return Object.entries(constraints).flatMap(([key, value]) => {
 36 |     if (!value) return [];
 37 | 
 38 |     const keys: string[] = [];
 39 | 
 40 |     // If this constraint key exists in the schema, include it
 41 |     if (key in toolInputSchema) {
 42 |       keys.push(key);
 43 |     }
 44 | 
 45 |     // Special handling: projectSlug constraint can also apply to projectSlugOrId parameter
 46 |     // Only add the alias to filter if projectSlugOrId isn't being explicitly constrained
 47 |     if (
 48 |       key === "projectSlug" &&
 49 |       "projectSlugOrId" in toolInputSchema &&
 50 |       !("projectSlugOrId" in constraints && constraints.projectSlugOrId)
 51 |     ) {
 52 |       keys.push("projectSlugOrId");
 53 |     }
 54 | 
 55 |     return keys;
 56 |   });
 57 | }
 58 | 
 59 | /**
 60 |  * Builds the constraint parameters that should be injected into tool calls.
 61 |  *
 62 |  * Handles parameter aliases: when a projectSlug constraint exists and the tool
 63 |  * has a projectSlugOrId parameter, the constraint value will be injected as
 64 |  * projectSlugOrId UNLESS projectSlugOrId is explicitly constrained with a truthy value.
 65 |  *
 66 |  * @param constraints - The active constraints (org, project, region)
 67 |  * @param toolInputSchema - The tool's input schema definition
 68 |  * @returns Object mapping parameter names to constraint values
 69 |  *
 70 |  * @example
 71 |  * ```typescript
 72 |  * const constraints = { projectSlug: "my-project", organizationSlug: "my-org" };
 73 |  * const schema = { organizationSlug: z.string(), projectSlugOrId: z.string() };
 74 |  * const params = getConstraintParametersToInject(constraints, schema);
 75 |  * // Returns: { organizationSlug: "my-org", projectSlugOrId: "my-project" }
 76 |  * // projectSlug constraint is injected as projectSlugOrId parameter
 77 |  * ```
 78 |  */
 79 | export function getConstraintParametersToInject(
 80 |   constraints: Constraints & Record<string, string | null | undefined>,
 81 |   toolInputSchema: Record<string, z.ZodType>,
 82 | ): Record<string, string> {
 83 |   return Object.fromEntries(
 84 |     Object.entries(constraints).flatMap(([key, value]) => {
 85 |       if (!value) return [];
 86 | 
 87 |       const entries: [string, string][] = [];
 88 | 
 89 |       // If this constraint key exists in the schema, add it
 90 |       if (key in toolInputSchema) {
 91 |         entries.push([key, value]);
 92 |       }
 93 | 
 94 |       // Special handling: projectSlug constraint can also apply to projectSlugOrId parameter
 95 |       // Only apply alias if the target parameter isn't already being constrained with a truthy value
 96 |       if (
 97 |         key === "projectSlug" &&
 98 |         "projectSlugOrId" in toolInputSchema &&
 99 |         !("projectSlugOrId" in constraints && constraints.projectSlugOrId)
100 |       ) {
101 |         entries.push(["projectSlugOrId", value]);
102 |       }
103 | 
104 |       return entries;
105 |     }),
106 |   );
107 | }
108 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/dataset-fields.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import type { SentryApiService } from "../../../api-client";
  3 | import { agentTool } from "./utils";
  4 | 
  5 | export type DatasetType = "events" | "errors" | "search_issues";
  6 | 
  7 | export interface DatasetField {
  8 |   key: string;
  9 |   name: string;
 10 |   totalValues: number;
 11 |   examples?: string[];
 12 | }
 13 | 
 14 | export interface DatasetFieldsResult {
 15 |   dataset: string;
 16 |   fields: DatasetField[];
 17 |   commonPatterns: Array<{ pattern: string; description: string }>;
 18 | }
 19 | 
 20 | /**
 21 |  * Discover available fields for a dataset by querying Sentry's tags API
 22 |  */
 23 | export async function discoverDatasetFields(
 24 |   apiService: SentryApiService,
 25 |   organizationSlug: string,
 26 |   dataset: DatasetType,
 27 |   options: {
 28 |     projectId?: string;
 29 |   } = {},
 30 | ): Promise<DatasetFieldsResult> {
 31 |   const { projectId } = options;
 32 | 
 33 |   // Get available tags for the dataset
 34 |   const tags = await apiService.listTags({
 35 |     organizationSlug,
 36 |     dataset,
 37 |     project: projectId,
 38 |     statsPeriod: "14d",
 39 |   });
 40 | 
 41 |   // Filter out internal Sentry tags and format
 42 |   const fields = tags
 43 |     .filter((tag) => !tag.key.startsWith("sentry:"))
 44 |     .map((tag) => ({
 45 |       key: tag.key,
 46 |       name: tag.name,
 47 |       totalValues: tag.totalValues,
 48 |       examples: getFieldExamples(tag.key, dataset),
 49 |     }));
 50 | 
 51 |   return {
 52 |     dataset,
 53 |     fields,
 54 |     commonPatterns: getCommonPatterns(dataset),
 55 |   };
 56 | }
 57 | 
 58 | /**
 59 |  * Create a tool for discovering available fields in a dataset
 60 |  * The tool is pre-bound with the API service and organization configured for the appropriate region
 61 |  */
 62 | export function createDatasetFieldsTool(options: {
 63 |   apiService: SentryApiService;
 64 |   organizationSlug: string;
 65 |   dataset: DatasetType;
 66 |   projectId?: string;
 67 | }) {
 68 |   const { apiService, organizationSlug, dataset, projectId } = options;
 69 |   return agentTool({
 70 |     description: `Discover available fields for ${dataset} searches in Sentry (includes example values)`,
 71 |     parameters: z.object({}),
 72 |     execute: async () => {
 73 |       return discoverDatasetFields(apiService, organizationSlug, dataset, {
 74 |         projectId,
 75 |       });
 76 |     },
 77 |   });
 78 | }
 79 | 
 80 | /**
 81 |  * Get example values for common fields
 82 |  */
 83 | export function getFieldExamples(
 84 |   key: string,
 85 |   dataset: string,
 86 | ): string[] | undefined {
 87 |   const commonExamples: Record<string, string[]> = {
 88 |     level: ["error", "warning", "info", "debug", "fatal"],
 89 |     environment: ["production", "staging", "development"],
 90 |     release: ["v1.0.0", "latest", "[email protected]"],
 91 |     user: ["user123", "[email protected]"],
 92 |   };
 93 | 
 94 |   const issueExamples: Record<string, string[]> = {
 95 |     ...commonExamples,
 96 |     assignedOrSuggested: ["[email protected]", "team-slug", "me"],
 97 |     is: ["unresolved", "resolved", "ignored"],
 98 |   };
 99 | 
100 |   const eventExamples: Record<string, string[]> = {
101 |     ...commonExamples,
102 |     "http.method": ["GET", "POST", "PUT", "DELETE"],
103 |     "http.status_code": ["200", "404", "500"],
104 |     "db.system": ["postgresql", "mysql", "redis"],
105 |   };
106 | 
107 |   if (dataset === "search_issues") {
108 |     return issueExamples[key];
109 |   }
110 |   if (dataset === "events" || dataset === "errors") {
111 |     return eventExamples[key];
112 |   }
113 | 
114 |   return commonExamples[key];
115 | }
116 | 
117 | /**
118 |  * Get common search patterns for a dataset
119 |  */
120 | export function getCommonPatterns(dataset: string) {
121 |   if (dataset === "search_issues") {
122 |     return [
123 |       { pattern: "is:unresolved", description: "Open issues" },
124 |       { pattern: "is:resolved", description: "Closed issues" },
125 |       { pattern: "level:error", description: "Error level issues" },
126 |       {
127 |         pattern: "firstSeen:-24h",
128 |         description: "New issues from last 24 hours",
129 |       },
130 |       {
131 |         pattern: "userCount:>100",
132 |         description: "Affecting more than 100 users",
133 |       },
134 |     ];
135 |   }
136 |   if (dataset === "events" || dataset === "errors") {
137 |     return [
138 |       { pattern: "level:error", description: "Error events" },
139 |       { pattern: "environment:production", description: "Production events" },
140 |       { pattern: "timestamp:-1h", description: "Events from last hour" },
141 |       { pattern: "has:http.method", description: "HTTP requests" },
142 |       { pattern: "has:db.statement", description: "Database queries" },
143 |     ];
144 |   }
145 | 
146 |   return [];
147 | }
148 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/package.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "name": "@sentry/mcp-server",
  3 |   "version": "0.20.0",
  4 |   "type": "module",
  5 |   "packageManager": "[email protected]",
  6 |   "engines": {
  7 |     "node": ">=20"
  8 |   },
  9 |   "publishConfig": {
 10 |     "access": "public"
 11 |   },
 12 |   "license": "FSL-1.1-ALv2",
 13 |   "author": "Sentry",
 14 |   "description": "Sentry MCP Server",
 15 |   "homepage": "https://github.com/getsentry/sentry-mcp",
 16 |   "keywords": [
 17 |     "sentry"
 18 |   ],
 19 |   "bugs": {
 20 |     "url": "https://github.com/getsentry/sentry-mcp/issues"
 21 |   },
 22 |   "repository": {
 23 |     "type": "git",
 24 |     "url": "[email protected]:getsentry/sentry-mcp.git"
 25 |   },
 26 |   "bin": {
 27 |     "sentry-mcp": "./dist/index.js"
 28 |   },
 29 |   "files": [
 30 |     "./dist/*"
 31 |   ],
 32 |   "exports": {
 33 |     ".": {
 34 |       "types": "./dist/index.ts",
 35 |       "default": "./dist/index.js"
 36 |     },
 37 |     "./api-client": {
 38 |       "types": "./dist/api-client/index.ts",
 39 |       "default": "./dist/api-client/index.js"
 40 |     },
 41 |     "./constants": {
 42 |       "types": "./dist/constants.ts",
 43 |       "default": "./dist/constants.js"
 44 |     },
 45 |     "./telem": {
 46 |       "types": "./dist/telem/index.ts",
 47 |       "default": "./dist/telem/index.js"
 48 |     },
 49 |     "./telem/logging": {
 50 |       "types": "./dist/telem/logging.ts",
 51 |       "default": "./dist/telem/logging.js"
 52 |     },
 53 |     "./telem/sentry": {
 54 |       "types": "./dist/telem/sentry.ts",
 55 |       "default": "./dist/telem/sentry.js"
 56 |     },
 57 |     "./permissions": {
 58 |       "types": "./dist/permissions.ts",
 59 |       "default": "./dist/permissions.js"
 60 |     },
 61 |     "./transports/stdio": {
 62 |       "types": "./dist/transports/stdio.ts",
 63 |       "default": "./dist/transports/stdio.js"
 64 |     },
 65 |     "./server": {
 66 |       "types": "./dist/server.ts",
 67 |       "default": "./dist/server.js"
 68 |     },
 69 |     "./toolDefinitions": {
 70 |       "types": "./dist/toolDefinitions.ts",
 71 |       "default": "./dist/toolDefinitions.js"
 72 |     },
 73 |     "./types": {
 74 |       "types": "./dist/types.ts",
 75 |       "default": "./dist/types.js"
 76 |     },
 77 |     "./version": {
 78 |       "types": "./dist/version.ts",
 79 |       "default": "./dist/version.js"
 80 |     },
 81 |     "./tools/search-events": {
 82 |       "types": "./dist/tools/search-events/index.ts",
 83 |       "default": "./dist/tools/search-events/index.js"
 84 |     },
 85 |     "./tools/search-issues": {
 86 |       "types": "./dist/tools/search-issues/index.ts",
 87 |       "default": "./dist/tools/search-issues/index.js"
 88 |     },
 89 |     "./tools/search-events/agent": {
 90 |       "types": "./dist/tools/search-events/agent.ts",
 91 |       "default": "./dist/tools/search-events/agent.js"
 92 |     },
 93 |     "./tools/search-issues/agent": {
 94 |       "types": "./dist/tools/search-issues/agent.ts",
 95 |       "default": "./dist/tools/search-issues/agent.js"
 96 |     },
 97 |     "./tools/agent-tools": {
 98 |       "types": "./dist/tools/agent-tools.ts",
 99 |       "default": "./dist/tools/agent-tools.js"
100 |     },
101 |     "./internal/agents/callEmbeddedAgent": {
102 |       "types": "./dist/internal/agents/callEmbeddedAgent.ts",
103 |       "default": "./dist/internal/agents/callEmbeddedAgent.js"
104 |     }
105 |   },
106 |   "scripts": {
107 |     "prebuild": "pnpm run generate-definitions",
108 |     "build": "tsdown",
109 |     "dev": "pnpm run generate-definitions && tsdown -w",
110 |     "start": "tsx src/index.ts",
111 |     "prepare": "pnpm run build",
112 |     "pretest": "pnpm run generate-definitions",
113 |     "test": "vitest run",
114 |     "test:ci": "pnpm run generate-definitions && vitest run --coverage --reporter=default --reporter=junit --outputFile=tests.junit.xml",
115 |     "test:watch": "pnpm run generate-definitions && vitest",
116 |     "tsc": "tsc --noEmit",
117 |     "generate-definitions": "tsx scripts/generate-definitions.ts",
118 |     "generate-otel-namespaces": "tsx scripts/generate-otel-namespaces.ts",
119 |     "measure-tokens": "tsx scripts/measure-token-cost.ts"
120 |   },
121 |   "devDependencies": {
122 |     "@sentry/mcp-server-mocks": "workspace:*",
123 |     "@sentry/mcp-server-tsconfig": "workspace:*",
124 |     "msw": "catalog:",
125 |     "tiktoken": "^1.0.18",
126 |     "yaml": "^2.6.1",
127 |     "zod-to-json-schema": "catalog:"
128 |   },
129 |   "dependencies": {
130 |     "@ai-sdk/openai": "catalog:",
131 |     "@logtape/logtape": "^1.1.1",
132 |     "@logtape/sentry": "^1.1.1",
133 |     "@modelcontextprotocol/sdk": "catalog:",
134 |     "@sentry/core": "catalog:",
135 |     "@sentry/node": "catalog:",
136 |     "ai": "catalog:",
137 |     "dotenv": "catalog:",
138 |     "zod": "catalog:"
139 |   }
140 | }
141 | 
```

--------------------------------------------------------------------------------
/docs/permissions-and-scopes.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Permissions and Scopes
  2 | 
  3 | OAuth-style scope system for controlling access to Sentry MCP tools.
  4 | 
  5 | ## Default Permissions
  6 | 
  7 | **By default, all users receive read-only access.** This includes:
  8 | - `org:read`, `project:read`, `team:read`, `event:read`
  9 | 
 10 | Additional permissions must be explicitly granted through the OAuth flow or CLI arguments.
 11 | 
 12 | ## Permission Levels
 13 | 
 14 | When authenticating via OAuth, users can select additional permissions:
 15 | 
 16 | | Level | Scopes | Tools Enabled |
 17 | |-------|--------|--------------|
 18 | | **Read-Only** (default) | `org:read`, `project:read`, `team:read`, `event:read` | Search, view issues/traces, documentation |
 19 | | **+ Issue Triage** | Adds `event:write` | All above + resolve/assign issues, AI analysis |
 20 | | **+ Project Management** | Adds `project:write`, `team:write` | All above + create/modify projects/teams/DSNs |
 21 | 
 22 | ### CLI Usage
 23 | 
 24 | ```bash
 25 | # Default: read-only access
 26 | npx @sentry/mcp-server --access-token=TOKEN
 27 | 
 28 | # Override defaults with specific scopes only
 29 | npx @sentry/mcp-server --access-token=TOKEN --scopes=org:read,event:read
 30 | 
 31 | # Add write permissions to default read-only scopes
 32 | npx @sentry/mcp-server --access-token=TOKEN --add-scopes=event:write,project:write
 33 | 
 34 | # Via environment variables
 35 | export MCP_SCOPES=org:read,project:write  # Overrides defaults
 36 | export MCP_ADD_SCOPES=event:write         # Adds to defaults
 37 | npx @sentry/mcp-server --access-token=TOKEN
 38 | ```
 39 | 
 40 | Precedence and validation:
 41 | - Flags override environment variables. If `--scopes` is provided, `MCP_SCOPES` is ignored. If `--add-scopes` is provided, `MCP_ADD_SCOPES` is ignored.
 42 | - Flags and env vars are strict: any invalid scope token causes an error listing allowed scopes.
 43 | 
 44 | **Note:** `--scopes` completely replaces the default scopes, while `--add-scopes` adds to them.
 45 | 
 46 | ## Scope Hierarchy
 47 | 
 48 | Higher scopes include lower ones:
 49 | 
 50 | ```
 51 | admin → write → read
 52 | ```
 53 | 
 54 | Examples:
 55 | - `team:write` includes `team:read`
 56 | - `event:admin` includes `event:write` and `event:read`
 57 | 
 58 | ## Available Scopes
 59 | 
 60 | | Resource | Read | Write | Admin |
 61 | |----------|------|-------|-------|
 62 | | **Organization** | `org:read` | `org:write` | `org:admin` |
 63 | | **Project** | `project:read` | `project:write` | `project:admin` |
 64 | | **Team** | `team:read` | `team:write` | `team:admin` |
 65 | | **Member** | `member:read` | `member:write` | `member:admin` |
 66 | | **Event/Issue** | `event:read` | `event:write` | `event:admin` |
 67 | | **Special** | `project:releases` | - | - |
 68 | 
 69 | ## Tool Requirements
 70 | 
 71 | ### Always Available (No Scopes)
 72 | - `whoami` - User identification
 73 | - `search_docs` - Documentation search
 74 | - `get_doc` - Documentation retrieval
 75 | 
 76 | ### Read Operations
 77 | - `find_organizations` - `org:read`
 78 | - `find_projects` - `project:read`
 79 | - `find_teams` - `team:read`
 80 | - `find_releases` - `project:read`
 81 | - `find_dsns` - `project:read`
 82 | - `get_issue_details` - `event:read`
 83 | - `get_event_attachment` - `event:read`
 84 | - `get_trace_details` - `event:read`
 85 | - `search_events` - `event:read`
 86 | - `search_issues` - `event:read`
 87 | - `analyze_issue_with_seer` - `event:read`
 88 | 
 89 | ### Write Operations
 90 | - `update_issue` - `event:write`
 91 | - `create_project` - `project:write`, `team:read`
 92 | - `update_project` - `project:write`
 93 | - `create_team` - `team:write`
 94 | - `create_dsn` - `project:write`
 95 | 
 96 | ## How It Works
 97 | 
 98 | 1. **Sentry Authentication**: MCP requests all necessary scopes from Sentry
 99 | 2. **Permission Selection**: User chooses permission level in approval dialog
100 | 3. **Tool Filtering**: MCP filters available tools based on granted scopes
101 | 4. **Runtime Validation**: Scopes checked when tools are invoked
102 | 
103 | ## Notes
104 | 
105 | - Default behavior grants read-only access if no scopes specified
106 | - Embedded agent tools don't require scope binding
107 | - Documentation tools always available regardless of scopes
108 | 
109 | ## Troubleshooting
110 | 
111 | | Issue | Solution |
112 | |-------|----------|
113 | | Tool not in list | Check required scopes are granted |
114 | | "Tool not allowed" error | Re-authenticate with higher permission level |
115 | | Invalid scope | Use lowercase with colon separator (e.g., `event:write`) |
116 | 
117 | ## References
118 | 
119 | - Adding Tools: @docs/adding-tools.mdc — Add tools with scope requirements
120 | - Testing: @docs/testing.mdc — Test with different scope configurations
121 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/internal/agents/tools/utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { tool } from "ai";
  2 | import { z } from "zod";
  3 | import { UserInputError } from "../../../errors";
  4 | import { ApiClientError, ApiServerError } from "../../../api-client";
  5 | import { logIssue, logWarn } from "../../../telem/logging";
  6 | 
  7 | /**
  8 |  * Standard response schema for all embedded agent tools.
  9 |  * Tools return either an error message or the result data, never both.
 10 |  */
 11 | const AgentToolResponseSchema = z.object({
 12 |   error: z
 13 |     .string()
 14 |     .optional()
 15 |     .describe("Error message if the operation failed"),
 16 |   result: z.unknown().optional().describe("The successful result data"),
 17 | });
 18 | 
 19 | export type AgentToolResponse<T = unknown> = {
 20 |   error?: string;
 21 |   result?: T;
 22 | };
 23 | 
 24 | /**
 25 |  * Handles errors from agent tool execution and returns appropriate error messages.
 26 |  *
 27 |  * SECURITY: Only returns trusted error messages to prevent prompt injection.
 28 |  * We trust: Sentry API errors, our own UserInputError messages, and system templates.
 29 |  */
 30 | function handleAgentToolError<T>(error: unknown): AgentToolResponse<T> {
 31 |   if (error instanceof UserInputError) {
 32 |     // Log UserInputError for Sentry logging (as log, not exception)
 33 |     logWarn(error, {
 34 |       loggerScope: ["agent-tools", "user-input"],
 35 |       contexts: {
 36 |         agentTool: {
 37 |           errorType: "UserInputError",
 38 |         },
 39 |       },
 40 |     });
 41 |     return {
 42 |       error: `Input Error: ${error.message}. You may be able to resolve this by addressing the concern and trying again.`,
 43 |     };
 44 |   }
 45 | 
 46 |   if (error instanceof ApiClientError) {
 47 |     // Log ApiClientError for Sentry logging (as log, not exception)
 48 |     const message = error.toUserMessage();
 49 |     logWarn(message, {
 50 |       loggerScope: ["agent-tools", "api-client"],
 51 |       contexts: {
 52 |         agentTool: {
 53 |           errorType: error.name,
 54 |           status: error.status ?? null,
 55 |         },
 56 |       },
 57 |     });
 58 |     return {
 59 |       error: `Input Error: ${message}. You may be able to resolve this by addressing the concern and trying again.`,
 60 |     };
 61 |   }
 62 | 
 63 |   if (error instanceof ApiServerError) {
 64 |     // Log server errors to Sentry and get Event ID
 65 |     const eventId = logIssue(error);
 66 |     const statusText = error.status ? ` (${error.status})` : "";
 67 |     return {
 68 |       error: `Server Error${statusText}: ${error.message}. Event ID: ${eventId}. This is a system error that cannot be resolved by retrying.`,
 69 |     };
 70 |   }
 71 | 
 72 |   // Log unexpected errors to Sentry and return safe generic message
 73 |   // SECURITY: Don't return untrusted error messages that could enable prompt injection
 74 |   const eventId = logIssue(error);
 75 |   return {
 76 |     error: `System Error: An unexpected error occurred. Event ID: ${eventId}. This is a system error that cannot be resolved by retrying.`,
 77 |   };
 78 | }
 79 | 
 80 | /**
 81 |  * Creates an embedded agent tool with automatic error handling and schema wrapping.
 82 |  *
 83 |  * This wrapper:
 84 |  * - Maintains the same API as the AI SDK's tool() function
 85 |  * - Automatically wraps the result schema with error/result structure
 86 |  * - Handles all error types and returns them as structured responses
 87 |  * - Preserves type inference from the original tool implementation
 88 |  *
 89 |  * @example
 90 |  * ```typescript
 91 |  * export function createMyTool(apiService: SentryApiService) {
 92 |  *   return agentTool({
 93 |  *     description: "My tool description",
 94 |  *     parameters: z.object({ param: z.string() }),
 95 |  *     execute: async (params) => {
 96 |  *       // Tool implementation that might throw errors
 97 |  *       const result = await apiService.someMethod(params);
 98 |  *       return result; // Original return type preserved
 99 |  *     }
100 |  *   });
101 |  * }
102 |  * ```
103 |  */
104 | export function agentTool<TParameters, TResult>(config: {
105 |   description: string;
106 |   parameters: z.ZodSchema<TParameters>;
107 |   execute: (params: TParameters) => Promise<TResult>;
108 | }) {
109 |   // Infer the result type from the execute function's return type
110 |   type InferredResult = Awaited<ReturnType<typeof config.execute>>;
111 | 
112 |   return tool({
113 |     description: config.description,
114 |     parameters: config.parameters,
115 |     execute: async (
116 |       params: TParameters,
117 |     ): Promise<AgentToolResponse<InferredResult>> => {
118 |       try {
119 |         const result = await config.execute(params);
120 |         return { result };
121 |       } catch (error) {
122 |         return handleAgentToolError<InferredResult>(error);
123 |       }
124 |     },
125 |   });
126 | }
127 | 
```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/app.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { Header } from "./components/ui/header";
  2 | import { useState, useEffect } from "react";
  3 | import { Chat } from "./components/chat";
  4 | import { useAuth } from "./contexts/auth-context";
  5 | import Home from "./pages/home";
  6 | 
  7 | export default function App() {
  8 |   const { isAuthenticated, handleLogout } = useAuth();
  9 | 
 10 |   const [isChatOpen, setIsChatOpen] = useState(() => {
 11 |     // Initialize based on URL query string only to avoid hydration issues
 12 |     const urlParams = new URLSearchParams(window.location.search);
 13 |     const hasQueryParam = urlParams.has("chat");
 14 | 
 15 |     if (hasQueryParam) {
 16 |       return urlParams.get("chat") !== "0";
 17 |     }
 18 | 
 19 |     // Default based on screen size to avoid flash on mobile
 20 |     // Note: This is safe for SSR since we handle the correction in useEffect
 21 |     if (typeof window !== "undefined") {
 22 |       return window.innerWidth >= 768; // Desktop: open, Mobile: closed
 23 |     }
 24 | 
 25 |     // SSR fallback - default to true for desktop-first approach
 26 |     return true;
 27 |   });
 28 | 
 29 |   // Adjust initial state for mobile after component mounts
 30 |   useEffect(() => {
 31 |     const urlParams = new URLSearchParams(window.location.search);
 32 | 
 33 |     // Only adjust state if no URL parameter exists and we're on mobile
 34 |     if (!urlParams.has("chat") && window.innerWidth < 768) {
 35 |       setIsChatOpen(false);
 36 |     }
 37 |   }, []);
 38 | 
 39 |   // Update URL when chat state changes
 40 |   const toggleChat = (open: boolean) => {
 41 |     setIsChatOpen(open);
 42 | 
 43 |     if (open) {
 44 |       // Add ?chat to URL
 45 |       const newUrl = new URL(window.location.href);
 46 |       newUrl.searchParams.set("chat", "1");
 47 |       window.history.pushState({}, "", newUrl.toString());
 48 |     } else {
 49 |       // Remove query string for home page
 50 |       const newUrl = new URL(window.location.href);
 51 |       newUrl.search = "";
 52 |       window.history.pushState({}, "", newUrl.toString());
 53 |     }
 54 |   };
 55 | 
 56 |   // Handle browser back/forward navigation
 57 |   useEffect(() => {
 58 |     const handlePopState = () => {
 59 |       const urlParams = new URLSearchParams(window.location.search);
 60 |       const hasQueryParam = urlParams.has("chat");
 61 | 
 62 |       if (hasQueryParam) {
 63 |         setIsChatOpen(urlParams.get("chat") !== "0");
 64 |       } else {
 65 |         // Default to open on desktop, closed on mobile
 66 |         setIsChatOpen(window.innerWidth >= 768);
 67 |       }
 68 |     };
 69 | 
 70 |     window.addEventListener("popstate", handlePopState);
 71 |     return () => window.removeEventListener("popstate", handlePopState);
 72 |   }, []);
 73 | 
 74 |   // Handle window resize to adjust chat state appropriately
 75 |   useEffect(() => {
 76 |     const handleResize = () => {
 77 |       // If no explicit URL state, adjust based on screen size
 78 |       const urlParams = new URLSearchParams(window.location.search);
 79 |       if (!urlParams.has("chat")) {
 80 |         const isDesktop = window.innerWidth >= 768;
 81 |         setIsChatOpen(isDesktop); // Open on desktop, closed on mobile
 82 |       }
 83 |     };
 84 | 
 85 |     window.addEventListener("resize", handleResize);
 86 |     return () => window.removeEventListener("resize", handleResize);
 87 |   }, []);
 88 | 
 89 |   return (
 90 |     <div className="min-h-screen text-white">
 91 |       {/* Mobile layout: Single column with overlay chat */}
 92 |       <div className="md:hidden h-screen flex flex-col">
 93 |         <div className="flex-1 overflow-y-auto sm:p-8 p-4">
 94 |           <div className="max-w-3xl mx-auto">
 95 |             <Header isAuthenticated={isAuthenticated} onLogout={handleLogout} />
 96 |             <Home onChatClick={() => toggleChat(true)} />
 97 |           </div>
 98 |         </div>
 99 |       </div>
100 | 
101 |       {/* Desktop layout: Main content adjusts width based on chat state */}
102 |       <div className="hidden md:flex h-screen">
103 |         <div
104 |           className={`flex flex-col ${isChatOpen ? "w-1/2" : "flex-1"} md:transition-all md:duration-300`}
105 |         >
106 |           <div className="flex-1 overflow-y-auto sm:p-8 p-4">
107 |             <div className="max-w-3xl mx-auto">
108 |               <Header
109 |                 isAuthenticated={isAuthenticated}
110 |                 onLogout={handleLogout}
111 |               />
112 |               <Home onChatClick={() => toggleChat(true)} />
113 |             </div>
114 |           </div>
115 |         </div>
116 |       </div>
117 | 
118 |       {/* Single Chat component - handles both mobile and desktop layouts */}
119 |       <Chat
120 |         isOpen={isChatOpen}
121 |         onClose={() => toggleChat(false)}
122 |         onLogout={handleLogout}
123 |       />
124 |     </div>
125 |   );
126 | }
127 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * Main CLI entry point for the Sentry MCP server.
  5 |  *
  6 |  * Handles command-line argument parsing, environment configuration, Sentry
  7 |  * initialization, and starts the MCP server with stdio transport. Requires
  8 |  * a Sentry access token and optionally accepts host and DSN configuration.
  9 |  *
 10 |  * @example CLI Usage
 11 |  * ```bash
 12 |  * npx @sentry/mcp-server --access-token=TOKEN --host=sentry.io
 13 |  * npx @sentry/mcp-server --access-token=TOKEN --url=https://sentry.example.com
 14 |  * ```
 15 |  */
 16 | 
 17 | import { buildServer } from "./server";
 18 | import { startStdio } from "./transports/stdio";
 19 | import * as Sentry from "@sentry/node";
 20 | import { LIB_VERSION } from "./version";
 21 | import { buildUsage } from "./cli/usage";
 22 | import { parseArgv, parseEnv, merge } from "./cli/parse";
 23 | import { finalize } from "./cli/resolve";
 24 | import { sentryBeforeSend } from "./telem/sentry";
 25 | import { ALL_SCOPES } from "./permissions";
 26 | import { DEFAULT_SCOPES } from "./constants";
 27 | import { configureOpenAIProvider } from "./internal/agents/openai-provider";
 28 | import agentTools from "./tools/agent-tools";
 29 | 
 30 | const packageName = "@sentry/mcp-server";
 31 | const usageText = buildUsage(packageName, DEFAULT_SCOPES, ALL_SCOPES);
 32 | 
 33 | function die(message: string): never {
 34 |   console.error(message);
 35 |   console.error(usageText);
 36 |   process.exit(1);
 37 | }
 38 | const cli = parseArgv(process.argv.slice(2));
 39 | if (cli.help) {
 40 |   console.log(usageText);
 41 |   process.exit(0);
 42 | }
 43 | if (cli.version) {
 44 |   console.log(`${packageName} ${LIB_VERSION}`);
 45 |   process.exit(0);
 46 | }
 47 | if (cli.unknownArgs.length > 0) {
 48 |   console.error("Error: Invalid argument(s):", cli.unknownArgs.join(", "));
 49 |   console.error(usageText);
 50 |   process.exit(1);
 51 | }
 52 | 
 53 | const env = parseEnv(process.env);
 54 | const cfg = (() => {
 55 |   try {
 56 |     return finalize(merge(cli, env));
 57 |   } catch (err) {
 58 |     die(err instanceof Error ? err.message : String(err));
 59 |   }
 60 | })();
 61 | 
 62 | // Check for OpenAI API key and warn if missing
 63 | if (!process.env.OPENAI_API_KEY) {
 64 |   console.warn("Warning: OPENAI_API_KEY environment variable is not set.");
 65 |   console.warn("The following AI-powered search tools will be unavailable:");
 66 |   console.warn("  - search_events (natural language event search)");
 67 |   console.warn("  - search_issues (natural language issue search)");
 68 |   console.warn(
 69 |     "All other tools will function normally. To enable AI-powered search, set OPENAI_API_KEY.",
 70 |   );
 71 |   console.warn("");
 72 | }
 73 | 
 74 | configureOpenAIProvider({ baseUrl: cfg.openaiBaseUrl });
 75 | 
 76 | Sentry.init({
 77 |   dsn: cfg.sentryDsn,
 78 |   sendDefaultPii: true,
 79 |   tracesSampleRate: 1,
 80 |   beforeSend: sentryBeforeSend,
 81 |   initialScope: {
 82 |     tags: {
 83 |       "mcp.server_version": LIB_VERSION,
 84 |       "mcp.transport": "stdio",
 85 |       "mcp.agent_mode": cli.agent ? "true" : "false",
 86 |       "sentry.host": cfg.sentryHost,
 87 |       "mcp.mcp-url": cfg.mcpUrl,
 88 |     },
 89 |   },
 90 |   release: process.env.SENTRY_RELEASE,
 91 |   integrations: [
 92 |     Sentry.consoleLoggingIntegration(),
 93 |     Sentry.zodErrorsIntegration(),
 94 |     Sentry.vercelAIIntegration({
 95 |       recordInputs: true,
 96 |       recordOutputs: true,
 97 |     }),
 98 |   ],
 99 |   environment:
100 |     process.env.SENTRY_ENVIRONMENT ??
101 |     (process.env.NODE_ENV !== "production" ? "development" : "production"),
102 | });
103 | 
104 | // Log agent mode status
105 | if (cli.agent) {
106 |   console.warn("Agent mode enabled: Only use_sentry tool is available.");
107 |   console.warn(
108 |     "The use_sentry tool provides access to all Sentry operations through natural language.",
109 |   );
110 |   console.warn("");
111 | }
112 | 
113 | const SENTRY_TIMEOUT = 5000; // 5 seconds
114 | 
115 | // Build context once for server configuration and runtime
116 | const context = {
117 |   accessToken: cfg.accessToken,
118 |   grantedScopes: cfg.finalScopes,
119 |   constraints: {
120 |     organizationSlug: cfg.organizationSlug ?? null,
121 |     projectSlug: cfg.projectSlug ?? null,
122 |   },
123 |   sentryHost: cfg.sentryHost,
124 |   mcpUrl: cfg.mcpUrl,
125 |   openaiBaseUrl: cfg.openaiBaseUrl,
126 | };
127 | 
128 | // Build server with context to filter tools based on granted scopes
129 | // Use agentTools when --agent flag is set (only exposes use_sentry tool)
130 | const server = buildServer({
131 |   context,
132 |   tools: cli.agent ? agentTools : undefined,
133 | });
134 | 
135 | startStdio(server, context).catch((err) => {
136 |   console.error("Server error:", err);
137 |   // ensure we've flushed all events
138 |   Sentry.flush(SENTRY_TIMEOUT);
139 |   process.exit(1);
140 | });
141 | 
142 | // ensure we've flushed all events
143 | Sentry.flush(SENTRY_TIMEOUT);
144 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/cli/parse.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { parseArgs } from "node:util";
  2 | import type { CliArgs, EnvArgs, MergedArgs } from "./types";
  3 | 
  4 | export function parseArgv(argv: string[]): CliArgs {
  5 |   const options = {
  6 |     "access-token": { type: "string" as const },
  7 |     host: { type: "string" as const },
  8 |     url: { type: "string" as const },
  9 |     "mcp-url": { type: "string" as const },
 10 |     "sentry-dsn": { type: "string" as const },
 11 |     "openai-base-url": { type: "string" as const },
 12 |     "organization-slug": { type: "string" as const },
 13 |     "project-slug": { type: "string" as const },
 14 |     scopes: { type: "string" as const },
 15 |     "add-scopes": { type: "string" as const },
 16 |     "all-scopes": { type: "boolean" as const },
 17 |     agent: { type: "boolean" as const },
 18 |     help: { type: "boolean" as const, short: "h" as const },
 19 |     version: { type: "boolean" as const, short: "v" as const },
 20 |   };
 21 | 
 22 |   const { values, positionals, tokens } = parseArgs({
 23 |     args: argv,
 24 |     options,
 25 |     allowPositionals: false,
 26 |     strict: false,
 27 |     tokens: true,
 28 |   });
 29 | 
 30 |   const knownLong = new Set(Object.keys(options));
 31 |   const knownShort = new Set([
 32 |     ...(Object.values(options)
 33 |       .map((o) => ("short" in o ? (o.short as string | undefined) : undefined))
 34 |       .filter(Boolean) as string[]),
 35 |   ]);
 36 | 
 37 |   const unknownArgs: string[] = [];
 38 |   for (const t of (tokens as any[]) || []) {
 39 |     if (t.kind === "option") {
 40 |       const name = t.name as string | undefined;
 41 |       if (name && !(knownLong.has(name) || knownShort.has(name))) {
 42 |         unknownArgs.push((t.raw as string) ?? `--${name}`);
 43 |       }
 44 |     } else if (t.kind === "positional") {
 45 |       unknownArgs.push((t.raw as string) ?? String(t.value ?? ""));
 46 |     }
 47 |   }
 48 | 
 49 |   return {
 50 |     accessToken: values["access-token"] as string | undefined,
 51 |     host: values.host as string | undefined,
 52 |     url: values.url as string | undefined,
 53 |     mcpUrl: values["mcp-url"] as string | undefined,
 54 |     sentryDsn: values["sentry-dsn"] as string | undefined,
 55 |     openaiBaseUrl: values["openai-base-url"] as string | undefined,
 56 |     organizationSlug: values["organization-slug"] as string | undefined,
 57 |     projectSlug: values["project-slug"] as string | undefined,
 58 |     scopes: values.scopes as string | undefined,
 59 |     addScopes: values["add-scopes"] as string | undefined,
 60 |     allScopes: (values["all-scopes"] as boolean | undefined) === true,
 61 |     agent: (values.agent as boolean | undefined) === true,
 62 |     help: (values.help as boolean | undefined) === true,
 63 |     version: (values.version as boolean | undefined) === true,
 64 |     unknownArgs:
 65 |       unknownArgs.length > 0 ? unknownArgs : (positionals as string[]) || [],
 66 |   };
 67 | }
 68 | 
 69 | export function parseEnv(env: NodeJS.ProcessEnv): EnvArgs {
 70 |   const fromEnv: EnvArgs = {};
 71 |   if (env.SENTRY_ACCESS_TOKEN) fromEnv.accessToken = env.SENTRY_ACCESS_TOKEN;
 72 |   if (env.SENTRY_URL) fromEnv.url = env.SENTRY_URL;
 73 |   if (env.SENTRY_HOST) fromEnv.host = env.SENTRY_HOST;
 74 |   if (env.MCP_URL) fromEnv.mcpUrl = env.MCP_URL;
 75 |   if (env.SENTRY_DSN || env.DEFAULT_SENTRY_DSN)
 76 |     fromEnv.sentryDsn = env.SENTRY_DSN || env.DEFAULT_SENTRY_DSN;
 77 |   if (env.MCP_SCOPES) fromEnv.scopes = env.MCP_SCOPES;
 78 |   if (env.MCP_ADD_SCOPES) fromEnv.addScopes = env.MCP_ADD_SCOPES;
 79 |   return fromEnv;
 80 | }
 81 | 
 82 | export function merge(cli: CliArgs, env: EnvArgs): MergedArgs {
 83 |   // CLI wins over env
 84 |   const merged: MergedArgs = {
 85 |     accessToken: cli.accessToken ?? env.accessToken,
 86 |     // If CLI provided url/host, prefer those; else fall back to env
 87 |     url: cli.url ?? env.url,
 88 |     host: cli.host ?? env.host,
 89 |     mcpUrl: cli.mcpUrl ?? env.mcpUrl,
 90 |     sentryDsn: cli.sentryDsn ?? env.sentryDsn,
 91 |     openaiBaseUrl: cli.openaiBaseUrl,
 92 |     // Scopes precedence: CLI scopes/add-scopes override their env counterparts
 93 |     scopes: cli.scopes ?? env.scopes,
 94 |     addScopes: cli.addScopes ?? env.addScopes,
 95 |     allScopes: cli.allScopes === true,
 96 |     agent: cli.agent === true,
 97 |     organizationSlug: cli.organizationSlug,
 98 |     projectSlug: cli.projectSlug,
 99 |     help: cli.help === true,
100 |     version: cli.version === true,
101 |     unknownArgs: cli.unknownArgs,
102 |   };
103 | 
104 |   // If CLI provided scopes, ignore additive env var
105 |   if (cli.scopes) merged.addScopes = cli.addScopes;
106 |   // If CLI provided add-scopes, ensure scopes override isn't pulled from env
107 |   if (cli.addScopes) merged.scopes = cli.scopes;
108 |   return merged;
109 | }
110 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/get-doc.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import { setTag } from "@sentry/core";
  3 | import { defineTool } from "../internal/tool-helpers/define";
  4 | import { fetchWithTimeout } from "../internal/fetch-utils";
  5 | import { UserInputError } from "../errors";
  6 | import { ApiError } from "../api-client/index";
  7 | import type { ServerContext } from "../types";
  8 | 
  9 | export default defineTool({
 10 |   name: "get_doc",
 11 |   requiredScopes: ["docs"], // Documentation reading requires docs permission
 12 |   description: [
 13 |     "Fetch the full markdown content of a Sentry documentation page.",
 14 |     "",
 15 |     "Use this tool when you need to:",
 16 |     "- Read the complete documentation for a specific topic",
 17 |     "- Get detailed implementation examples or code snippets",
 18 |     "- Access the full context of a documentation page",
 19 |     "- Extract specific sections from documentation",
 20 |     "",
 21 |     "<examples>",
 22 |     "### Get the Next.js integration guide",
 23 |     "",
 24 |     "```",
 25 |     "get_doc(path='/platforms/javascript/guides/nextjs.md')",
 26 |     "```",
 27 |     "</examples>",
 28 |     "",
 29 |     "<hints>",
 30 |     "- Use the path from search_docs results for accurate fetching",
 31 |     "- Paths should end with .md extension",
 32 |     "</hints>",
 33 |   ].join("\n"),
 34 |   inputSchema: {
 35 |     path: z
 36 |       .string()
 37 |       .trim()
 38 |       .describe(
 39 |         "The documentation path (e.g., '/platforms/javascript/guides/nextjs.md'). Get this from search_docs results.",
 40 |       ),
 41 |   },
 42 |   annotations: {
 43 |     readOnlyHint: true,
 44 |     openWorldHint: true,
 45 |   },
 46 |   async handler(params, context: ServerContext) {
 47 |     setTag("doc.path", params.path);
 48 | 
 49 |     let output = `# Documentation Content\n\n`;
 50 |     output += `**Path**: ${params.path}\n\n`;
 51 | 
 52 |     // Validate path format
 53 |     if (!params.path.endsWith(".md")) {
 54 |       throw new UserInputError(
 55 |         "Invalid documentation path. Path must end with .md extension.",
 56 |       );
 57 |     }
 58 | 
 59 |     // Use docs.sentry.io for now - will be configurable via flag in the future
 60 |     const baseUrl = "https://docs.sentry.io";
 61 | 
 62 |     // Construct the full URL for the markdown file
 63 |     const docUrl = new URL(params.path, baseUrl);
 64 | 
 65 |     // Validate domain whitelist for security
 66 |     const allowedDomains = ["docs.sentry.io", "develop.sentry.io"];
 67 |     if (!allowedDomains.includes(docUrl.hostname)) {
 68 |       throw new UserInputError(
 69 |         `Invalid domain. Documentation can only be fetched from allowed domains: ${allowedDomains.join(", ")}`,
 70 |       );
 71 |     }
 72 | 
 73 |     const response = await fetchWithTimeout(
 74 |       docUrl.toString(),
 75 |       {
 76 |         headers: {
 77 |           Accept: "text/plain, text/markdown",
 78 |           "User-Agent": "Sentry-MCP/1.0",
 79 |         },
 80 |       },
 81 |       15000, // 15 second timeout
 82 |     );
 83 | 
 84 |     if (!response.ok) {
 85 |       if (response.status === 404) {
 86 |         output += `**Error**: Documentation not found at this path.\n\n`;
 87 |         output += `Please verify the path is correct. Common issues:\n`;
 88 |         output += `- Path should start with / (e.g., /platforms/javascript/guides/nextjs.md)\n`;
 89 |         output += `- Path should match exactly what's shown in search_docs results\n`;
 90 |         output += `- Some pages may have been moved or renamed\n\n`;
 91 |         output += `Try searching again with \`search_docs()\` to find the correct path.\n`;
 92 |         return output;
 93 |       }
 94 | 
 95 |       throw new ApiError(
 96 |         `Failed to fetch documentation: ${response.statusText}`,
 97 |         response.status,
 98 |       );
 99 |     }
100 | 
101 |     const content = await response.text();
102 | 
103 |     // Check if we got HTML instead of markdown (wrong path format)
104 |     if (
105 |       content.trim().startsWith("<!DOCTYPE") ||
106 |       content.trim().startsWith("<html")
107 |     ) {
108 |       output += `> **Error**: Received HTML instead of markdown. The path may be incorrect.\n\n`;
109 |       output += `Make sure to use the .md extension in the path.\n`;
110 |       output += `Example: /platforms/javascript/guides/nextjs.md\n`;
111 |       return output;
112 |     }
113 | 
114 |     // Add the markdown content
115 |     output += "---\n\n";
116 |     output += content;
117 |     output += "\n\n---\n\n";
118 | 
119 |     output += "## Using this documentation\n\n";
120 |     output +=
121 |       "- This is the raw markdown content from Sentry's documentation\n";
122 |     output +=
123 |       "- Code examples and configuration snippets can be copied directly\n";
124 |     output +=
125 |       "- Links in the documentation are relative to https://docs.sentry.io\n";
126 |     output +=
127 |       "- For more related topics, use `search_docs()` to find additional pages\n";
128 | 
129 |     return output;
130 |   },
131 | });
132 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/search-docs.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import { defineTool } from "../internal/tool-helpers/define";
  3 | import { fetchWithTimeout } from "../internal/fetch-utils";
  4 | import { ApiError } from "../api-client/index";
  5 | import type { ServerContext } from "../types";
  6 | import type { SearchResponse } from "./types";
  7 | import { ParamSentryGuide } from "../schema";
  8 | 
  9 | export default defineTool({
 10 |   name: "search_docs",
 11 |   requiredScopes: ["docs"], // Documentation search requires docs permission
 12 |   description: [
 13 |     "Search Sentry documentation for SDK setup, instrumentation, and configuration guidance.",
 14 |     "",
 15 |     "Use this tool when you need to:",
 16 |     "- Set up Sentry SDK or framework integrations (Django, Flask, Express, Next.js, etc.)",
 17 |     "- Configure features like performance monitoring, error sampling, or release tracking",
 18 |     "- Implement custom instrumentation (spans, transactions, breadcrumbs)",
 19 |     "- Configure data scrubbing, filtering, or sampling rules",
 20 |     "",
 21 |     "Returns snippets only. Use `get_doc(path='...')` to fetch full documentation content.",
 22 |     "",
 23 |     "<examples>",
 24 |     "```",
 25 |     "search_docs(query='Django setup configuration SENTRY_DSN', guide='python/django')",
 26 |     "search_docs(query='source maps webpack upload', guide='javascript/nextjs')",
 27 |     "```",
 28 |     "</examples>",
 29 |     "",
 30 |     "<hints>",
 31 |     "- Use guide parameter to filter to specific technologies (e.g., 'javascript/nextjs')",
 32 |     "- Include specific feature names like 'beforeSend', 'tracesSampleRate', 'SENTRY_DSN'",
 33 |     "</hints>",
 34 |   ].join("\n"),
 35 |   inputSchema: {
 36 |     query: z
 37 |       .string()
 38 |       .trim()
 39 |       .min(
 40 |         2,
 41 |         "Search query is too short. Please provide at least 2 characters.",
 42 |       )
 43 |       .max(
 44 |         200,
 45 |         "Search query is too long. Please keep your query under 200 characters.",
 46 |       )
 47 |       .describe(
 48 |         "The search query in natural language. Be specific about what you're looking for.",
 49 |       ),
 50 |     maxResults: z
 51 |       .number()
 52 |       .int()
 53 |       .min(1)
 54 |       .max(10)
 55 |       .default(3)
 56 |       .describe("Maximum number of results to return (1-10)")
 57 |       .optional(),
 58 |     guide: ParamSentryGuide.optional(),
 59 |   },
 60 |   annotations: {
 61 |     readOnlyHint: true,
 62 |     openWorldHint: true,
 63 |   },
 64 |   async handler(params, context: ServerContext) {
 65 |     let output = `# Documentation Search Results\n\n`;
 66 |     output += `**Query**: "${params.query}"\n`;
 67 |     if (params.guide) {
 68 |       output += `**Guide**: ${params.guide}\n`;
 69 |     }
 70 |     output += `\n`;
 71 | 
 72 |     // Determine the URL - use context.mcpUrl if available, otherwise default to production
 73 |     const host = context.mcpUrl || "https://mcp.sentry.dev";
 74 |     const searchUrl = new URL("/api/search", host);
 75 | 
 76 |     const response = await fetchWithTimeout(
 77 |       searchUrl.toString(),
 78 |       {
 79 |         method: "POST",
 80 |         headers: {
 81 |           "Content-Type": "application/json",
 82 |         },
 83 |         body: JSON.stringify({
 84 |           query: params.query,
 85 |           maxResults: params.maxResults,
 86 |           guide: params.guide,
 87 |         }),
 88 |       },
 89 |       15000, // 15 second timeout
 90 |     );
 91 | 
 92 |     if (!response.ok) {
 93 |       // TODO: improve error responses with types
 94 |       const errorData = (await response.json().catch(() => null)) as {
 95 |         error?: string;
 96 |       } | null;
 97 | 
 98 |       const errorMessage =
 99 |         errorData?.error || `Search failed with status ${response.status}`;
100 |       throw new ApiError(errorMessage, response.status);
101 |     }
102 | 
103 |     const data = (await response.json()) as SearchResponse;
104 | 
105 |     // Handle error in response
106 |     if ("error" in data && data.error) {
107 |       output += `**Error**: ${data.error}\n\n`;
108 |       return output;
109 |     }
110 | 
111 |     // Display results
112 |     if (data.results.length === 0) {
113 |       output += "No documentation found matching your query.\n\n";
114 |       return output;
115 |     }
116 | 
117 |     output += `Found ${data.results.length} match${data.results.length === 1 ? "" : "es"}\n\n`;
118 | 
119 |     output += `These are just snippets. Use \`get_doc(path='...')\` to fetch the full content.\n\n`;
120 | 
121 |     for (const [index, result] of data.results.entries()) {
122 |       output += `## ${index + 1}. ${result.url}\n\n`;
123 |       output += `**Path**: ${result.id}\n`;
124 |       output += `**Relevance**: ${(result.relevance * 100).toFixed(1)}%\n\n`;
125 |       if (index < 3) {
126 |         output += "**Matching Context**\n";
127 |         output += `> ${result.snippet.replace(/\n/g, "\n> ")}\n\n`;
128 |       }
129 |     }
130 | 
131 |     return output;
132 |   },
133 | });
134 | 
```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/lib/mcp-handler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * MCP Handler using experimental_createMcpHandler from Cloudflare agents library.
  3 |  *
  4 |  * Stateless request handling approach:
  5 |  * - Uses experimental_createMcpHandler to wrap the MCP server
  6 |  * - Extracts auth props directly from ExecutionContext (set by OAuth provider)
  7 |  * - Context captured in tool handler closures during buildServer()
  8 |  * - No session state required - each request is independent
  9 |  */
 10 | 
 11 | import * as Sentry from "@sentry/cloudflare";
 12 | import { experimental_createMcpHandler as createMcpHandler } from "agents/mcp";
 13 | import { buildServer } from "@sentry/mcp-server/server";
 14 | import {
 15 |   expandScopes,
 16 |   parseScopes,
 17 |   type Scope,
 18 | } from "@sentry/mcp-server/permissions";
 19 | import { logWarn } from "@sentry/mcp-server/telem/logging";
 20 | import type { ServerContext } from "@sentry/mcp-server/types";
 21 | import type { Env } from "../types";
 22 | import { verifyConstraintsAccess } from "./constraint-utils";
 23 | import type { ExportedHandler } from "@cloudflare/workers-types";
 24 | import agentTools from "@sentry/mcp-server/tools/agent-tools";
 25 | 
 26 | /**
 27 |  * ExecutionContext with OAuth props injected by the OAuth provider.
 28 |  */
 29 | type OAuthExecutionContext = ExecutionContext & {
 30 |   props?: Record<string, unknown>;
 31 | };
 32 | 
 33 | /**
 34 |  * Main request handler that:
 35 |  * 1. Extracts auth props from ExecutionContext
 36 |  * 2. Parses org/project constraints from URL
 37 |  * 3. Verifies user has access to the constraints
 38 |  * 4. Builds complete ServerContext
 39 |  * 5. Creates and configures MCP server per-request (context captured in closures)
 40 |  * 6. Runs MCP handler
 41 |  */
 42 | const mcpHandler: ExportedHandler<Env> = {
 43 |   async fetch(
 44 |     request: Request,
 45 |     env: Env,
 46 |     ctx: ExecutionContext,
 47 |   ): Promise<Response> {
 48 |     const url = new URL(request.url);
 49 | 
 50 |     // Parse constraints from URL pattern /mcp/:org?/:project?
 51 |     const pattern = new URLPattern({ pathname: "/mcp/:org?/:project?" });
 52 |     const result = pattern.exec(url);
 53 | 
 54 |     if (!result) {
 55 |       return new Response("Not found", { status: 404 });
 56 |     }
 57 | 
 58 |     const { groups } = result.pathname;
 59 |     const organizationSlug = groups?.org || null;
 60 |     const projectSlug = groups?.project || null;
 61 | 
 62 |     // Check for agent mode query parameter
 63 |     const isAgentMode = url.searchParams.get("agent") === "1";
 64 | 
 65 |     // Extract OAuth props from ExecutionContext (set by OAuth provider)
 66 |     const oauthCtx = ctx as OAuthExecutionContext;
 67 |     if (!oauthCtx.props) {
 68 |       throw new Error("No authentication context available");
 69 |     }
 70 | 
 71 |     const sentryHost = env.SENTRY_HOST || "sentry.io";
 72 | 
 73 |     // Verify user has access to the requested org/project
 74 |     const verification = await verifyConstraintsAccess(
 75 |       { organizationSlug, projectSlug },
 76 |       {
 77 |         accessToken: oauthCtx.props.accessToken as string,
 78 |         sentryHost,
 79 |       },
 80 |     );
 81 | 
 82 |     if (!verification.ok) {
 83 |       return new Response(verification.message, {
 84 |         status: verification.status ?? 500,
 85 |       });
 86 |     }
 87 | 
 88 |     // Parse and expand granted scopes
 89 |     let expandedScopes: Set<Scope> | undefined;
 90 |     if (oauthCtx.props.grantedScopes) {
 91 |       const { valid, invalid } = parseScopes(
 92 |         oauthCtx.props.grantedScopes as string[],
 93 |       );
 94 |       if (invalid.length > 0) {
 95 |         logWarn("Ignoring invalid scopes from OAuth provider", {
 96 |           loggerScope: ["cloudflare", "mcp-handler"],
 97 |           extra: {
 98 |             invalidScopes: invalid,
 99 |           },
100 |         });
101 |       }
102 |       expandedScopes = expandScopes(new Set(valid));
103 |     }
104 | 
105 |     // Build complete ServerContext from OAuth props + verified constraints
106 |     const serverContext: ServerContext = {
107 |       userId: oauthCtx.props.id as string | undefined,
108 |       clientId: oauthCtx.props.clientId as string,
109 |       accessToken: oauthCtx.props.accessToken as string,
110 |       grantedScopes: expandedScopes,
111 |       constraints: verification.constraints,
112 |       sentryHost,
113 |       mcpUrl: env.MCP_URL,
114 |     };
115 | 
116 |     // Create and configure MCP server with tools filtered by context
117 |     // Context is captured in tool handler closures during buildServer()
118 |     const server = buildServer({
119 |       context: serverContext,
120 |       tools: isAgentMode ? agentTools : undefined,
121 |       onToolComplete: () => {
122 |         // Flush Sentry events after tool execution
123 |         Sentry.flush(2000);
124 |       },
125 |     });
126 | 
127 |     // Run MCP handler - context already captured in closures
128 |     return createMcpHandler(server, {
129 |       route: url.pathname,
130 |     })(request, env, ctx);
131 |   },
132 | };
133 | 
134 | export default mcpHandler;
135 | 
```

--------------------------------------------------------------------------------
/.github/workflows/smoke-tests.yml:
--------------------------------------------------------------------------------

```yaml
  1 | name: Smoke Tests (Local)
  2 | 
  3 | permissions:
  4 |   contents: read
  5 |   checks: write
  6 | 
  7 | on:
  8 |   push:
  9 |     branches: [main]
 10 |   pull_request:
 11 | 
 12 | jobs:
 13 |   smoke-tests:
 14 |     name: Run Smoke Tests Against Local Server
 15 |     runs-on: ubuntu-latest
 16 |     
 17 |     steps:
 18 |       - uses: actions/checkout@v4
 19 | 
 20 |       - name: Setup Node.js
 21 |         uses: actions/setup-node@v4
 22 |         with:
 23 |           node-version: "20"
 24 | 
 25 |       # pnpm/action-setup@v4
 26 |       - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda
 27 |         name: Install pnpm
 28 |         with:
 29 |           run_install: false
 30 | 
 31 |       - name: Get pnpm store directory
 32 |         shell: bash
 33 |         run: |
 34 |           echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
 35 | 
 36 |       - uses: actions/cache@v4
 37 |         name: Setup pnpm cache
 38 |         with:
 39 |           path: ${{ env.STORE_PATH }}
 40 |           key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
 41 |           restore-keys: |
 42 |             ${{ runner.os }}-pnpm-store-
 43 | 
 44 |       - name: Install dependencies
 45 |         run: pnpm install --no-frozen-lockfile
 46 |       
 47 |       - name: Build
 48 |         run: pnpm build
 49 | 
 50 |       - name: Start local dev server
 51 |         working-directory: packages/mcp-cloudflare
 52 |         run: |
 53 |           # Start wrangler in background and capture output
 54 |           pnpm exec wrangler dev --port 8788 --local > wrangler.log 2>&1 &
 55 |           WRANGLER_PID=$!
 56 |           echo "WRANGLER_PID=$WRANGLER_PID" >> $GITHUB_ENV
 57 |           echo "Waiting for server to start (PID: $WRANGLER_PID)..."
 58 |           
 59 |           # Wait for server to be ready (up to 2 minutes)
 60 |           MAX_ATTEMPTS=24
 61 |           ATTEMPT=0
 62 |           while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
 63 |             # Check if wrangler process is still running
 64 |             if ! kill -0 $WRANGLER_PID 2>/dev/null; then
 65 |               echo "❌ Wrangler process died unexpectedly!"
 66 |               echo "📋 Last 50 lines of wrangler.log:"
 67 |               tail -50 wrangler.log
 68 |               exit 1
 69 |             fi
 70 |             
 71 |             if curl -s -f -o /dev/null http://localhost:8788/; then
 72 |               echo "✅ Server is ready!"
 73 |               echo "📋 Wrangler startup log:"
 74 |               cat wrangler.log
 75 |               break
 76 |             else
 77 |               echo "⏳ Waiting for server to start (attempt $((ATTEMPT+1))/$MAX_ATTEMPTS)..."
 78 |               # Show partial log every 5 attempts
 79 |               if [ $((ATTEMPT % 5)) -eq 0 ] && [ $ATTEMPT -gt 0 ]; then
 80 |                 echo "📋 Current wrangler.log output:"
 81 |                 tail -20 wrangler.log
 82 |               fi
 83 |             fi
 84 |             
 85 |             ATTEMPT=$((ATTEMPT+1))
 86 |             sleep 5
 87 |           done
 88 |           
 89 |           if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
 90 |             echo "❌ Server failed to start after $MAX_ATTEMPTS attempts"
 91 |             echo "📋 Full wrangler.log:"
 92 |             cat wrangler.log
 93 |             exit 1
 94 |           fi
 95 | 
 96 |       - name: Run smoke tests against local server
 97 |         env:
 98 |           PREVIEW_URL: http://localhost:8788
 99 |         working-directory: packages/smoke-tests
100 |         run: |
101 |           echo "🧪 Running smoke tests against local server at $PREVIEW_URL"
102 |           
103 |           # Give server a bit more time to stabilize after startup
104 |           echo "⏳ Waiting 5 seconds for server to stabilize..."
105 |           sleep 5
106 |           
107 |           # Verify server is still responding before running tests
108 |           if ! curl -s -f -o /dev/null http://localhost:8788/; then
109 |             echo "❌ Server is not responding before tests!"
110 |             echo "📋 Wrangler log:"
111 |             cat ../mcp-cloudflare/wrangler.log
112 |             exit 1
113 |           fi
114 |           
115 |           echo "✅ Server is responding, running tests..."
116 |           pnpm test:ci || TEST_EXIT_CODE=$?
117 |           
118 |           # If tests failed, show server logs for debugging
119 |           if [ "${TEST_EXIT_CODE:-0}" -ne 0 ]; then
120 |             echo "❌ Tests failed with exit code ${TEST_EXIT_CODE}"
121 |             echo "📋 Wrangler log at time of failure:"
122 |             cat ../mcp-cloudflare/wrangler.log
123 |             exit ${TEST_EXIT_CODE}
124 |           fi
125 | 
126 |       - name: Publish Smoke Test Report
127 |         uses: mikepenz/action-junit-report@cf701569b05ccdd861a76b8607a66d76f6fd4857
128 |         if: always()
129 |         with:
130 |           report_paths: "packages/smoke-tests/tests.junit.xml"
131 |           check_name: "Local Smoke Test Results"
132 |           fail_on_failure: true
133 | 
134 |       - name: Stop local server
135 |         if: always()
136 |         run: |
137 |           if [ ! -z "$WRANGLER_PID" ]; then
138 |             kill $WRANGLER_PID || true
139 |           fi
```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/routes/metadata.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * MCP Metadata API endpoint
  3 |  *
  4 |  * Provides immediate access to MCP server metadata including tools
  5 |  * without requiring a chat stream to be initialized.
  6 |  */
  7 | import { Hono } from "hono";
  8 | import { experimental_createMCPClient } from "ai";
  9 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
 10 | import type { Env } from "../types";
 11 | import { logIssue, logWarn } from "@sentry/mcp-server/telem/logging";
 12 | import type { ErrorResponse } from "../types/chat";
 13 | import { analyzeAuthError, getAuthErrorResponse } from "../utils/auth-errors";
 14 | import { z } from "zod";
 15 | 
 16 | type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient>>;
 17 | 
 18 | function createErrorResponse(errorResponse: ErrorResponse): ErrorResponse {
 19 |   return errorResponse;
 20 | }
 21 | 
 22 | export default new Hono<{ Bindings: Env }>().get("/", async (c) => {
 23 |   // Support cookie-based auth (preferred) with fallback to Authorization header
 24 |   let accessToken: string | null = null;
 25 | 
 26 |   // Try to read from signed cookie set during OAuth
 27 |   try {
 28 |     const { getCookie } = await import("hono/cookie");
 29 |     const authDataCookie = getCookie(c, "sentry_auth_data");
 30 |     if (authDataCookie) {
 31 |       const AuthDataSchema = z.object({ access_token: z.string() });
 32 |       const authData = AuthDataSchema.parse(JSON.parse(authDataCookie));
 33 |       accessToken = authData.access_token;
 34 |     }
 35 |   } catch {
 36 |     // Ignore cookie parse errors; we'll check header below
 37 |   }
 38 | 
 39 |   // Fallback to Authorization header if cookie is not present
 40 |   if (!accessToken) {
 41 |     const authHeader = c.req.header("Authorization");
 42 |     if (authHeader?.startsWith("Bearer ")) {
 43 |       accessToken = authHeader.substring(7);
 44 |     }
 45 |   }
 46 | 
 47 |   if (!accessToken) {
 48 |     return c.json(
 49 |       createErrorResponse({
 50 |         error: "Authorization required",
 51 |         name: "MISSING_AUTH_TOKEN",
 52 |       }),
 53 |       401,
 54 |     );
 55 |   }
 56 | 
 57 |   // Declare mcpClient in outer scope for cleanup in catch block
 58 |   let mcpClient: MCPClient | undefined;
 59 | 
 60 |   try {
 61 |     // Get tools by connecting to MCP server
 62 |     let tools: string[] = [];
 63 |     try {
 64 |       const requestUrl = new URL(c.req.url);
 65 |       const mcpUrl = `${requestUrl.protocol}//${requestUrl.host}/mcp`;
 66 | 
 67 |       const httpTransport = new StreamableHTTPClientTransport(new URL(mcpUrl), {
 68 |         requestInit: {
 69 |           headers: {
 70 |             Authorization: `Bearer ${accessToken}`,
 71 |           },
 72 |         },
 73 |       });
 74 | 
 75 |       mcpClient = await experimental_createMCPClient({
 76 |         name: "sentry",
 77 |         transport: httpTransport,
 78 |       });
 79 | 
 80 |       const mcpTools = await mcpClient.tools();
 81 |       tools = Object.keys(mcpTools);
 82 |     } catch (error) {
 83 |       // If we can't get tools, return empty array
 84 |       logWarn(error, {
 85 |         loggerScope: ["cloudflare", "metadata"],
 86 |         extra: {
 87 |           message: "Failed to fetch tools from MCP server",
 88 |         },
 89 |       });
 90 |     } finally {
 91 |       // Ensure the MCP client connection is properly closed to prevent hanging connections
 92 |       if (mcpClient && typeof mcpClient.close === "function") {
 93 |         try {
 94 |           await mcpClient.close();
 95 |         } catch (closeError) {
 96 |           logWarn(closeError, {
 97 |             loggerScope: ["cloudflare", "metadata"],
 98 |             extra: {
 99 |               message: "Failed to close MCP client connection",
100 |             },
101 |           });
102 |         }
103 |       }
104 |     }
105 | 
106 |     // Return the metadata
107 |     return c.json({
108 |       type: "mcp-metadata",
109 |       tools,
110 |       timestamp: new Date().toISOString(),
111 |     });
112 |   } catch (error) {
113 |     // Cleanup mcpClient if it was created
114 |     if (mcpClient && typeof mcpClient.close === "function") {
115 |       try {
116 |         await mcpClient.close();
117 |       } catch (closeError) {
118 |         logWarn(closeError, {
119 |           loggerScope: ["cloudflare", "metadata"],
120 |           extra: {
121 |             message: "Failed to close MCP client connection in error handler",
122 |           },
123 |         });
124 |       }
125 |     }
126 | 
127 |     logIssue(error, {
128 |       loggerScope: ["cloudflare", "metadata"],
129 |       extra: {
130 |         message: "Metadata API error",
131 |       },
132 |     });
133 | 
134 |     // Check if this is an authentication error
135 |     const authInfo = analyzeAuthError(error);
136 |     if (authInfo.isAuthError) {
137 |       return c.json(
138 |         createErrorResponse(getAuthErrorResponse(authInfo)),
139 |         authInfo.statusCode || (401 as any),
140 |       );
141 |     }
142 | 
143 |     const eventId = logIssue(error);
144 |     return c.json(
145 |       createErrorResponse({
146 |         error: "Failed to fetch MCP metadata",
147 |         name: "METADATA_FETCH_FAILED",
148 |         eventId,
149 |       }),
150 |       500,
151 |     );
152 |   }
153 | });
154 | 
```
Page 4/15FirstPrevNextLast