#
tokens: 49932/50000 48/422 files (page 3/12)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 of 12. Use http://codebase.md/getsentry/sentry-mcp?lines=false&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/utils/slug-validation.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Slug validation utilities to prevent path traversal and injection attacks.
 *
 * Provides reusable validation functions for use with Zod's superRefine()
 * to add security validation for URL parameters.
 */

import { z } from "zod";

/**
 * Maximum reasonable length for a slug.
 */
const MAX_SLUG_LENGTH = 100;

/**
 * Maximum reasonable length for a numeric ID.
 */
const MAX_ID_LENGTH = 20;

/**
 * Helper to check if a string is a numeric ID.
 */
export function isNumericId(value: string): boolean {
  return /^\d+$/.test(value);
}

/**
 * Valid slug pattern: alphanumeric, hyphens, underscores, and dots.
 * Must start with alphanumeric character.
 */
const VALID_SLUG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;

/**
 * Validates a slug to prevent path traversal and injection attacks.
 * Designed to be used with Zod's superRefine() method.
 *
 * @example
 * ```typescript
 * const OrganizationSlug = z.string()
 *   .toLowerCase()
 *   .trim()
 *   .superRefine(validateSlug)
 *   .describe("Organization slug");
 *
 * const TeamSlug = z.string()
 *   .toLowerCase()
 *   .trim()
 *   .superRefine(validateSlug)
 *   .describe("Team slug");
 * ```
 */
export function validateSlug(val: string, ctx: z.RefinementCtx): void {
  // Check for empty string
  if (val.length === 0) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Slug cannot be empty",
    });
    return;
  }

  // Check length
  if (val.length > MAX_SLUG_LENGTH) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: `Slug exceeds maximum length of ${MAX_SLUG_LENGTH} characters`,
    });
    return;
  }

  // Validate pattern - this implicitly blocks all dangerous characters and patterns
  if (!VALID_SLUG_PATTERN.test(val)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message:
        "Slug must contain only alphanumeric characters, hyphens, underscores, and dots, and must start with an alphanumeric character",
    });
  }
}

/**
 * Validates a parameter that can be either a slug or numeric ID.
 * Designed to be used with Zod's superRefine() method.
 *
 * @example
 * ```typescript
 * const ProjectSlugOrId = z.string()
 *   .toLowerCase()
 *   .trim()
 *   .superRefine(validateSlugOrId)
 *   .describe("Project slug or numeric ID");
 *
 * const IssueSlugOrId = z.string()
 *   .trim()
 *   .superRefine(validateSlugOrId)
 *   .describe("Issue slug or numeric ID");
 * ```
 */
export function validateSlugOrId(val: string, ctx: z.RefinementCtx): void {
  // Check if it's a numeric ID
  if (isNumericId(val)) {
    if (val.length > MAX_ID_LENGTH) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `Numeric ID exceeds maximum length of ${MAX_ID_LENGTH} characters`,
      });
    }
    // Numeric IDs don't need slug validation
    return;
  }

  // Otherwise validate as a slug
  validateSlug(val, ctx);
}

```

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

```json
{
  "namespace": "jvm",
  "description": "This document defines Java Virtual machine related attributes.\n",
  "attributes": {
    "jvm.gc.action": {
      "description": "Name of the garbage collector action.",
      "type": "string",
      "note": "Garbage collector action is generally obtained via [GarbageCollectionNotificationInfo#getGcAction()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcAction()).\n",
      "stability": "stable",
      "examples": ["end of minor GC", "end of major GC"]
    },
    "jvm.gc.cause": {
      "description": "Name of the garbage collector cause.",
      "type": "string",
      "note": "Garbage collector cause is generally obtained via [GarbageCollectionNotificationInfo#getGcCause()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcCause()).\n",
      "stability": "development",
      "examples": ["System.gc()", "Allocation Failure"]
    },
    "jvm.gc.name": {
      "description": "Name of the garbage collector.",
      "type": "string",
      "note": "Garbage collector name is generally obtained via [GarbageCollectionNotificationInfo#getGcName()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcName()).\n",
      "stability": "stable",
      "examples": ["G1 Young Generation", "G1 Old Generation"]
    },
    "jvm.memory.type": {
      "description": "The type of memory.",
      "type": "string",
      "stability": "stable",
      "examples": ["heap", "non_heap"]
    },
    "jvm.memory.pool.name": {
      "description": "Name of the memory pool.",
      "type": "string",
      "note": "Pool names are generally obtained via [MemoryPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/MemoryPoolMXBean.html#getName()).\n",
      "stability": "stable",
      "examples": ["G1 Old Gen", "G1 Eden space", "G1 Survivor Space"]
    },
    "jvm.thread.daemon": {
      "description": "Whether the thread is daemon or not.",
      "type": "boolean",
      "stability": "stable"
    },
    "jvm.thread.state": {
      "description": "State of the thread.",
      "type": "string",
      "stability": "stable",
      "examples": [
        "new",
        "runnable",
        "blocked",
        "waiting",
        "timed_waiting",
        "terminated"
      ]
    },
    "jvm.buffer.pool.name": {
      "description": "Name of the buffer pool.",
      "type": "string",
      "note": "Pool names are generally obtained via [BufferPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/BufferPoolMXBean.html#getName()).\n",
      "stability": "development",
      "examples": ["mapped", "direct"]
    }
  }
}

```

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

```typescript
import {
  SentryApiService,
  ApiClientError,
  ApiNotFoundError,
} from "../../api-client/index";
import { UserInputError } from "../../errors";
import type { ServerContext } from "../../types";
import { validateRegionUrl } from "./validate-region-url";

/**
 * Create a Sentry API service from server context with optional region override
 * @param context - Server context containing host and access token
 * @param opts - Options object containing optional regionUrl override
 * @returns Configured SentryApiService instance (always uses HTTPS)
 * @throws {UserInputError} When regionUrl is provided but invalid
 */
export function apiServiceFromContext(
  context: ServerContext,
  opts: { regionUrl?: string } = {},
) {
  let host = context.sentryHost;

  if (opts.regionUrl?.trim()) {
    // Validate the regionUrl against the base host to prevent SSRF
    // Use default host if context.sentryHost is not set
    const baseHost = context.sentryHost || "sentry.io";
    host = validateRegionUrl(opts.regionUrl.trim(), baseHost);
  }

  return new SentryApiService({
    host,
    accessToken: context.accessToken,
  });
}

/**
 * Maps API errors to user-friendly errors based on context
 * @param error - The error to handle
 * @param params - The parameters that were used in the API call
 * @returns Never - always throws an error
 * @throws {UserInputError} For 4xx errors that are likely user input issues
 * @throws {Error} For other errors
 */
export function handleApiError(
  error: unknown,
  params?: Record<string, unknown>,
): never {
  // Use the new error hierarchy - all 4xx errors extend ApiClientError
  if (error instanceof ApiClientError) {
    let message = `API error (${error.status}): ${error.message}`;

    // Special handling for 404s with parameter context
    if (error instanceof ApiNotFoundError && params) {
      const paramsList: string[] = [];
      for (const [key, value] of Object.entries(params)) {
        if (value !== undefined && value !== null && value !== "") {
          paramsList.push(`${key}: '${value}'`);
        }
      }

      if (paramsList.length > 0) {
        message = `Resource not found (404): ${error.message}\nPlease verify these parameters are correct:\n${paramsList.map((p) => `  - ${p}`).join("\n")}`;
      }
    }

    throw new UserInputError(message, { cause: error });
  }

  // All other errors bubble up (including ApiServerError for 5xx)
  throw error;
}

/**
 * Wraps an async API call with automatic error handling
 * @param fn - The async function to execute
 * @param params - The parameters that were used in the API call
 * @returns The result of the function
 * @throws {UserInputError} For user input errors
 * @throws {Error} For other errors
 */
export async function withApiErrorHandling<T>(
  fn: () => Promise<T>,
  params?: Record<string, unknown>,
): Promise<T> {
  try {
    return await fn();
  } catch (error) {
    handleApiError(error, params);
  }
}

```

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

```typescript
/**
 * Generic tool wrapper for the use_sentry embedded agent.
 *
 * Provides a single function that can wrap ANY MCP tool handler
 * to work with the embedded agent pattern.
 */

import { z } from "zod";
import { agentTool } from "../../internal/agents/tools/utils";
import type { ServerContext } from "../../types";
import type { ToolConfig } from "../types";

/**
 * Options for wrapping a tool
 */
export interface WrapToolOptions {
  context: ServerContext;
}

/**
 * Helper to inject constrained parameters into tool calls.
 * This applies session-level constraints (org, project, region) to tool parameters.
 */
function injectConstrainedParams(
  params: Record<string, any>,
  constraints: ServerContext["constraints"],
): Record<string, any> {
  const result = { ...params };

  // Apply organization constraint if set
  if (constraints.organizationSlug && !result.organizationSlug) {
    result.organizationSlug = constraints.organizationSlug;
  }

  // Apply project constraint (handle both projectSlug and projectSlugOrId)
  if (constraints.projectSlug) {
    if (!result.projectSlug) {
      result.projectSlug = constraints.projectSlug;
    }
    if (!result.projectSlugOrId) {
      result.projectSlugOrId = constraints.projectSlug;
    }
  }

  // Apply region constraint if set
  if (constraints.regionUrl && !result.regionUrl) {
    result.regionUrl = constraints.regionUrl;
  }

  return result;
}

/**
 * Wraps any MCP tool to work with the embedded agent pattern.
 *
 * This function:
 * 1. Takes a tool definition with its handler
 * 2. Creates an agentTool-wrapped version
 * 3. Pre-binds ServerContext so the agent doesn't need it
 * 4. Applies session constraints automatically
 * 5. Handles errors via agentTool's error handling
 *
 * @param tool - The MCP tool to wrap (from defineTool)
 * @param options - Context and configuration for the tool
 * @returns An agentTool-wrapped version ready for use by the embedded agent
 *
 * @example
 * ```typescript
 * const whoami = wrapToolForAgent(whoamiTool, { context });
 * const findOrgs = wrapToolForAgent(findOrganizationsTool, { context });
 * ```
 */
export function wrapToolForAgent<TSchema extends Record<string, z.ZodType>>(
  tool: ToolConfig<TSchema>,
  options: WrapToolOptions,
) {
  return agentTool({
    description: tool.description,
    parameters: z.object(tool.inputSchema),
    execute: async (params: unknown) => {
      // Type safety: params is validated by agentTool's Zod schema before reaching here
      const fullParams = injectConstrainedParams(
        params as Record<string, unknown>,
        options.context.constraints,
      );

      // Call the actual tool handler with full context
      // Type assertion is safe: fullParams matches the tool's input schema (enforced by Zod)
      const result = await tool.handler(fullParams as never, options.context);

      // Return the result - agentTool handles error wrapping
      return result;
    },
  });
}

```

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

```typescript
import { experimental_createMCPClient } from "ai";
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { startNewTrace, startSpan } from "@sentry/core";
import { logSuccess } from "./logger.js";
import type { MCPConnection, MCPConfig } from "./types.js";
import { randomUUID } from "node:crypto";
import { LIB_VERSION } from "./version.js";

export async function connectToMCPServer(
  config: MCPConfig,
): Promise<MCPConnection> {
  const sessionId = randomUUID();

  return await startNewTrace(async () => {
    return await startSpan(
      {
        name: "mcp.connect/stdio",
        attributes: {
          "mcp.transport": "stdio",
          "gen_ai.conversation.id": sessionId,
          "service.version": LIB_VERSION,
        },
      },
      async (span) => {
        try {
          const args = [
            `--access-token=${config.accessToken}`,
            "--all-scopes", // Ensure all tools are available in local stdio runs
          ];
          if (config.host) {
            args.push(`--host=${config.host}`);
          }
          if (config.sentryDsn) {
            args.push(`--sentry-dsn=${config.sentryDsn}`);
          }
          if (config.useAgentEndpoint) {
            args.push("--agent");
          }

          // Resolve the path to the mcp-server binary
          const __dirname = dirname(fileURLToPath(import.meta.url));
          const mcpServerPath = join(
            __dirname,
            "../../mcp-server/dist/index.js",
          );

          const transport = new Experimental_StdioMCPTransport({
            command: "node",
            args: [mcpServerPath, ...args],
            env: {
              ...process.env,
              SENTRY_ACCESS_TOKEN: config.accessToken,
              SENTRY_HOST: config.host || "sentry.io",
              ...(config.sentryDsn && { SENTRY_DSN: config.sentryDsn }),
            },
          });

          const client = await experimental_createMCPClient({
            name: "mcp.sentry.dev (test-client)",
            transport,
          });

          // Discover available tools
          const toolsMap = await client.tools();
          const tools = new Map<string, any>();

          for (const [name, tool] of Object.entries(toolsMap)) {
            tools.set(name, tool);
          }

          // Remove custom attributes - let SDK handle standard attributes
          span.setStatus({ code: 1 }); // OK status

          logSuccess(
            "Connected to MCP server (stdio)",
            `${tools.size} tools available`,
          );

          const disconnect = async () => {
            await client.close();
          };

          return {
            client,
            tools,
            disconnect,
            sessionId,
            transport: "stdio" as const,
          };
        } catch (error) {
          span.setStatus({ code: 2 }); // Error status
          throw error;
        }
      },
    );
  });
}

```

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

```json
{
  "namespace": "app",
  "description": "Describes attributes related to client-side applications (e.g. web apps or mobile apps).\n",
  "attributes": {
    "app.installation.id": {
      "description": "A unique identifier representing the installation of an application on a specific device\n",
      "type": "string",
      "note": "Its value SHOULD persist across launches of the same application installation, including through application upgrades.\nIt SHOULD change if the application is uninstalled or if all applications of the vendor are uninstalled.\nAdditionally, users might be able to reset this value (e.g. by clearing application data).\nIf an app is installed multiple times on the same device (e.g. in different accounts on Android), each `app.installation.id` SHOULD have a different value.\nIf multiple OpenTelemetry SDKs are used within the same application, they SHOULD use the same value for `app.installation.id`.\nHardware IDs (e.g. serial number, IMEI, MAC address) MUST NOT be used as the `app.installation.id`.\n\nFor iOS, this value SHOULD be equal to the [vendor identifier](https://developer.apple.com/documentation/uikit/uidevice/identifierforvendor).\n\nFor Android, examples of `app.installation.id` implementations include:\n\n- [Firebase Installation ID](https://firebase.google.com/docs/projects/manage-installations).\n- A globally unique UUID which is persisted across sessions in your application.\n- [App set ID](https://developer.android.com/identity/app-set-id).\n- [`Settings.getString(Settings.Secure.ANDROID_ID)`](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID).\n\nMore information about Android identifier best practices can be found [here](https://developer.android.com/training/articles/user-data-ids).\n",
      "stability": "development",
      "examples": ["2ab2916d-a51f-4ac8-80ee-45ac31a28092"]
    },
    "app.screen.coordinate.x": {
      "description": "The x (horizontal) coordinate of a screen coordinate, in screen pixels.",
      "type": "number",
      "stability": "development",
      "examples": ["0", "131"]
    },
    "app.screen.coordinate.y": {
      "description": "The y (vertical) component of a screen coordinate, in screen pixels.\n",
      "type": "number",
      "stability": "development",
      "examples": ["12", "99"]
    },
    "app.widget.id": {
      "description": "An identifier that uniquely differentiates this widget from other widgets in the same application.\n",
      "type": "string",
      "note": "A widget is an application component, typically an on-screen visual GUI element.\n",
      "stability": "development",
      "examples": ["f9bc787d-ff05-48ad-90e1-fca1d46130b3", "submit_order_1829"]
    },
    "app.widget.name": {
      "description": "The name of an application widget.",
      "type": "string",
      "note": "A widget is an application component, typically an on-screen visual GUI element.\n",
      "stability": "development",
      "examples": ["submit", "attack", "Clear Cart"]
    }
  }
}

```

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

```json
{
  "namespace": "feature_flags",
  "description": "This document defines attributes for Feature Flags.\n",
  "attributes": {
    "feature_flag.key": {
      "description": "The lookup key of the feature flag.",
      "type": "string",
      "stability": "release_candidate",
      "examples": ["logo-color"]
    },
    "feature_flag.provider.name": {
      "description": "Identifies the feature flag provider.",
      "type": "string",
      "stability": "release_candidate",
      "examples": ["Flag Manager"]
    },
    "feature_flag.result.variant": {
      "description": "A semantic identifier for an evaluated flag value.\n",
      "type": "string",
      "note": "A semantic identifier, commonly referred to as a variant, provides a means\nfor referring to a value without including the value itself. This can\nprovide additional context for understanding the meaning behind a value.\nFor example, the variant `red` maybe be used for the value `#c05543`.",
      "stability": "release_candidate",
      "examples": ["red", "true", "on"]
    },
    "feature_flag.context.id": {
      "description": "The unique identifier for the flag evaluation context. For example, the targeting key.\n",
      "type": "string",
      "stability": "release_candidate",
      "examples": ["5157782b-2203-4c80-a857-dbbd5e7761db"]
    },
    "feature_flag.version": {
      "description": "The version of the ruleset used during the evaluation. This may be any stable value which uniquely identifies the ruleset.\n",
      "type": "string",
      "stability": "release_candidate",
      "examples": ["1", "01ABCDEF"]
    },
    "feature_flag.set.id": {
      "description": "The identifier of the [flag set](https://openfeature.dev/specification/glossary/#flag-set) to which the feature flag belongs.\n",
      "type": "string",
      "stability": "release_candidate",
      "examples": ["proj-1", "ab98sgs", "service1/dev"]
    },
    "feature_flag.result.reason": {
      "description": "The reason code which shows how a feature flag value was determined.\n",
      "type": "string",
      "stability": "release_candidate",
      "examples": [
        "static",
        "default",
        "targeting_match",
        "split",
        "cached",
        "disabled",
        "unknown",
        "stale",
        "error"
      ]
    },
    "feature_flag.result.value": {
      "description": "The evaluated value of the feature flag.",
      "type": "string",
      "note": "With some feature flag providers, feature flag results can be quite large or contain private or sensitive details.\nBecause of this, `feature_flag.result.variant` is often the preferred attribute if it is available.\n\nIt may be desirable to redact or otherwise limit the size and scope of `feature_flag.result.value` if possible.\nBecause the evaluated flag value is unstructured and may be any type, it is left to the instrumentation author to determine how best to achieve this.\n",
      "stability": "release_candidate",
      "examples": ["#ff0000", "true", "3"]
    }
  }
}

```

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

```typescript
import { describe, it, expect } from "vitest";
import {
  isTerminalStatus,
  isHumanInterventionStatus,
  getStatusDisplayName,
  getHumanInterventionGuidance,
} from "./seer";

describe("seer-utils", () => {
  describe("isTerminalStatus", () => {
    it("returns true for terminal statuses", () => {
      expect(isTerminalStatus("COMPLETED")).toBe(true);
      expect(isTerminalStatus("FAILED")).toBe(true);
      expect(isTerminalStatus("ERROR")).toBe(true);
      expect(isTerminalStatus("CANCELLED")).toBe(true);
      expect(isTerminalStatus("NEED_MORE_INFORMATION")).toBe(true);
      expect(isTerminalStatus("WAITING_FOR_USER_RESPONSE")).toBe(true);
    });

    it("returns false for non-terminal statuses", () => {
      expect(isTerminalStatus("PROCESSING")).toBe(false);
      expect(isTerminalStatus("IN_PROGRESS")).toBe(false);
      expect(isTerminalStatus("PENDING")).toBe(false);
    });
  });

  describe("isHumanInterventionStatus", () => {
    it("returns true for human intervention statuses", () => {
      expect(isHumanInterventionStatus("NEED_MORE_INFORMATION")).toBe(true);
      expect(isHumanInterventionStatus("WAITING_FOR_USER_RESPONSE")).toBe(true);
    });

    it("returns false for other statuses", () => {
      expect(isHumanInterventionStatus("COMPLETED")).toBe(false);
      expect(isHumanInterventionStatus("PROCESSING")).toBe(false);
      expect(isHumanInterventionStatus("FAILED")).toBe(false);
    });
  });

  describe("getStatusDisplayName", () => {
    it("returns friendly names for known statuses", () => {
      expect(getStatusDisplayName("COMPLETED")).toBe("Complete");
      expect(getStatusDisplayName("FAILED")).toBe("Failed");
      expect(getStatusDisplayName("ERROR")).toBe("Failed");
      expect(getStatusDisplayName("CANCELLED")).toBe("Cancelled");
      expect(getStatusDisplayName("NEED_MORE_INFORMATION")).toBe(
        "Needs More Information",
      );
      expect(getStatusDisplayName("WAITING_FOR_USER_RESPONSE")).toBe(
        "Waiting for Response",
      );
      expect(getStatusDisplayName("PROCESSING")).toBe("Processing");
      expect(getStatusDisplayName("IN_PROGRESS")).toBe("In Progress");
    });

    it("returns status as-is for unknown statuses", () => {
      expect(getStatusDisplayName("UNKNOWN_STATUS")).toBe("UNKNOWN_STATUS");
    });
  });

  describe("getHumanInterventionGuidance", () => {
    it("returns guidance for NEED_MORE_INFORMATION", () => {
      const guidance = getHumanInterventionGuidance("NEED_MORE_INFORMATION");
      expect(guidance).toContain("Seer needs additional information");
    });

    it("returns guidance for WAITING_FOR_USER_RESPONSE", () => {
      const guidance = getHumanInterventionGuidance(
        "WAITING_FOR_USER_RESPONSE",
      );
      expect(guidance).toContain("Seer is waiting for your response");
    });

    it("returns empty string for other statuses", () => {
      expect(getHumanInterventionGuidance("COMPLETED")).toBe("");
      expect(getHumanInterventionGuidance("PROCESSING")).toBe("");
    });
  });
});

```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/find-organizations.ts:
--------------------------------------------------------------------------------

```typescript
import { defineTool } from "../internal/tool-helpers/define";
import { apiServiceFromContext } from "../internal/tool-helpers/api";
import type { ServerContext } from "../types";
import { ParamSearchQuery } from "../schema";

const RESULT_LIMIT = 25;

export default defineTool({
  name: "find_organizations",
  requiredScopes: ["org:read"],
  description: [
    "Find organizations that the user has access to in Sentry.",
    "",
    "Use this tool when you need to:",
    "- View organizations in Sentry",
    "- Find an organization's slug to aid other tool requests",
    "- Search for specific organizations by name or slug",
    "",
    `Returns up to ${RESULT_LIMIT} results. If you hit this limit, use the query parameter to narrow down results.`,
  ].join("\n"),
  inputSchema: {
    query: ParamSearchQuery.optional(),
  },
  annotations: {
    readOnlyHint: true,
    openWorldHint: true,
  },
  async handler(params, context: ServerContext) {
    // User data endpoints (like /users/me/regions/) should never use regionUrl
    // as they must always query the main API server, not region-specific servers
    const apiService = apiServiceFromContext(context);
    const organizations = await apiService.listOrganizations({
      query: params.query,
    });

    let output = "# Organizations\n\n";

    if (params.query) {
      output += `**Search query:** "${params.query}"\n\n`;
    }

    if (organizations.length === 0) {
      output += params.query
        ? `No organizations found matching "${params.query}".\n`
        : "You don't appear to be a member of any organizations.\n";
      return output;
    }

    output += organizations
      .map((org) =>
        [
          `## **${org.slug}**`,
          "",
          `**Web URL:** ${org.links?.organizationUrl || "Not available"}`,
          `**Region URL:** ${org.links?.regionUrl || ""}`,
        ].join("\n"),
      )
      .join("\n\n");

    if (organizations.length === RESULT_LIMIT) {
      output += `\n\n---\n\n**Note:** Showing ${RESULT_LIMIT} results (maximum). There may be more organizations available. Use the \`query\` parameter to search for specific organizations.`;
    }

    output += "\n\n# Using this information\n\n";
    output += `- The organization's name is the identifier for the organization, and is used in many tools for \`organizationSlug\`.\n`;

    const hasValidRegionUrls = organizations.some((org) =>
      org.links?.regionUrl?.trim(),
    );

    if (hasValidRegionUrls) {
      output += `- If a tool supports passing in the \`regionUrl\`, you MUST pass in the correct value shown above for each organization.\n`;
      output += `- For Sentry's Cloud Service (sentry.io), always use the regionUrl to ensure requests go to the correct region.\n`;
    } else {
      output += `- This appears to be a self-hosted Sentry installation. You can omit the \`regionUrl\` parameter when using other tools.\n`;
      output += `- For self-hosted Sentry, the regionUrl is typically empty and not needed for API calls.\n`;
    }

    return output;
  },
});

```

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

```typescript
import { describe, it, expect } from "vitest";
import { ApiNotFoundError, createApiError } from "../../api-client";
import { UserInputError } from "../../errors";
import { handleApiError, withApiErrorHandling } from "./api";

describe("handleApiError", () => {
  it("converts 404 errors with params to list all parameters", () => {
    const error = new ApiNotFoundError("Not Found");

    expect(() =>
      handleApiError(error, {
        organizationSlug: "my-org",
        issueId: "PROJ-123",
      }),
    ).toThrow(UserInputError);

    expect(() =>
      handleApiError(error, {
        organizationSlug: "my-org",
        issueId: "PROJ-123",
      }),
    ).toThrow(
      "Resource not found (404): Not Found\nPlease verify these parameters are correct:\n  - organizationSlug: 'my-org'\n  - issueId: 'PROJ-123'",
    );
  });

  it("converts 404 errors with multiple params including nullish values", () => {
    const error = new ApiNotFoundError("Not Found");

    expect(() =>
      handleApiError(error, {
        organizationSlug: "my-org",
        projectSlug: "my-project",
        query: undefined,
        sortBy: null,
        limit: 0,
        emptyString: "",
      }),
    ).toThrow(
      "Resource not found (404): Not Found\nPlease verify these parameters are correct:\n  - organizationSlug: 'my-org'\n  - projectSlug: 'my-project'\n  - limit: '0'",
    );
  });

  it("converts 404 errors with no params to generic message", () => {
    const error = new ApiNotFoundError("Not Found");

    expect(() => handleApiError(error, {})).toThrow(
      "API error (404): Not Found",
    );
  });

  it("converts 400 errors to UserInputError", () => {
    const error = createApiError("Invalid parameters", 400);

    expect(() => handleApiError(error)).toThrow(UserInputError);

    expect(() => handleApiError(error)).toThrow(
      "API error (400): Invalid parameters",
    );
  });

  it("converts 403 errors to UserInputError with access message", () => {
    const error = createApiError("Forbidden", 403);

    expect(() => handleApiError(error)).toThrow("API error (403): Forbidden");
  });

  it("re-throws non-API errors unchanged", () => {
    const error = new Error("Network error");

    expect(() => handleApiError(error)).toThrow(error);
  });
});

describe("withApiErrorHandling", () => {
  it("returns successful results unchanged", async () => {
    const result = await withApiErrorHandling(
      async () => ({ id: "123", title: "Test Issue" }),
      { issueId: "PROJ-123" },
    );

    expect(result).toEqual({ id: "123", title: "Test Issue" });
  });

  it("handles errors through handleApiError", async () => {
    const error = new ApiNotFoundError("Not Found");

    await expect(
      withApiErrorHandling(
        async () => {
          throw error;
        },
        {
          organizationSlug: "my-org",
          issueId: "PROJ-123",
        },
      ),
    ).rejects.toThrow(
      "Resource not found (404): Not Found\nPlease verify these parameters are correct:\n  - organizationSlug: 'my-org'\n  - issueId: 'PROJ-123'",
    );
  });
});

```

--------------------------------------------------------------------------------
/benchmark-agent.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# Benchmark script for comparing direct vs agent mode performance
# Usage: ./benchmark-agent.sh [iterations]

ITERATIONS=${1:-10}
QUERY="what organizations do I have access to?"

echo "=========================================="
echo "MCP Agent Performance Benchmark"
echo "=========================================="
echo "Query: $QUERY"
echo "Iterations: $ITERATIONS"
echo ""

# Arrays to store timings
declare -a direct_times
declare -a agent_times

echo "Running direct mode tests..."
for i in $(seq 1 $ITERATIONS); do
    echo -n "  Run $i/$ITERATIONS... "

    # Run and capture timing (real time in seconds)
    START=$(date +%s.%N)
    pnpm -w run cli "$QUERY" > /dev/null 2>&1
    END=$(date +%s.%N)

    # Calculate duration
    DURATION=$(echo "$END - $START" | bc)
    direct_times+=($DURATION)

    echo "${DURATION}s"
done

echo ""
echo "Running agent mode tests..."
for i in $(seq 1 $ITERATIONS); do
    echo -n "  Run $i/$ITERATIONS... "

    # Run and capture timing
    START=$(date +%s.%N)
    pnpm -w run cli --agent "$QUERY" > /dev/null 2>&1
    END=$(date +%s.%N)

    # Calculate duration
    DURATION=$(echo "$END - $START" | bc)
    agent_times+=($DURATION)

    echo "${DURATION}s"
done

echo ""
echo "=========================================="
echo "Results"
echo "=========================================="

# Calculate statistics for direct mode
direct_sum=0
direct_min=${direct_times[0]}
direct_max=${direct_times[0]}
for time in "${direct_times[@]}"; do
    direct_sum=$(echo "$direct_sum + $time" | bc)
    if (( $(echo "$time < $direct_min" | bc -l) )); then
        direct_min=$time
    fi
    if (( $(echo "$time > $direct_max" | bc -l) )); then
        direct_max=$time
    fi
done
direct_avg=$(echo "scale=2; $direct_sum / $ITERATIONS" | bc)

# Calculate statistics for agent mode
agent_sum=0
agent_min=${agent_times[0]}
agent_max=${agent_times[0]}
for time in "${agent_times[@]}"; do
    agent_sum=$(echo "$agent_sum + $time" | bc)
    if (( $(echo "$time < $agent_min" | bc -l) )); then
        agent_min=$time
    fi
    if (( $(echo "$time > $agent_max" | bc -l) )); then
        agent_max=$time
    fi
done
agent_avg=$(echo "scale=2; $agent_sum / $ITERATIONS" | bc)

# Calculate difference
diff=$(echo "scale=2; $agent_avg - $direct_avg" | bc)
percent=$(echo "scale=1; ($agent_avg - $direct_avg) / $direct_avg * 100" | bc)

echo ""
echo "Direct Mode:"
echo "  Min:     ${direct_min}s"
echo "  Max:     ${direct_max}s"
echo "  Average: ${direct_avg}s"
echo ""
echo "Agent Mode:"
echo "  Min:     ${agent_min}s"
echo "  Max:     ${agent_max}s"
echo "  Average: ${agent_avg}s"
echo ""
echo "Difference:"
if (( $(echo "$diff > 0" | bc -l) )); then
  echo "  +${diff}s (${percent}% slower)"
elif (( $(echo "$diff < 0" | bc -l) )); then
  abs_diff=$(echo "scale=2; -1 * $diff" | bc)
  abs_percent=$(echo "scale=1; -1 * $percent" | bc)
  echo "  -${abs_diff}s (${abs_percent}% faster)"
else
  echo "  No difference (0%)"
fi
echo ""

# Show all individual results
echo "All timings:"
echo "  Direct: ${direct_times[*]}"
echo "  Agent:  ${agent_times[*]}"

```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/ui/interactive-markdown.tsx:
--------------------------------------------------------------------------------

```typescript
/**
 * Markdown component that makes slash commands clickable
 */
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { cn } from "@/client/lib/utils";
import { Markdown } from "./markdown";

interface InteractiveMarkdownProps {
  children: string;
  className?: string;
  hasSlashCommands?: boolean;
  onSlashCommand?: (command: string) => void;
}

export function InteractiveMarkdown({
  children,
  className,
  hasSlashCommands,
  onSlashCommand,
}: InteractiveMarkdownProps) {
  // If this content has slash commands and we have a handler, create custom renderer
  if (hasSlashCommands && onSlashCommand) {
    return (
      <ReactMarkdown
        className={cn(
          "prose prose-invert prose-slate max-w-none",
          "prose-p:my-2 prose-p:leading-relaxed",
          "prose-pre:bg-slate-900 prose-pre:border prose-pre:border-slate-700",
          "prose-code:bg-slate-800 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm",
          "prose-code:before:content-none prose-code:after:content-none",
          "prose-strong:text-slate-100",
          "prose-em:text-slate-200",
          "prose-a:text-violet-300",
          "prose-blockquote:border-l-violet-500 prose-blockquote:bg-slate-800/50 prose-blockquote:py-2 prose-blockquote:px-4",
          "prose-h1:text-slate-100 prose-h2:text-slate-100 prose-h3:text-slate-100",
          "prose-h4:text-slate-100 prose-h5:text-slate-100 prose-h6:text-slate-100",
          "prose-ul:my-2 prose-ol:my-2",
          "prose-li:my-1",
          "prose-hr:border-slate-700",
          "prose-table:border-slate-700",
          "prose-th:border-slate-700 prose-td:border-slate-700",
          className,
        )}
        remarkPlugins={[remarkGfm]}
        disallowedElements={["script", "style", "iframe", "object", "embed"]}
        unwrapDisallowed={true}
        components={{
          // Custom renderer for code that might contain slash commands
          code: ({ children, ref, ...props }) => {
            const text = String(children);
            if (text.startsWith("/") && text.match(/^\/[a-zA-Z]+$/)) {
              // This is a slash command, make it clickable
              const command = text.slice(1);
              return (
                <button
                  onClick={() => onSlashCommand(command)}
                  className="inline-flex items-center gap-1 px-1 py-0.5 text-xs bg-blue-900/50 border border-blue-700/50 rounded text-blue-300 hover:bg-blue-800/50 hover:border-blue-600/50 transition-colors font-mono cursor-pointer"
                  type="button"
                  {...props}
                >
                  {text}
                </button>
              );
            }
            // Regular code rendering
            return (
              <code ref={ref as any} {...props}>
                {children}
              </code>
            );
          },
        }}
      >
        {children}
      </ReactMarkdown>
    );
  }

  // Otherwise, render as normal markdown
  return <Markdown className={className}>{children}</Markdown>;
}

```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/index.css:
--------------------------------------------------------------------------------

```css
@import "tailwindcss";
@import "tw-animate-css";

@custom-variant dark (&:is(.dark *));
@plugin "@tailwindcss/typography";

:root {
  --background: oklch(0.13 0.028 261.692);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.145 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.145 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.985 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.396 0.141 25.723);
  --destructive-foreground: oklch(0.637 0.237 25.331);
  --border: oklch(0.269 0 0);
  --input: oklch(0.269 0 0);
  --ring: oklch(0.439 0 0);
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(0.269 0 0);
  --sidebar-ring: oklch(0.439 0 0);
}

@theme inline {
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-popover: var(--popover);
  --color-popover-foreground: var(--popover-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --color-chart-1: var(--chart-1);
  --color-chart-2: var(--chart-2);
  --color-chart-3: var(--chart-3);
  --color-chart-4: var(--chart-4);
  --color-chart-5: var(--chart-5);
  --color-sidebar: var(--sidebar);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-ring: var(--sidebar-ring);
}

@layer base {
  * {
    @apply border-border outline-ring/50;
  }

  body {
    @apply bg-background text-foreground bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950;
    min-height: 100vh;
  }

  pre {
    white-space: pre-wrap;
  }
}

```

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

```typescript
import { describe, it, expect, vi, beforeEach } from "vitest";
import "urlpattern-polyfill";
import type { Env } from "../types";
import type { ExecutionContext } from "@cloudflare/workers-types";
import handler from "./mcp-handler.js";

// Mock Sentry to avoid actual telemetry
vi.mock("@sentry/cloudflare", () => ({
  flush: vi.fn(() => Promise.resolve(true)),
}));

// Mock the MCP handler creation - we're testing the wrapper logic, not the MCP protocol
vi.mock("agents/mcp", () => ({
  experimental_createMcpHandler: vi.fn(() => {
    return vi.fn(() => {
      return Promise.resolve(new Response("OK", { status: 200 }));
    });
  }),
}));

describe("mcp-handler", () => {
  let env: Env;
  let ctx: ExecutionContext & { props?: Record<string, unknown> };

  beforeEach(() => {
    vi.clearAllMocks();

    env = {
      SENTRY_HOST: "sentry.io",
      COOKIE_SECRET: "test-secret",
    } as Env;

    // ExecutionContext with OAuth props (set by OAuth provider)
    ctx = {
      waitUntil: vi.fn(),
      passThroughOnException: vi.fn(),
      props: {
        id: "test-user-123",
        clientId: "test-client",
        accessToken: "test-token",
        grantedScopes: ["org:read", "project:read"],
      },
    };
  });

  it("successfully handles request with org constraint", async () => {
    const request = new Request(
      "https://test.mcp.sentry.io/mcp/sentry-mcp-evals",
    );

    const response = await handler.fetch!(request as any, env, ctx);

    // Verify successful response
    expect(response.status).toBe(200);
  });

  it("returns 404 for invalid organization", async () => {
    const request = new Request(
      "https://test.mcp.sentry.io/mcp/nonexistent-org",
    );

    const response = await handler.fetch!(request as any, env, ctx);

    expect(response.status).toBe(404);
    expect(await response.text()).toContain("not found");
  });

  it("returns 404 for invalid project", async () => {
    const request = new Request(
      "https://test.mcp.sentry.io/mcp/sentry-mcp-evals/nonexistent-project",
    );

    const response = await handler.fetch!(request as any, env, ctx);

    expect(response.status).toBe(404);
    expect(await response.text()).toContain("not found");
  });

  it("returns error when authentication context is missing", async () => {
    const ctxWithoutAuth = {
      waitUntil: vi.fn(),
      passThroughOnException: vi.fn(),
      props: undefined,
    };

    const request = new Request("https://test.mcp.sentry.io/mcp");

    await expect(
      handler.fetch!(request as any, env, ctxWithoutAuth as any),
    ).rejects.toThrow("No authentication context");
  });

  it("successfully handles request with org and project constraints", async () => {
    const request = new Request(
      "https://test.mcp.sentry.io/mcp/sentry-mcp-evals/cloudflare-mcp",
    );

    const response = await handler.fetch!(request as any, env, ctx);

    // Verify successful response
    expect(response.status).toBe(200);
  });

  it("successfully handles request without constraints", async () => {
    const request = new Request("https://test.mcp.sentry.io/mcp");

    const response = await handler.fetch!(request as any, env, ctx);

    // Verify successful response
    expect(response.status).toBe(200);
  });
});

```

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

```typescript
import { describeEval } from "vitest-evals";
import { FIXTURES, NoOpTaskRunner, ToolPredictionScorer } from "./utils";

// Note: This eval requires OPENAI_API_KEY to be set in the environment
// The search_events tool uses the AI SDK to translate natural language queries
describeEval("search-events", {
  data: async () => {
    return [
      // Core test: Basic error event search
      {
        input: `Find database timeouts in ${FIXTURES.organizationSlug} from the last week`,
        expectedTools: [
          {
            name: "find_organizations",
            arguments: {},
          },
          {
            name: "search_events",
            arguments: {
              organizationSlug: FIXTURES.organizationSlug,
              naturalLanguageQuery: "database timeouts from the last week",
              dataset: "errors",
            },
          },
        ],
      },
      // Core test: Performance spans search
      {
        input: `Find slow API calls taking over 5 seconds in ${FIXTURES.organizationSlug}`,
        expectedTools: [
          {
            name: "find_organizations",
            arguments: {},
          },
          {
            name: "search_events",
            arguments: {
              organizationSlug: FIXTURES.organizationSlug,
              naturalLanguageQuery: "slow API calls taking over 5 seconds",
              dataset: "spans",
            },
          },
        ],
      },
      // Core test: Logs search
      {
        input: `Show me error logs from the last hour in ${FIXTURES.organizationSlug}`,
        expectedTools: [
          {
            name: "find_organizations",
            arguments: {},
          },
          {
            name: "search_events",
            arguments: {
              organizationSlug: FIXTURES.organizationSlug,
              naturalLanguageQuery: "error logs from the last hour",
              dataset: "logs",
            },
          },
        ],
      },
      // Core test: Project-specific search
      {
        input: `Show me authentication errors in ${FIXTURES.organizationSlug}/${FIXTURES.projectSlug}`,
        expectedTools: [
          {
            name: "find_organizations",
            arguments: {},
          },
          {
            name: "search_events",
            arguments: {
              organizationSlug: FIXTURES.organizationSlug,
              projectSlug: FIXTURES.projectSlug,
              naturalLanguageQuery: "authentication errors",
              dataset: "errors",
            },
          },
        ],
      },
      // Core test: Search with 'me' reference
      {
        input: `Show me errors affecting me in ${FIXTURES.organizationSlug}`,
        expectedTools: [
          {
            name: "find_organizations",
            arguments: {},
          },
          {
            name: "whoami",
            arguments: {},
          },
          {
            name: "search_events",
            arguments: {
              organizationSlug: FIXTURES.organizationSlug,
              naturalLanguageQuery: "errors affecting user.id:12345",
              dataset: "errors",
            },
          },
        ],
      },
    ];
  },
  task: NoOpTaskRunner(),
  scorers: [ToolPredictionScorer()],
  threshold: 0.6,
  timeout: 30000,
});

```

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

```typescript
interface ScrubPattern {
  pattern: RegExp;
  replacement: string;
  description: string;
}

// Patterns for sensitive data that should be scrubbed
// Pre-compile patterns with global flag for replacement
const SCRUB_PATTERNS: ScrubPattern[] = [
  {
    pattern: /\bsk-[a-zA-Z0-9]{48}\b/g,
    replacement: "[REDACTED_OPENAI_KEY]",
    description: "OpenAI API key",
  },
  {
    pattern: /\bBearer\s+[a-zA-Z0-9\-._~+/]+={0,}/g,
    replacement: "Bearer [REDACTED_TOKEN]",
    description: "Bearer token",
  },
  {
    pattern: /\bsntrys_[a-zA-Z0-9_]+\b/g,
    replacement: "[REDACTED_SENTRY_TOKEN]",
    description: "Sentry access token",
  },
];

// Maximum depth for recursive scrubbing to prevent stack overflow
const MAX_SCRUB_DEPTH = 20;

/**
 * Recursively scrub sensitive data from any value.
 * Returns tuple of [scrubbedValue, didScrub, descriptionsOfMatchedPatterns]
 */
function scrubValue(value: unknown, depth = 0): [unknown, boolean, string[]] {
  // Prevent stack overflow by limiting recursion depth
  if (depth >= MAX_SCRUB_DEPTH) {
    return ["[MAX_DEPTH_EXCEEDED]", false, []];
  }

  if (typeof value === "string") {
    let scrubbed = value;
    let didScrub = false;
    const matchedDescriptions: string[] = [];

    for (const { pattern, replacement, description } of SCRUB_PATTERNS) {
      // Reset lastIndex to avoid stateful regex issues
      pattern.lastIndex = 0;
      if (pattern.test(scrubbed)) {
        didScrub = true;
        matchedDescriptions.push(description);
        // Reset again before replace
        pattern.lastIndex = 0;
        scrubbed = scrubbed.replace(pattern, replacement);
      }
    }
    return [scrubbed, didScrub, matchedDescriptions];
  }

  if (Array.isArray(value)) {
    let arrayDidScrub = false;
    const arrayDescriptions: string[] = [];
    const scrubbedArray = value.map((item) => {
      const [scrubbed, didScrub, descriptions] = scrubValue(item, depth + 1);
      if (didScrub) {
        arrayDidScrub = true;
        arrayDescriptions.push(...descriptions);
      }
      return scrubbed;
    });
    return [scrubbedArray, arrayDidScrub, arrayDescriptions];
  }

  if (value && typeof value === "object") {
    let objectDidScrub = false;
    const objectDescriptions: string[] = [];
    const scrubbed: Record<string, unknown> = {};
    for (const [key, val] of Object.entries(value)) {
      const [scrubbedVal, didScrub, descriptions] = scrubValue(val, depth + 1);
      if (didScrub) {
        objectDidScrub = true;
        objectDescriptions.push(...descriptions);
      }
      scrubbed[key] = scrubbedVal;
    }
    return [scrubbed, objectDidScrub, objectDescriptions];
  }

  return [value, false, []];
}

/**
 * Sentry beforeSend hook that scrubs sensitive data from events
 */
export function sentryBeforeSend(event: any, hint: any): any {
  // Always scrub the entire event
  const [scrubbedEvent, didScrub, descriptions] = scrubValue(event);

  // Log to console if we found and scrubbed sensitive data
  // (avoiding LogTape dependency for edge/browser compatibility)
  if (didScrub) {
    const uniqueDescriptions = [...new Set(descriptions)];
    console.warn(
      `[Sentry] Event contained sensitive data: ${uniqueDescriptions.join(", ")}`,
    );
  }

  return scrubbedEvent as any;
}

```

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

```typescript
#!/usr/bin/env tsx
/**
 * Generate tool definitions JSON for external consumption.
 *
 * Outputs to src/ so they can be bundled and imported by clients and the Cloudflare app.
 */
import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { z, type ZodTypeAny } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Lazy imports of server modules to avoid type bleed
const toolsModule = await import("../src/tools/index.ts");

function writeJson(file: string, data: unknown) {
  fs.writeFileSync(file, JSON.stringify(data, null, 2));
}

function ensureDirExists(dir: string) {
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}

// Shared helpers for Zod parameter maps
function zodFieldMapToDescriptions(
  fieldMap: Record<string, ZodTypeAny>,
): Record<string, { description: string }> {
  const out: Record<string, { description: string }> = {};
  for (const [key, schema] of Object.entries(fieldMap)) {
    const js = zodToJsonSchema(schema, { $refStrategy: "none" }) as {
      description?: string;
    };
    out[key] = { description: js.description || "" };
  }
  return out;
}

function zodFieldMapToJsonSchema(
  fieldMap: Record<string, ZodTypeAny>,
): unknown {
  if (!fieldMap || Object.keys(fieldMap).length === 0) return {};
  const obj = z.object(fieldMap);
  return zodToJsonSchema(obj, { $refStrategy: "none" });
}

function byName<T extends { name: string }>(a: T, b: T) {
  return a.name.localeCompare(b.name);
}

// Tools
function generateToolDefinitions() {
  const toolsDefault = toolsModule.default as
    | Record<string, unknown>
    | undefined;
  if (!toolsDefault || typeof toolsDefault !== "object") {
    throw new Error("Failed to import tools from src/tools/index.ts");
  }

  const defs = Object.entries(toolsDefault).map(([key, tool]) => {
    if (!tool || typeof tool !== "object")
      throw new Error(`Invalid tool: ${key}`);
    const t = tool as {
      name: string;
      description: string;
      inputSchema: Record<string, ZodTypeAny>;
      requiredScopes: string[]; // must exist on all tools (can be empty)
    };
    if (!Array.isArray(t.requiredScopes)) {
      throw new Error(`Tool '${t.name}' is missing requiredScopes array`);
    }
    const jsonSchema = zodFieldMapToJsonSchema(t.inputSchema || {});
    return {
      name: t.name,
      description: t.description,
      // Export full JSON Schema under inputSchema for external docs
      inputSchema: jsonSchema,
      // Preserve tool access requirements for UIs/docs
      requiredScopes: t.requiredScopes,
    };
  });
  return defs.sort(byName);
}

async function main() {
  try {
    console.log("Generating tool definitions...");
    const outDir = path.join(__dirname, "../src");
    ensureDirExists(outDir);

    const tools = generateToolDefinitions();

    writeJson(path.join(outDir, "toolDefinitions.json"), tools);

    console.log(`✅ Generated: tools(${tools.length})`);
  } catch (error) {
    const err = error as Error;
    console.error("[ERROR]", err.message, err.stack);
    process.exit(1);
  }
}

if (import.meta.url === `file://${process.argv[1]}`) {
  void main();
}

```

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

```typescript
import { describe, it, expect } from "vitest";
import "urlpattern-polyfill";
import { verifyConstraintsAccess } from "./constraint-utils";

describe("verifyConstraintsAccess", () => {
  const token = "test-token";
  const host = "sentry.io";

  it("returns ok with empty constraints when no org constraint provided", async () => {
    const result = await verifyConstraintsAccess(
      { organizationSlug: null, projectSlug: null },
      { accessToken: token, sentryHost: host },
    );
    expect(result).toEqual({
      ok: true,
      constraints: {
        organizationSlug: null,
        projectSlug: null,
        regionUrl: null,
      },
    });
  });

  it("fails when access token is missing, null, undefined, or empty", async () => {
    const testCases = [
      { accessToken: "", label: "empty" },
      { accessToken: null, label: "null" },
      { accessToken: undefined, label: "undefined" },
    ];

    for (const { accessToken, label } of testCases) {
      const result = await verifyConstraintsAccess(
        { organizationSlug: "org", projectSlug: null },
        { accessToken, sentryHost: host },
      );
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.status).toBe(401);
        expect(result.message).toBe(
          "Missing access token for constraint verification",
        );
      }
    }
  });

  it("successfully verifies org access and returns constraints with regionUrl", async () => {
    const result = await verifyConstraintsAccess(
      { organizationSlug: "sentry-mcp-evals", projectSlug: null },
      { accessToken: token, sentryHost: host },
    );
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.constraints).toEqual({
        organizationSlug: "sentry-mcp-evals",
        projectSlug: null,
        regionUrl: "https://us.sentry.io",
      });
    }
  });

  it("successfully verifies org and project access", async () => {
    const result = await verifyConstraintsAccess(
      { organizationSlug: "sentry-mcp-evals", projectSlug: "cloudflare-mcp" },
      { accessToken: token, sentryHost: host },
    );
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.constraints).toEqual({
        organizationSlug: "sentry-mcp-evals",
        projectSlug: "cloudflare-mcp",
        regionUrl: "https://us.sentry.io",
      });
    }
  });

  it("fails when org does not exist", async () => {
    const result = await verifyConstraintsAccess(
      { organizationSlug: "nonexistent-org", projectSlug: null },
      { accessToken: token, sentryHost: host },
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.status).toBe(404);
      expect(result.message).toBe("Organization 'nonexistent-org' not found");
    }
  });

  it("fails when project does not exist", async () => {
    const result = await verifyConstraintsAccess(
      {
        organizationSlug: "sentry-mcp-evals",
        projectSlug: "nonexistent-project",
      },
      { accessToken: token, sentryHost: host },
    );
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.status).toBe(404);
      expect(result.message).toBe(
        "Project 'nonexistent-project' not found in organization 'sentry-mcp-evals'",
      );
    }
  });
});

```

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

```typescript
import { describeEval } from "vitest-evals";
import { ToolCallScorer } from "vitest-evals";
import { searchIssuesAgent } from "@sentry/mcp-server/tools/search-issues/agent";
import { SentryApiService } from "@sentry/mcp-server/api-client";
import { StructuredOutputScorer } from "./utils/structuredOutputScorer";
import "../setup-env";

// The shared MSW server is already started in setup-env.ts

describeEval("search-issues-agent", {
  data: async () => {
    return [
      {
        // Simple query with common fields - should NOT require tool calls
        input: "Show me unresolved issues",
        expectedTools: [],
        expected: {
          query: "is:unresolved",
          sort: "date", // Agent uses "date" as default
        },
      },
      {
        // Query with "me" reference - should only require whoami
        input: "Show me issues assigned to me",
        expectedTools: [
          {
            name: "whoami",
            arguments: {},
          },
        ],
        expected: {
          query:
            /assignedOrSuggested:test@example\.com|assigned:test@example\.com|assigned:me/, // Various valid forms
          sort: "date",
        },
      },
      {
        // Complex query but with common fields - should NOT require tool calls
        // NOTE: AI often incorrectly uses firstSeen instead of lastSeen - known limitation
        input: "Show me critical unhandled errors from the last 24 hours",
        expectedTools: [],
        expected: {
          query: /level:error.*is:unresolved.*lastSeen:-24h/,
          sort: "date",
        },
      },
      {
        // Query with custom/uncommon field that would require discovery
        input: "Show me issues with custom.payment.failed tag",
        expectedTools: [
          {
            name: "issueFields",
            arguments: {}, // No arguments needed anymore
          },
        ],
        expected: {
          query: /custom\.payment\.failed|tags\[custom\.payment\.failed\]/, // Both syntaxes are valid for tags
          sort: "date", // Agent should always return a sort value
        },
      },
      {
        // Another query requiring field discovery
        input: "Find issues where the kafka.consumer.group is orders-processor",
        expectedTools: [
          {
            name: "issueFields",
            arguments: {}, // No arguments needed anymore
          },
        ],
        expected: {
          query: "kafka.consumer.group:orders-processor",
          sort: "date", // Agent should always return a sort value
        },
      },
    ];
  },
  task: async (input) => {
    // Create a real API service that will use MSW mocks
    const apiService = new SentryApiService({
      accessToken: "test-token",
    });

    const agentResult = await searchIssuesAgent({
      query: input,
      organizationSlug: "sentry-mcp-evals",
      apiService,
    });

    // Return in the format expected by ToolCallScorer
    return {
      result: JSON.stringify(agentResult.result),
      toolCalls: agentResult.toolCalls.map((call: any) => ({
        name: call.toolName,
        arguments: call.args,
      })),
    };
  },
  scorers: [
    ToolCallScorer(), // Validates tool calls
    StructuredOutputScorer({ match: "fuzzy" }), // Validates the structured query output with flexible matching
  ],
});

```

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

```typescript
/**
 * Configuration for the search-issues agent
 */

export const systemPrompt = `You are a Sentry issue search query translator. Convert natural language queries to Sentry issue search syntax.

IMPORTANT RULES:
1. Use Sentry issue search syntax, NOT SQL
2. Time ranges use relative notation: -24h, -7d, -30d
3. Comparisons: >, <, >=, <=
4. Boolean operators: AND, OR, NOT (or !)
5. Field values with spaces need quotes: environment:"dev server"

BUILT-IN FIELDS:
- is: Issue status (unresolved, resolved, ignored, archived)
- level: Severity level (error, warning, info, debug, fatal)
  IMPORTANT: Almost NEVER use this field. Terms like "critical", "important", "severe" refer to IMPACT not level.
  Only use if user explicitly says "error level", "warning level", etc.
- environment: Deployment environment (production, staging, development)
- release: Version/release identifier
- firstSeen: When the issue was FIRST encountered (use for "new issues", "started", "began")
  WARNING: Excludes ongoing issues that started before the time window
- lastSeen: When the issue was LAST encountered (use for "from the last", "recent", "active")
  This includes ALL issues seen during the time window, regardless of when they started
- assigned: Issues explicitly assigned to a user (email or "me")  
- assignedOrSuggested: Issues assigned to OR suggested for a user (broader match)
- userCount: Number of unique users affected
- eventCount: Total number of events

COMMON QUERY PATTERNS:
- Unresolved issues: is:unresolved (NO level filter unless explicitly requested)
- Critical/important issues: is:unresolved with sort:freq or sort:user (NOT level:error)
- Recent activity: lastSeen:-24h
- New issues: firstSeen:-7d
- High impact: userCount:>100
- My work: assignedOrSuggested:me

SORTING RULES:
1. CRITICAL: Sort MUST go in the separate "sort" field, NEVER in the "query" field
   - WRONG: query: "is:unresolved sort:user" ← Sort syntax in query field is FORBIDDEN
   - CORRECT: query: "is:unresolved", sort: "user" ← Sort in separate field

2. AVAILABLE SORT OPTIONS:
   - date: Last seen (default)
   - freq: Event frequency  
   - new: First seen
   - user: User count

3. IMPORTANT: Query field is for filtering only (is:, level:, environment:, etc.)

'ME' REFERENCES:
- When the user says "assigned to me" or similar, you MUST use the whoami tool to get the current user's email
- Replace "me" with the actual email address in the query
- Example: "assigned to me" → use whoami tool → assignedOrSuggested:[email protected]

EXAMPLES:
"critical bugs" → query: "level:error is:unresolved", sort: "date"
"worst issues affecting the most users" → query: "is:unresolved", sort: "user"
"assigned to [email protected]" → query: "assignedOrSuggested:[email protected]", sort: "date"

NEVER: query: "is:unresolved sort:user" ← Sort goes in separate field!

CRITICAL - TOOL RESPONSE HANDLING:
All tools return responses in this format: {error?: string, result?: data}
- If 'error' is present: The tool failed - analyze the error message and potentially retry with corrections
- If 'result' is present: The tool succeeded - use the result data for your query construction
- Always check for errors before using results

Always use the issueFields tool to discover available fields when needed.
Use the whoami tool when you need to resolve 'me' references.`;

```

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

```typescript
import { z } from "zod";
import { setTag } from "@sentry/core";
import { defineTool } from "../internal/tool-helpers/define";
import { apiServiceFromContext } from "../internal/tool-helpers/api";
import { logIssue } from "../telem/logging";
import type { ServerContext } from "../types";
import type { ClientKey } from "../api-client/index";
import {
  ParamOrganizationSlug,
  ParamRegionUrl,
  ParamTeamSlug,
  ParamPlatform,
} from "../schema";

export default defineTool({
  name: "create_project",
  requiredScopes: ["project:write", "team:read"],
  description: [
    "Create a new project in Sentry (includes DSN automatically).",
    "",
    "🔍 USE THIS TOOL WHEN USERS WANT TO:",
    "- 'Create a new project'",
    "- 'Set up a project for [app/service] with team [X]'",
    "- 'I need a new Sentry project'",
    "- Create project AND need DSN in one step",
    "",
    "❌ DO NOT USE create_dsn after this - DSN is included in output.",
    "",
    "Be careful when using this tool!",
    "",
    "<examples>",
    "### Create new project with team",
    "```",
    "create_project(organizationSlug='my-organization', teamSlug='my-team', name='my-project', platform='javascript')",
    "```",
    "</examples>",
    "",
    "<hints>",
    "- If the user passes a parameter in the form of name/otherName, its likely in the format of <organizationSlug>/<teamSlug>.",
    "- If any parameter is ambiguous, you should clarify with the user what they meant.",
    "</hints>",
  ].join("\n"),
  inputSchema: {
    organizationSlug: ParamOrganizationSlug,
    regionUrl: ParamRegionUrl.optional(),
    teamSlug: ParamTeamSlug,
    name: z
      .string()
      .trim()
      .describe(
        "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.",
      ),
    platform: ParamPlatform.optional(),
  },
  annotations: {
    readOnlyHint: false,
    destructiveHint: false,
    openWorldHint: true,
  },
  async handler(params, context: ServerContext) {
    const apiService = apiServiceFromContext(context, {
      regionUrl: params.regionUrl,
    });
    const organizationSlug = params.organizationSlug;

    setTag("organization.slug", organizationSlug);
    setTag("team.slug", params.teamSlug);

    const project = await apiService.createProject({
      organizationSlug,
      teamSlug: params.teamSlug,
      name: params.name,
      platform: params.platform,
    });
    let clientKey: ClientKey | null = null;
    try {
      clientKey = await apiService.createClientKey({
        organizationSlug,
        projectSlug: project.slug,
        name: "Default",
      });
    } catch (err) {
      logIssue(err);
    }
    let output = `# New Project in **${organizationSlug}**\n\n`;
    output += `**ID**: ${project.id}\n`;
    output += `**Slug**: ${project.slug}\n`;
    output += `**Name**: ${project.name}\n`;
    if (clientKey) {
      output += `**SENTRY_DSN**: ${clientKey?.dsn.public}\n\n`;
    } else {
      output += "**SENTRY_DSN**: There was an error fetching this value.\n\n";
    }
    output += "# Using this information\n\n";
    output += `- You can reference the **SENTRY_DSN** value to initialize Sentry's SDKs.\n`;
    output += `- You should always inform the user of the **SENTRY_DSN** and Project Slug values.\n`;
    return output;
  },
});

```

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

```typescript
/**
 * Constants for Sentry MCP server.
 *
 * Defines platform and framework combinations available in Sentry documentation.
 */

/**
 * MCP Server identification
 */
export const MCP_SERVER_NAME = "Sentry MCP" as const;

/**
 * Allowed region domains for sentry.io
 * Only these specific domains are permitted when using Sentry's cloud service
 * This is used to prevent SSRF attacks by restricting regionUrl to known domains
 */
export const SENTRY_ALLOWED_REGION_DOMAINS = new Set([
  "sentry.io",
  "us.sentry.io",
  "de.sentry.io",
]);

/**
 * Common Sentry platforms that have documentation available
 */
export const SENTRY_PLATFORMS_BASE = [
  "javascript",
  "python",
  "java",
  "dotnet",
  "go",
  "php",
  "ruby",
  "android",
  "apple",
  "unity",
  "unreal",
  "rust",
  "elixir",
  "kotlin",
  "native",
  "dart",
  "godot",
  "nintendo-switch",
  "playstation",
  "powershell",
  "react-native",
  "xbox",
] as const;

/**
 * Platform-specific frameworks that have Sentry guides
 */
export const SENTRY_FRAMEWORKS: Record<string, string[]> = {
  javascript: [
    "nextjs",
    "react",
    "gatsby",
    "remix",
    "vue",
    "angular",
    "hono",
    "svelte",
    "express",
    "fastify",
    "astro",
    "bun",
    "capacitor",
    "cloudflare",
    "connect",
    "cordova",
    "deno",
    "electron",
    "ember",
    "nuxt",
    "solid",
    "solidstart",
    "sveltekit",
    "tanstack-react",
    "wasm",
    "node",
    "koa",
    "nestjs",
    "hapi",
  ],
  python: [
    "django",
    "flask",
    "fastapi",
    "celery",
    "tornado",
    "pyramid",
    "aiohttp",
    "anthropic",
    "airflow",
    "aws-lambda",
    "boto3",
    "bottle",
    "chalice",
    "dramatiq",
    "falcon",
    "langchain",
    "litestar",
    "logging",
    "loguru",
    "openai",
    "quart",
    "ray",
    "redis",
    "rq",
    "sanic",
    "sqlalchemy",
    "starlette",
  ],
  dart: ["flutter"],
  dotnet: [
    "aspnetcore",
    "maui",
    "wpf",
    "winforms",
    "aspnet",
    "aws-lambda",
    "azure-functions",
    "blazor-webassembly",
    "entityframework",
    "google-cloud-functions",
    "extensions-logging",
    "log4net",
    "nlog",
    "serilog",
    "uwp",
    "xamarin",
  ],
  java: [
    "spring",
    "spring-boot",
    "android",
    "jul",
    "log4j2",
    "logback",
    "servlet",
  ],
  go: [
    "echo",
    "fasthttp",
    "fiber",
    "gin",
    "http",
    "iris",
    "logrus",
    "negroni",
    "slog",
    "zerolog",
  ],
  php: ["laravel", "symfony"],
  ruby: ["delayed_job", "rack", "rails", "resque", "sidekiq"],
  android: ["kotlin"],
  apple: ["ios", "macos", "watchos", "tvos", "visionos"],
  kotlin: ["multiplatform"],
} as const;

/**
 * All valid guides for Sentry docs search filtering.
 * A guide can be either a platform (e.g., 'javascript') or a platform/framework combination (e.g., 'javascript/nextjs').
 */
export const SENTRY_GUIDES = [
  // Base platforms
  ...SENTRY_PLATFORMS_BASE,
  // Platform/guide combinations
  ...Object.entries(SENTRY_FRAMEWORKS).flatMap(([platform, guides]) =>
    guides.map((guide) => `${platform}/${guide}`),
  ),
] as const;

export const DEFAULT_SCOPES = [
  "org:read",
  "project:read",
  "team:read",
  "event:read",
] as const;

// Note: All scopes are now exported from permissions.ts to avoid pulling this
// heavy constants module into scope-only consumers.

```

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

```typescript
import { experimental_createMCPClient } from "ai";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { startNewTrace, startSpan } from "@sentry/core";
import { OAuthClient } from "./auth/oauth.js";
import { DEFAULT_MCP_URL } from "./constants.js";
import { logError, logSuccess } from "./logger.js";
import type { MCPConnection, RemoteMCPConfig } from "./types.js";
import { randomUUID } from "node:crypto";
import { LIB_VERSION } from "./version.js";

export async function connectToRemoteMCPServer(
  config: RemoteMCPConfig,
): Promise<MCPConnection> {
  const sessionId = randomUUID();

  return await startNewTrace(async () => {
    return await startSpan(
      {
        name: "mcp.connect/http",
        attributes: {
          "mcp.transport": "http",
          "gen_ai.conversation.id": sessionId,
          "service.version": LIB_VERSION,
        },
      },
      async (span) => {
        try {
          const mcpHost = config.mcpHost || DEFAULT_MCP_URL;

          // Remove custom attributes - let SDK handle standard attributes
          let accessToken = config.accessToken;

          // If no access token provided, we need to authenticate
          if (!accessToken) {
            await startSpan(
              {
                name: "mcp.auth/oauth",
              },
              async (authSpan) => {
                try {
                  const oauthClient = new OAuthClient({
                    mcpHost: mcpHost,
                  });
                  accessToken = await oauthClient.getAccessToken();
                  authSpan.setStatus({ code: 1 });
                } catch (error) {
                  authSpan.setStatus({ code: 2 });
                  logError(
                    "OAuth authentication failed",
                    error instanceof Error ? error : String(error),
                  );
                  throw error;
                }
              },
            );
          }

          // Create HTTP streaming client with authentication
          // Use ?agent=1 query param for agent mode, otherwise standard /mcp
          const mcpUrl = new URL(`${mcpHost}/mcp`);
          if (config.useAgentEndpoint) {
            mcpUrl.searchParams.set("agent", "1");
          }
          const httpTransport = new StreamableHTTPClientTransport(mcpUrl, {
            requestInit: {
              headers: {
                Authorization: `Bearer ${accessToken}`,
              },
            },
          });

          const client = await experimental_createMCPClient({
            name: "mcp.sentry.dev (test-client)",
            transport: httpTransport,
          });

          // Discover available tools
          const toolsMap = await client.tools();
          const tools = new Map<string, any>();

          for (const [name, tool] of Object.entries(toolsMap)) {
            tools.set(name, tool);
          }

          // Remove custom attributes - let SDK handle standard attributes
          span.setStatus({ code: 1 });

          logSuccess(
            `Connected to MCP server (${mcpHost})`,
            `${tools.size} tools available`,
          );

          const disconnect = async () => {
            await client.close();
          };

          return {
            client,
            tools,
            disconnect,
            sessionId,
            transport: "http" as const,
          };
        } catch (error) {
          span.setStatus({ code: 2 });
          throw error;
        }
      },
    );
  });
}

```

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

```typescript
import { describe, it, expect } from "vitest";
import getEventAttachment from "./get-event-attachment.js";

describe("get_event_attachment", () => {
  it("lists attachments for an event", async () => {
    const result = await getEventAttachment.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        projectSlug: "cloudflare-mcp",
        eventId: "7ca573c0f4814912aaa9bdc77d1a7d51",
        attachmentId: undefined,
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
          projectSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );
    expect(result).toMatchInlineSnapshot(`
      "# Event Attachments

      **Event ID:** 7ca573c0f4814912aaa9bdc77d1a7d51
      **Project:** cloudflare-mcp

      Found 1 attachment(s):

      ## Attachment 1

      **ID:** 123
      **Name:** screenshot.png
      **Type:** event.attachment
      **Size:** 1024 bytes
      **MIME Type:** image/png
      **Created:** 2025-04-08T21:15:04.000Z
      **SHA1:** abc123def456

      To download this attachment, use the "get_event_attachment" tool with the attachmentId provided:
      \`get_event_attachment(organizationSlug="sentry-mcp-evals", projectSlug="cloudflare-mcp", eventId="7ca573c0f4814912aaa9bdc77d1a7d51", attachmentId="123")\`

      "
    `);
  });

  it("downloads a specific attachment by ID", async () => {
    const result = await getEventAttachment.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        projectSlug: "cloudflare-mcp",
        eventId: "7ca573c0f4814912aaa9bdc77d1a7d51",
        attachmentId: "123",
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
          projectSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );

    // Should return an array with both text description and image content
    expect(Array.isArray(result)).toBe(true);
    expect(result).toHaveLength(2);

    // First item should be the image content
    expect(result[0]).toMatchObject({
      type: "image",
      mimeType: "image/png",
      data: expect.any(String), // base64 encoded data
    });

    // Second item should be the text description
    expect(result[1]).toMatchInlineSnapshot(`
      {
        "text": "# Event Attachment Download

      **Event ID:** 7ca573c0f4814912aaa9bdc77d1a7d51
      **Attachment ID:** 123
      **Filename:** screenshot.png
      **Type:** event.attachment
      **Size:** 1024 bytes
      **MIME Type:** image/png
      **Created:** 2025-04-08T21:15:04.000Z
      **SHA1:** abc123def456

      **Download URL:** https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/events/7ca573c0f4814912aaa9bdc77d1a7d51/attachments/123/?download=1

      ## Binary Content

      The attachment is included as a resource and accessible through your client.
      ",
        "type": "text",
      }
    `);
  });

  it("throws error for malformed regionUrl", async () => {
    await expect(
      getEventAttachment.handler(
        {
          organizationSlug: "sentry-mcp-evals",
          projectSlug: "cloudflare-mcp",
          eventId: "7ca573c0f4814912aaa9bdc77d1a7d51",
          attachmentId: undefined,
          regionUrl: "https",
        },
        {
          constraints: {
            organizationSlug: null,
            projectSlug: null,
          },
          accessToken: "access-token",
          userId: "1",
        },
      ),
    ).rejects.toThrow(
      "Invalid regionUrl provided: https. Must be a valid URL.",
    );
  });
});

```

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

```json
{
  "namespace": "code",
  "description": "These attributes provide context about source code\n",
  "attributes": {
    "code.function.name": {
      "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",
      "type": "string",
      "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",
      "stability": "stable",
      "examples": [
        "com.example.MyHttpService.serveRequest",
        "GuzzleHttp\\Client::transfer",
        "fopen"
      ]
    },
    "code.file.path": {
      "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",
      "type": "string",
      "stability": "stable",
      "examples": ["/usr/local/MyApplication/content_root/app/index.php"]
    },
    "code.line.number": {
      "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",
      "type": "number",
      "stability": "stable",
      "examples": ["42"]
    },
    "code.column.number": {
      "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",
      "type": "number",
      "stability": "stable",
      "examples": ["16"]
    },
    "code.stacktrace": {
      "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",
      "type": "string",
      "stability": "stable",
      "examples": [
        "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"
      ]
    }
  }
}

```

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

```typescript
import { z } from "zod";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { experimental_createMCPClient } from "ai";
import { defineTool } from "../../internal/tool-helpers/define";
import type { ServerContext } from "../../types";
import { useSentryAgent } from "./agent";
import { buildServer } from "../../server";
import tools from "../index";
import type { ToolCall } from "../../internal/agents/callEmbeddedAgent";

/**
 * Format tool calls into a readable trace
 */
function formatToolCallTrace(toolCalls: ToolCall[]): string {
  let trace = "";

  for (let i = 0; i < toolCalls.length; i++) {
    const call = toolCalls[i];
    trace += `### ${i + 1}. ${call.toolName}\n\n`;

    // Type assertion is safe: AI SDK guarantees args is always a JSON-serializable object
    const args = call.args as Record<string, unknown>;

    // Format arguments
    if (Object.keys(args).length === 0) {
      trace += "_No arguments_\n\n";
    } else {
      trace += "**Arguments:**\n```json\n";
      trace += JSON.stringify(args, null, 2);
      trace += "\n```\n\n";
    }
  }

  return trace;
}

export default defineTool({
  name: "use_sentry",
  requiredScopes: [], // No specific scopes - uses authentication token
  description: [
    "Use Sentry's MCP Agent to answer questions related to Sentry (sentry.io).",
    "",
    "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.",
    "",
  ].join("\n"),
  inputSchema: {
    request: z
      .string()
      .trim()
      .min(1)
      .describe(
        "The user's raw input. Do not interpret the prompt in any way. Do not add any additional information to the prompt.",
      ),
    trace: z
      .boolean()
      .optional()
      .describe(
        "Enable tracing to see all tool calls made by the agent. Useful for debugging.",
      ),
  },
  annotations: {
    readOnlyHint: true, // Will be adjusted based on actual implementation
    openWorldHint: true,
  },
  async handler(params, context: ServerContext) {
    // Create linked pair of in-memory transports for client-server communication
    const [clientTransport, serverTransport] =
      InMemoryTransport.createLinkedPair();

    // Filter out use_sentry from tools to prevent recursion and circular dependency
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { use_sentry, ...toolsForAgent } = tools;

    // Build internal MCP server with the provided context
    // Context is captured in tool handler closures during buildServer()
    const server = buildServer({
      context,
      tools: toolsForAgent,
    });

    // Connect server to its transport
    await server.server.connect(serverTransport);

    // Create MCP client with the other end of the transport
    const mcpClient = await experimental_createMCPClient({
      name: "mcp.sentry.dev (use-sentry)",
      transport: clientTransport,
    });

    try {
      // Get tools from MCP server (returns Vercel AI SDK compatible tools)
      const mcpTools = await mcpClient.tools();

      // Call the embedded agent with MCP tools and the user's request
      const agentResult = await useSentryAgent({
        request: params.request,
        tools: mcpTools,
      });

      let output = agentResult.result.result;

      // If tracing is enabled, append the tool call trace
      if (params.trace && agentResult.toolCalls.length > 0) {
        output += "\n\n---\n\n## Tool Call Trace\n\n";
        output += formatToolCallTrace(agentResult.toolCalls);
      }

      return output;
    } finally {
      // Clean up connections
      await mcpClient.close();
      await server.server.close();
    }
  },
});

```

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

```typescript
import { z } from "zod";
import { ConfigurationError } from "../../errors";
import { callEmbeddedAgent } from "../../internal/agents/callEmbeddedAgent";
import type { SentryApiService } from "../../api-client";
import { createOtelLookupTool } from "../../internal/agents/tools/otel-semantics";
import { createWhoamiTool } from "../../internal/agents/tools/whoami";
import { createDatasetAttributesTool } from "./utils";
import { systemPrompt } from "./config";

const outputSchema = z
  .object({
    dataset: z
      .enum(["spans", "errors", "logs"])
      .describe("Which dataset to use for the query"),
    query: z
      .string()
      .default("")
      .nullish()
      .describe("The Sentry query string for filtering results"),
    fields: z
      .array(z.string())
      .describe("Array of field names to return in results."),
    sort: z.string().describe("Sort parameter for results."),
    timeRange: z
      .union([
        z.object({
          statsPeriod: z
            .string()
            .describe("Relative time period like '1h', '24h', '7d'"),
        }),
        z.object({
          start: z.string().describe("ISO 8601 start time"),
          end: z.string().describe("ISO 8601 end time"),
        }),
      ])
      .nullish()
      .describe(
        "Time range for filtering events. Use either statsPeriod for relative time or start/end for absolute time.",
      ),
    explanation: z
      .string()
      .describe("Brief explanation of how you translated this query."),
  })
  .refine(
    (data) => {
      // Only validate if both sort and fields are present
      if (!data.sort || !data.fields || data.fields.length === 0) {
        return true;
      }

      // Extract the field name from sort parameter (e.g., "-timestamp" -> "timestamp", "-count()" -> "count()")
      const sortField = data.sort.startsWith("-")
        ? data.sort.substring(1)
        : data.sort;

      // Check if sort field is in fields array
      return data.fields.includes(sortField);
    },
    {
      message:
        "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.",
    },
  );

export interface SearchEventsAgentOptions {
  query: string;
  organizationSlug: string;
  apiService: SentryApiService;
  projectId?: string;
}

/**
 * Search events agent - single entry point for translating natural language queries to Sentry search syntax
 * This returns both the translated query result AND the tool calls made by the agent
 */
export async function searchEventsAgent(
  options: SearchEventsAgentOptions,
): Promise<{
  result: z.infer<typeof outputSchema>;
  toolCalls: any[];
}> {
  if (!process.env.OPENAI_API_KEY) {
    throw new ConfigurationError(
      "OPENAI_API_KEY environment variable is required for semantic search",
    );
  }

  // Create tools pre-bound with the provided API service and organization
  const datasetAttributesTool = createDatasetAttributesTool({
    apiService: options.apiService,
    organizationSlug: options.organizationSlug,
    projectId: options.projectId,
  });
  const otelLookupTool = createOtelLookupTool({
    apiService: options.apiService,
    organizationSlug: options.organizationSlug,
    projectId: options.projectId,
  });
  const whoamiTool = createWhoamiTool({ apiService: options.apiService });

  // Use callEmbeddedAgent to translate the query with tool call capture
  return await callEmbeddedAgent({
    system: systemPrompt,
    prompt: options.query,
    tools: {
      datasetAttributes: datasetAttributesTool,
      otelSemantics: otelLookupTool,
      whoami: whoamiTool,
    },
    schema: outputSchema,
  });
}

```

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

```typescript
/**
 * TypeScript type definitions derived from Zod schemas.
 *
 * This module provides strongly-typed interfaces for all Sentry API data
 * structures. Types are automatically derived from their corresponding
 * Zod schemas using `z.infer<>`, ensuring perfect synchronization between
 * runtime validation and compile-time type checking.
 *
 * Type Categories:
 * - **Core Resources**: User, Organization, Team, Project
 * - **Issue Management**: Issue, Event, AssignedTo
 * - **Release Management**: Release
 * - **Search & Discovery**: Tag
 * - **Integrations**: ClientKey, AutofixRun, AutofixRunState
 *
 * Array Types:
 * All list types follow the pattern `ResourceList = Resource[]` for consistency.
 *
 * @example Type Usage
 * ```typescript
 * import type { Issue, IssueList } from "./types";
 *
 * function processIssues(issues: IssueList): void {
 *   issues.forEach((issue: Issue) => {
 *     console.log(`${issue.shortId}: ${issue.title}`);
 *   });
 * }
 * ```
 *
 * @example API Response Typing
 * ```typescript
 * async function getIssue(id: string): Promise<Issue> {
 *   const response = await apiService.getIssue({
 *     organizationSlug: "my-org",
 *     issueId: id
 *   });
 *   return response; // Already typed as Issue from schema validation
 * }
 * ```
 */
import type { z } from "zod";
import type {
  AssignedToSchema,
  AutofixRunSchema,
  AutofixRunStateSchema,
  ClientKeyListSchema,
  ClientKeySchema,
  ErrorEventSchema,
  DefaultEventSchema,
  TransactionEventSchema,
  UnknownEventSchema,
  EventSchema,
  EventAttachmentSchema,
  EventAttachmentListSchema,
  IssueListSchema,
  IssueSchema,
  OrganizationListSchema,
  OrganizationSchema,
  ProjectListSchema,
  ProjectSchema,
  ReleaseListSchema,
  ReleaseSchema,
  TagListSchema,
  TagSchema,
  TeamListSchema,
  TeamSchema,
  TraceMetaSchema,
  TraceSchema,
  TraceSpanSchema,
  TraceIssueSchema,
  UserSchema,
} from "./schema";

export type User = z.infer<typeof UserSchema>;
export type Organization = z.infer<typeof OrganizationSchema>;
export type Team = z.infer<typeof TeamSchema>;
export type Project = z.infer<typeof ProjectSchema>;
export type ClientKey = z.infer<typeof ClientKeySchema>;
export type Release = z.infer<typeof ReleaseSchema>;
export type Issue = z.infer<typeof IssueSchema>;

// Individual event types
export type ErrorEvent = z.infer<typeof ErrorEventSchema>;
export type DefaultEvent = z.infer<typeof DefaultEventSchema>;
export type TransactionEvent = z.infer<typeof TransactionEventSchema>;
export type UnknownEvent = z.infer<typeof UnknownEventSchema>;

// Event union - use RawEvent for parsing, Event for known types only
export type RawEvent = z.infer<typeof EventSchema>;
export type Event = ErrorEvent | DefaultEvent | TransactionEvent;

export type EventAttachment = z.infer<typeof EventAttachmentSchema>;
export type Tag = z.infer<typeof TagSchema>;
export type AutofixRun = z.infer<typeof AutofixRunSchema>;
export type AutofixRunState = z.infer<typeof AutofixRunStateSchema>;
export type AssignedTo = z.infer<typeof AssignedToSchema>;

export type OrganizationList = z.infer<typeof OrganizationListSchema>;
export type TeamList = z.infer<typeof TeamListSchema>;
export type ProjectList = z.infer<typeof ProjectListSchema>;
export type ReleaseList = z.infer<typeof ReleaseListSchema>;
export type IssueList = z.infer<typeof IssueListSchema>;
export type EventAttachmentList = z.infer<typeof EventAttachmentListSchema>;
export type TagList = z.infer<typeof TagListSchema>;
export type ClientKeyList = z.infer<typeof ClientKeyListSchema>;

// Trace types
export type TraceMeta = z.infer<typeof TraceMetaSchema>;
export type TraceSpan = z.infer<typeof TraceSpanSchema>;
export type TraceIssue = z.infer<typeof TraceIssueSchema>;
export type Trace = z.infer<typeof TraceSchema>;

```

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

```json
{
  "namespace": "service",
  "description": "A service instance.\n",
  "attributes": {
    "service.name": {
      "description": "Logical name of the service.\n",
      "type": "string",
      "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",
      "stability": "stable",
      "examples": ["shoppingcart"]
    },
    "service.version": {
      "description": "The version string of the service API or implementation. The format is not defined by these conventions.\n",
      "type": "string",
      "stability": "stable",
      "examples": ["2.0.0", "a01dbef8a"]
    },
    "service.namespace": {
      "description": "A namespace for `service.name`.\n",
      "type": "string",
      "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",
      "stability": "development",
      "examples": ["Shop"]
    },
    "service.instance.id": {
      "description": "The string ID of the service instance.\n",
      "type": "string",
      "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",
      "stability": "development",
      "examples": ["627cc493-f310-47de-96bd-71410b7dec09"]
    }
  }
}

```

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

```typescript
import type { z } from "zod";
import type {
  AutofixRunStepSchema,
  AutofixRunStepRootCauseAnalysisSchema,
  AutofixRunStepSolutionSchema,
  AutofixRunStepDefaultSchema,
} from "../../api-client/index";

export const SEER_POLLING_INTERVAL = 5000; // 5 seconds
export const SEER_TIMEOUT = 5 * 60 * 1000; // 5 minutes
export const SEER_MAX_RETRIES = 3; // Maximum retries for transient failures
export const SEER_INITIAL_RETRY_DELAY = 1000; // 1 second initial retry delay

export function getStatusDisplayName(status: string): string {
  switch (status) {
    case "COMPLETED":
      return "Complete";
    case "FAILED":
    case "ERROR":
      return "Failed";
    case "CANCELLED":
      return "Cancelled";
    case "NEED_MORE_INFORMATION":
      return "Needs More Information";
    case "WAITING_FOR_USER_RESPONSE":
      return "Waiting for Response";
    case "PROCESSING":
      return "Processing";
    case "IN_PROGRESS":
      return "In Progress";
    default:
      return status;
  }
}

/**
 * Check if an autofix status is terminal (no more updates expected)
 */
export function isTerminalStatus(status: string): boolean {
  return [
    "COMPLETED",
    "FAILED",
    "ERROR",
    "CANCELLED",
    "NEED_MORE_INFORMATION",
    "WAITING_FOR_USER_RESPONSE",
  ].includes(status);
}

/**
 * Check if an autofix status requires human intervention
 */
export function isHumanInterventionStatus(status: string): boolean {
  return (
    status === "NEED_MORE_INFORMATION" || status === "WAITING_FOR_USER_RESPONSE"
  );
}

/**
 * Get guidance message for human intervention states
 */
export function getHumanInterventionGuidance(status: string): string {
  if (status === "NEED_MORE_INFORMATION") {
    return "\nSeer needs additional information to continue the analysis. Please review the insights above and consider providing more context.\n";
  }
  if (status === "WAITING_FOR_USER_RESPONSE") {
    return "\nSeer is waiting for your response to proceed. Please review the analysis and provide feedback.\n";
  }
  return "";
}

export function getOutputForAutofixStep(
  step: z.infer<typeof AutofixRunStepSchema>,
) {
  let output = `## ${step.title}\n\n`;

  if (step.status === "FAILED") {
    output += `**Sentry hit an error completing this step.\n\n`;
    return output;
  }

  if (step.status !== "COMPLETED") {
    output += `**Sentry is still working on this step. Please check back in a minute.**\n\n`;
    return output;
  }

  if (step.type === "root_cause_analysis") {
    const typedStep = step as z.infer<
      typeof AutofixRunStepRootCauseAnalysisSchema
    >;

    for (const cause of typedStep.causes) {
      if (cause.description) {
        output += `${cause.description}\n\n`;
      }
      for (const entry of cause.root_cause_reproduction) {
        output += `**${entry.title}**\n\n`;
        output += `${entry.code_snippet_and_analysis}\n\n`;
      }
    }
    return output;
  }

  if (step.type === "solution") {
    const typedStep = step as z.infer<typeof AutofixRunStepSolutionSchema>;
    output += `${typedStep.description}\n\n`;
    for (const entry of typedStep.solution) {
      output += `**${entry.title}**\n`;
      output += `${entry.code_snippet_and_analysis}\n\n`;
    }

    if (typedStep.status === "FAILED") {
      output += `**Sentry hit an error completing this step.\n\n`;
    } else if (typedStep.status !== "COMPLETED") {
      output += `**Sentry is still working on this step.**\n\n`;
    }

    return output;
  }

  const typedStep = step as z.infer<typeof AutofixRunStepDefaultSchema>;
  if (typedStep.insights && typedStep.insights.length > 0) {
    for (const entry of typedStep.insights) {
      output += `**${entry.insight}**\n`;
      output += `${entry.justification}\n\n`;
    }
  } else if (step.output_stream) {
    output += `${step.output_stream}\n`;
  }

  return output;
}

```

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

```typescript
import { describe, it, expect, vi } from "vitest";
import searchDocs from "./search-docs.js";

describe("search_docs", () => {
  // Note: Query validation (empty, too short, too long) is now handled by Zod schema
  // These validation tests are no longer needed as they test framework behavior, not our tool logic

  it("returns results from the API", async () => {
    const result = await searchDocs.handler(
      {
        query: "How do I configure rate limiting?",
        maxResults: 5,
        guide: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
        mcpUrl: "https://mcp.sentry.dev",
      },
    );
    expect(result).toMatchInlineSnapshot(`
      "# Documentation Search Results

      **Query**: "How do I configure rate limiting?"

      Found 2 matches

      These are just snippets. Use \`get_doc(path='...')\` to fetch the full content.

      ## 1. https://docs.sentry.io/product/rate-limiting

      **Path**: product/rate-limiting.md
      **Relevance**: 95.0%

      **Matching Context**
      > Learn how to configure rate limiting in Sentry to prevent quota exhaustion and control event ingestion.

      ## 2. https://docs.sentry.io/product/accounts/quotas/spike-protection

      **Path**: product/accounts/quotas/spike-protection.md
      **Relevance**: 87.0%

      **Matching Context**
      > Spike protection helps prevent unexpected spikes in event volume from consuming your quota.

      "
    `);
  });

  it("handles API errors", async () => {
    vi.spyOn(global, "fetch").mockResolvedValueOnce({
      ok: false,
      status: 500,
      statusText: "Internal Server Error",
      json: async () => ({ error: "Internal server error" }),
    } as Response);

    await expect(
      searchDocs.handler(
        {
          query: "test query",
          maxResults: undefined,
          guide: undefined,
        },
        {
          constraints: {
            organizationSlug: null,
          },
          accessToken: "access-token",
          userId: "1",
        },
      ),
    ).rejects.toThrow();
  });

  it("handles timeout errors", async () => {
    // Mock fetch to simulate a timeout by throwing an AbortError
    vi.spyOn(global, "fetch").mockImplementationOnce(() => {
      const error = new Error("The operation was aborted");
      error.name = "AbortError";
      return Promise.reject(error);
    });

    await expect(
      searchDocs.handler(
        {
          query: "test query",
          maxResults: undefined,
          guide: undefined,
        },
        {
          constraints: {
            organizationSlug: null,
          },
          accessToken: "access-token",
          userId: "1",
        },
      ),
    ).rejects.toThrow("Request timeout after 15000ms");
  });

  it("includes platform in output and request", async () => {
    const mockFetch = vi.spyOn(global, "fetch");

    const result = await searchDocs.handler(
      {
        query: "test query",
        maxResults: 5,
        guide: "javascript/nextjs",
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
        mcpUrl: "https://mcp.sentry.dev",
      },
    );

    // Check that platform is included in the output
    expect(result).toContain("**Guide**: javascript/nextjs");

    // Check that platform is included in the request
    expect(mockFetch).toHaveBeenCalledWith(
      "https://mcp.sentry.dev/api/search",
      expect.objectContaining({
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          query: "test query",
          maxResults: 5,
          guide: "javascript/nextjs",
        }),
      }),
    );
  });
});

```

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

```typescript
import { UserInputError, ConfigurationError } from "../errors";
import { ApiError, ApiClientError, ApiServerError } from "../api-client";
import { logIssue } from "../telem/logging";

/**
 * Type guard to identify user input validation errors.
 */
export function isUserInputError(error: unknown): error is UserInputError {
  return error instanceof UserInputError;
}

/**
 * Type guard to identify configuration errors.
 */
export function isConfigurationError(
  error: unknown,
): error is ConfigurationError {
  return error instanceof ConfigurationError;
}

/**
 * Type guard to identify API errors.
 */
export function isApiError(error: unknown): error is ApiError {
  return error instanceof ApiError;
}

/**
 * Type guard to identify API client errors (4xx).
 */
export function isApiClientError(error: unknown): error is ApiClientError {
  return error instanceof ApiClientError;
}

/**
 * Type guard to identify API server errors (5xx).
 */
export function isApiServerError(error: unknown): error is ApiServerError {
  return error instanceof ApiServerError;
}

/**
 * Format an error for user display with markdown formatting.
 * This is used by tool handlers to format errors for MCP responses.
 *
 * SECURITY: Only return trusted error messages to prevent prompt injection vulnerabilities.
 * We trust: Sentry API errors, our own UserInputError/ConfigurationError messages, and system templates.
 */
export async function formatErrorForUser(error: unknown): Promise<string> {
  if (isUserInputError(error)) {
    return [
      "**Input Error**",
      "It looks like there was a problem with the input you provided.",
      error.message,
      `You may be able to resolve the issue by addressing the concern and trying again.`,
    ].join("\n\n");
  }

  if (isConfigurationError(error)) {
    return [
      "**Configuration Error**",
      "There appears to be a configuration issue with your setup.",
      error.message,
      `Please check your environment configuration and try again.`,
    ].join("\n\n");
  }

  // Handle ApiClientError (4xx) - user input errors, should NOT be logged to Sentry
  if (isApiClientError(error)) {
    const statusText = error.status
      ? `There was an HTTP ${error.status} error with your request to the Sentry API.`
      : "There was an error with your request.";

    return [
      "**Input Error**",
      statusText,
      error.toUserMessage(),
      `You may be able to resolve the issue by addressing the concern and trying again.`,
    ].join("\n\n");
  }

  // Handle ApiServerError (5xx) - system errors, SHOULD be logged to Sentry
  if (isApiServerError(error)) {
    const eventId = logIssue(error);
    const statusText = error.status
      ? `There was an HTTP ${error.status} server error with the Sentry API.`
      : "There was a server error.";

    return [
      "**Error**",
      statusText,
      `${error.message}`,
      `**Event ID**: ${eventId}`,
      `Please contact support with this Event ID if the problem persists.`,
    ].join("\n\n");
  }

  // Handle generic ApiError (shouldn't happen with new hierarchy, but just in case)
  if (isApiError(error)) {
    const statusText = error.status
      ? `There was an HTTP ${error.status} error with your request to the Sentry API.`
      : "There was an error with your request.";

    return [
      "**Error**",
      statusText,
      `${error.message}`,
      `You may be able to resolve the issue by addressing the concern and trying again.`,
    ].join("\n\n");
  }

  const eventId = logIssue(error);

  return [
    "**Error**",
    "It looks like there was a problem communicating with the Sentry API.",
    "Please report the following to the user for the Sentry team:",
    `**Event ID**: ${eventId}`,
    process.env.NODE_ENV !== "production"
      ? error instanceof Error
        ? error.message
        : String(error)
      : "",
  ]
    .filter(Boolean)
    .join("\n\n");
}

```

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

```typescript
import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
import useSentry from "./handler";
import type { ServerContext } from "../../types";
import type { Scope } from "../../permissions";

// Mock the embedded agent
vi.mock("./agent", () => ({
  useSentryAgent: vi.fn(),
}));

// Import the mocked module to get access to the mock function
import { useSentryAgent } from "./agent";
const mockUseSentryAgent = useSentryAgent as Mock;

// Use all scopes for testing to ensure all tools are available
const ALL_SCOPES: Scope[] = [
  "org:read",
  "org:write",
  "project:read",
  "project:write",
  "team:read",
  "team:write",
  "event:read",
  "event:write",
  "project:releases",
  "seer",
  "docs",
];

const mockContext: ServerContext = {
  accessToken: "test-token",
  sentryHost: "sentry.io",
  userId: "1",
  clientId: "test-client",
  constraints: {},
  grantedScopes: new Set(ALL_SCOPES),
};

describe("use_sentry handler", () => {
  beforeEach(() => {
    mockUseSentryAgent.mockClear();
  });

  it("calls embedded agent with request and wrapped tools", async () => {
    mockUseSentryAgent.mockResolvedValue({
      result: {
        result: "Agent executed tools successfully",
      },
      toolCalls: [{ toolName: "whoami", args: {} }],
    });

    const result = await useSentry.handler(
      { request: "Show me unresolved issues" },
      mockContext,
    );

    // Verify agent was called
    expect(mockUseSentryAgent).toHaveBeenCalledWith({
      request: "Show me unresolved issues",
      tools: expect.objectContaining({
        whoami: expect.any(Object),
        find_organizations: expect.any(Object),
        search_issues: expect.any(Object),
      }),
    });

    // Verify all 19 tools were provided (20 total - use_sentry itself)
    const toolsArg = mockUseSentryAgent.mock.calls[0][0].tools;
    expect(Object.keys(toolsArg)).toHaveLength(19);

    // Verify result is returned
    expect(result).toBe("Agent executed tools successfully");
  });

  it("provides wrapped tools with ServerContext", async () => {
    mockUseSentryAgent.mockResolvedValue({
      result: {
        result: "Success",
      },
      toolCalls: [],
    });

    await useSentry.handler({ request: "test request" }, mockContext);

    // Verify tools were provided to agent
    const toolsArg = mockUseSentryAgent.mock.calls[0][0].tools;
    expect(toolsArg).toBeDefined();

    // Verify key tools are present
    expect(toolsArg.whoami).toBeDefined();
    expect(toolsArg.find_organizations).toBeDefined();
    expect(toolsArg.search_events).toBeDefined();
    expect(toolsArg.search_issues).toBeDefined();
    expect(toolsArg.get_issue_details).toBeDefined();
  });

  it("excludes use_sentry from available tools to prevent recursion", async () => {
    mockUseSentryAgent.mockResolvedValue({
      result: {
        result: "Success",
      },
      toolCalls: [],
    });

    await useSentry.handler({ request: "test" }, mockContext);

    const toolsArg = mockUseSentryAgent.mock.calls[0][0].tools;
    const toolNames = Object.keys(toolsArg);

    // Verify use_sentry is NOT in the list
    expect(toolNames).not.toContain("use_sentry");

    // Verify we have exactly 19 tools (20 total - 1 use_sentry)
    expect(toolNames).toHaveLength(19);
  });

  it("wraps tools with session constraints", async () => {
    const constrainedContext: ServerContext = {
      ...mockContext,
      constraints: {
        organizationSlug: "constrained-org",
        projectSlug: "constrained-project",
      },
    };

    mockUseSentryAgent.mockResolvedValue({
      result: {
        result: "Success with constraints",
      },
      toolCalls: [],
    });

    await useSentry.handler(
      { request: "test with constraints" },
      constrainedContext,
    );

    // Verify agent was called with tools
    expect(mockUseSentryAgent).toHaveBeenCalled();
    const toolsArg = mockUseSentryAgent.mock.calls[0][0].tools;
    expect(toolsArg).toBeDefined();
    expect(Object.keys(toolsArg)).toHaveLength(19);
  });
});

```

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

```json
{
  "namespace": "artifact",
  "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",
  "attributes": {
    "artifact.filename": {
      "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",
      "type": "string",
      "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",
      "stability": "development",
      "examples": [
        "golang-binary-amd64-v0.1.0",
        "docker-image-amd64-v0.1.0",
        "release-1.tar.gz",
        "file-name-package.tar.gz"
      ]
    },
    "artifact.version": {
      "description": "The version of the artifact.\n",
      "type": "string",
      "stability": "development",
      "examples": ["v0.1.0", "1.2.1", "122691-build"]
    },
    "artifact.purl": {
      "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",
      "type": "string",
      "stability": "development",
      "examples": [
        "pkg:github/package-url/purl-spec@1209109710924",
        "pkg:npm/[email protected]"
      ]
    },
    "artifact.hash": {
      "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",
      "type": "string",
      "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",
      "stability": "development",
      "examples": [
        "9ff4c52759e2c4ac70b7d517bc7fcdc1cda631ca0045271ddd1b192544f8a3e9"
      ]
    },
    "artifact.attestation.id": {
      "description": "The id of the build [software attestation](https://slsa.dev/attestation-model).\n",
      "type": "string",
      "stability": "development",
      "examples": ["123"]
    },
    "artifact.attestation.filename": {
      "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",
      "type": "string",
      "stability": "development",
      "examples": [
        "golang-binary-amd64-v0.1.0.attestation",
        "docker-image-amd64-v0.1.0.intoto.json1",
        "release-1.tar.gz.attestation",
        "file-name-package.tar.gz.intoto.json1"
      ]
    },
    "artifact.attestation.hash": {
      "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",
      "type": "string",
      "stability": "development",
      "examples": [
        "1b31dfcd5b7f9267bf2ff47651df1cfb9147b9e4df1f335accf65b4cda498408"
      ]
    }
  }
}

```

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

```typescript
/**
 * Constraint application helpers for MCP server configuration.
 *
 * These functions handle the logic for filtering tool schemas and injecting
 * constraint parameters, including support for parameter aliases (e.g., projectSlug → projectSlugOrId).
 */
import type { Constraints } from "../types";
import type { z } from "zod";

/**
 * Determines which tool parameter keys should be filtered out of the schema
 * because they will be injected from constraints.
 *
 * Handles parameter aliases: when a projectSlug constraint exists and the tool
 * has a projectSlugOrId parameter, the alias will be applied UNLESS projectSlugOrId
 * is explicitly constrained with a truthy value.
 *
 * @param constraints - The active constraints (org, project, region)
 * @param toolInputSchema - The tool's input schema definition
 * @returns Array of parameter keys that should be filtered from the schema
 *
 * @example
 * ```typescript
 * const constraints = { projectSlug: "my-project", organizationSlug: "my-org" };
 * const schema = { organizationSlug: z.string(), projectSlugOrId: z.string() };
 * const keys = getConstraintKeysToFilter(constraints, schema);
 * // Returns: ["organizationSlug", "projectSlugOrId"]
 * // projectSlugOrId is included because projectSlug constraint will map to it
 * ```
 */
export function getConstraintKeysToFilter(
  constraints: Constraints & Record<string, string | null | undefined>,
  toolInputSchema: Record<string, z.ZodType>,
): string[] {
  return Object.entries(constraints).flatMap(([key, value]) => {
    if (!value) return [];

    const keys: string[] = [];

    // If this constraint key exists in the schema, include it
    if (key in toolInputSchema) {
      keys.push(key);
    }

    // Special handling: projectSlug constraint can also apply to projectSlugOrId parameter
    // Only add the alias to filter if projectSlugOrId isn't being explicitly constrained
    if (
      key === "projectSlug" &&
      "projectSlugOrId" in toolInputSchema &&
      !("projectSlugOrId" in constraints && constraints.projectSlugOrId)
    ) {
      keys.push("projectSlugOrId");
    }

    return keys;
  });
}

/**
 * Builds the constraint parameters that should be injected into tool calls.
 *
 * Handles parameter aliases: when a projectSlug constraint exists and the tool
 * has a projectSlugOrId parameter, the constraint value will be injected as
 * projectSlugOrId UNLESS projectSlugOrId is explicitly constrained with a truthy value.
 *
 * @param constraints - The active constraints (org, project, region)
 * @param toolInputSchema - The tool's input schema definition
 * @returns Object mapping parameter names to constraint values
 *
 * @example
 * ```typescript
 * const constraints = { projectSlug: "my-project", organizationSlug: "my-org" };
 * const schema = { organizationSlug: z.string(), projectSlugOrId: z.string() };
 * const params = getConstraintParametersToInject(constraints, schema);
 * // Returns: { organizationSlug: "my-org", projectSlugOrId: "my-project" }
 * // projectSlug constraint is injected as projectSlugOrId parameter
 * ```
 */
export function getConstraintParametersToInject(
  constraints: Constraints & Record<string, string | null | undefined>,
  toolInputSchema: Record<string, z.ZodType>,
): Record<string, string> {
  return Object.fromEntries(
    Object.entries(constraints).flatMap(([key, value]) => {
      if (!value) return [];

      const entries: [string, string][] = [];

      // If this constraint key exists in the schema, add it
      if (key in toolInputSchema) {
        entries.push([key, value]);
      }

      // Special handling: projectSlug constraint can also apply to projectSlugOrId parameter
      // Only apply alias if the target parameter isn't already being constrained with a truthy value
      if (
        key === "projectSlug" &&
        "projectSlugOrId" in toolInputSchema &&
        !("projectSlugOrId" in constraints && constraints.projectSlugOrId)
      ) {
        entries.push(["projectSlugOrId", value]);
      }

      return entries;
    }),
  );
}

```

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

```typescript
import { z } from "zod";
import type { SentryApiService } from "../../../api-client";
import { agentTool } from "./utils";

export type DatasetType = "events" | "errors" | "search_issues";

export interface DatasetField {
  key: string;
  name: string;
  totalValues: number;
  examples?: string[];
}

export interface DatasetFieldsResult {
  dataset: string;
  fields: DatasetField[];
  commonPatterns: Array<{ pattern: string; description: string }>;
}

/**
 * Discover available fields for a dataset by querying Sentry's tags API
 */
export async function discoverDatasetFields(
  apiService: SentryApiService,
  organizationSlug: string,
  dataset: DatasetType,
  options: {
    projectId?: string;
  } = {},
): Promise<DatasetFieldsResult> {
  const { projectId } = options;

  // Get available tags for the dataset
  const tags = await apiService.listTags({
    organizationSlug,
    dataset,
    project: projectId,
    statsPeriod: "14d",
  });

  // Filter out internal Sentry tags and format
  const fields = tags
    .filter((tag) => !tag.key.startsWith("sentry:"))
    .map((tag) => ({
      key: tag.key,
      name: tag.name,
      totalValues: tag.totalValues,
      examples: getFieldExamples(tag.key, dataset),
    }));

  return {
    dataset,
    fields,
    commonPatterns: getCommonPatterns(dataset),
  };
}

/**
 * Create a tool for discovering available fields in a dataset
 * The tool is pre-bound with the API service and organization configured for the appropriate region
 */
export function createDatasetFieldsTool(options: {
  apiService: SentryApiService;
  organizationSlug: string;
  dataset: DatasetType;
  projectId?: string;
}) {
  const { apiService, organizationSlug, dataset, projectId } = options;
  return agentTool({
    description: `Discover available fields for ${dataset} searches in Sentry (includes example values)`,
    parameters: z.object({}),
    execute: async () => {
      return discoverDatasetFields(apiService, organizationSlug, dataset, {
        projectId,
      });
    },
  });
}

/**
 * Get example values for common fields
 */
export function getFieldExamples(
  key: string,
  dataset: string,
): string[] | undefined {
  const commonExamples: Record<string, string[]> = {
    level: ["error", "warning", "info", "debug", "fatal"],
    environment: ["production", "staging", "development"],
    release: ["v1.0.0", "latest", "[email protected]"],
    user: ["user123", "[email protected]"],
  };

  const issueExamples: Record<string, string[]> = {
    ...commonExamples,
    assignedOrSuggested: ["[email protected]", "team-slug", "me"],
    is: ["unresolved", "resolved", "ignored"],
  };

  const eventExamples: Record<string, string[]> = {
    ...commonExamples,
    "http.method": ["GET", "POST", "PUT", "DELETE"],
    "http.status_code": ["200", "404", "500"],
    "db.system": ["postgresql", "mysql", "redis"],
  };

  if (dataset === "search_issues") {
    return issueExamples[key];
  }
  if (dataset === "events" || dataset === "errors") {
    return eventExamples[key];
  }

  return commonExamples[key];
}

/**
 * Get common search patterns for a dataset
 */
export function getCommonPatterns(dataset: string) {
  if (dataset === "search_issues") {
    return [
      { pattern: "is:unresolved", description: "Open issues" },
      { pattern: "is:resolved", description: "Closed issues" },
      { pattern: "level:error", description: "Error level issues" },
      {
        pattern: "firstSeen:-24h",
        description: "New issues from last 24 hours",
      },
      {
        pattern: "userCount:>100",
        description: "Affecting more than 100 users",
      },
    ];
  }
  if (dataset === "events" || dataset === "errors") {
    return [
      { pattern: "level:error", description: "Error events" },
      { pattern: "environment:production", description: "Production events" },
      { pattern: "timestamp:-1h", description: "Events from last hour" },
      { pattern: "has:http.method", description: "HTTP requests" },
      { pattern: "has:db.statement", description: "Database queries" },
    ];
  }

  return [];
}

```

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

```json
{
  "name": "@sentry/mcp-server",
  "version": "0.20.0",
  "type": "module",
  "packageManager": "[email protected]",
  "engines": {
    "node": ">=20"
  },
  "publishConfig": {
    "access": "public"
  },
  "license": "FSL-1.1-ALv2",
  "author": "Sentry",
  "description": "Sentry MCP Server",
  "homepage": "https://github.com/getsentry/sentry-mcp",
  "keywords": [
    "sentry"
  ],
  "bugs": {
    "url": "https://github.com/getsentry/sentry-mcp/issues"
  },
  "repository": {
    "type": "git",
    "url": "[email protected]:getsentry/sentry-mcp.git"
  },
  "bin": {
    "sentry-mcp": "./dist/index.js"
  },
  "files": [
    "./dist/*"
  ],
  "exports": {
    ".": {
      "types": "./dist/index.ts",
      "default": "./dist/index.js"
    },
    "./api-client": {
      "types": "./dist/api-client/index.ts",
      "default": "./dist/api-client/index.js"
    },
    "./constants": {
      "types": "./dist/constants.ts",
      "default": "./dist/constants.js"
    },
    "./telem": {
      "types": "./dist/telem/index.ts",
      "default": "./dist/telem/index.js"
    },
    "./telem/logging": {
      "types": "./dist/telem/logging.ts",
      "default": "./dist/telem/logging.js"
    },
    "./telem/sentry": {
      "types": "./dist/telem/sentry.ts",
      "default": "./dist/telem/sentry.js"
    },
    "./permissions": {
      "types": "./dist/permissions.ts",
      "default": "./dist/permissions.js"
    },
    "./transports/stdio": {
      "types": "./dist/transports/stdio.ts",
      "default": "./dist/transports/stdio.js"
    },
    "./server": {
      "types": "./dist/server.ts",
      "default": "./dist/server.js"
    },
    "./toolDefinitions": {
      "types": "./dist/toolDefinitions.ts",
      "default": "./dist/toolDefinitions.js"
    },
    "./types": {
      "types": "./dist/types.ts",
      "default": "./dist/types.js"
    },
    "./version": {
      "types": "./dist/version.ts",
      "default": "./dist/version.js"
    },
    "./tools/search-events": {
      "types": "./dist/tools/search-events/index.ts",
      "default": "./dist/tools/search-events/index.js"
    },
    "./tools/search-issues": {
      "types": "./dist/tools/search-issues/index.ts",
      "default": "./dist/tools/search-issues/index.js"
    },
    "./tools/search-events/agent": {
      "types": "./dist/tools/search-events/agent.ts",
      "default": "./dist/tools/search-events/agent.js"
    },
    "./tools/search-issues/agent": {
      "types": "./dist/tools/search-issues/agent.ts",
      "default": "./dist/tools/search-issues/agent.js"
    },
    "./tools/agent-tools": {
      "types": "./dist/tools/agent-tools.ts",
      "default": "./dist/tools/agent-tools.js"
    },
    "./internal/agents/callEmbeddedAgent": {
      "types": "./dist/internal/agents/callEmbeddedAgent.ts",
      "default": "./dist/internal/agents/callEmbeddedAgent.js"
    }
  },
  "scripts": {
    "prebuild": "pnpm run generate-definitions",
    "build": "tsdown",
    "dev": "pnpm run generate-definitions && tsdown -w",
    "start": "tsx src/index.ts",
    "prepare": "pnpm run build",
    "pretest": "pnpm run generate-definitions",
    "test": "vitest run",
    "test:ci": "pnpm run generate-definitions && vitest run --coverage --reporter=default --reporter=junit --outputFile=tests.junit.xml",
    "test:watch": "pnpm run generate-definitions && vitest",
    "tsc": "tsc --noEmit",
    "generate-definitions": "tsx scripts/generate-definitions.ts",
    "generate-otel-namespaces": "tsx scripts/generate-otel-namespaces.ts",
    "measure-tokens": "tsx scripts/measure-token-cost.ts"
  },
  "devDependencies": {
    "@sentry/mcp-server-mocks": "workspace:*",
    "@sentry/mcp-server-tsconfig": "workspace:*",
    "msw": "catalog:",
    "tiktoken": "^1.0.18",
    "yaml": "^2.6.1",
    "zod-to-json-schema": "catalog:"
  },
  "dependencies": {
    "@ai-sdk/openai": "catalog:",
    "@logtape/logtape": "^1.1.1",
    "@logtape/sentry": "^1.1.1",
    "@modelcontextprotocol/sdk": "catalog:",
    "@sentry/core": "catalog:",
    "@sentry/node": "catalog:",
    "ai": "catalog:",
    "dotenv": "catalog:",
    "zod": "catalog:"
  }
}

```

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

```markdown
# Permissions and Scopes

OAuth-style scope system for controlling access to Sentry MCP tools.

## Default Permissions

**By default, all users receive read-only access.** This includes:
- `org:read`, `project:read`, `team:read`, `event:read`

Additional permissions must be explicitly granted through the OAuth flow or CLI arguments.

## Permission Levels

When authenticating via OAuth, users can select additional permissions:

| Level | Scopes | Tools Enabled |
|-------|--------|--------------|
| **Read-Only** (default) | `org:read`, `project:read`, `team:read`, `event:read` | Search, view issues/traces, documentation |
| **+ Issue Triage** | Adds `event:write` | All above + resolve/assign issues, AI analysis |
| **+ Project Management** | Adds `project:write`, `team:write` | All above + create/modify projects/teams/DSNs |

### CLI Usage

```bash
# Default: read-only access
npx @sentry/mcp-server --access-token=TOKEN

# Override defaults with specific scopes only
npx @sentry/mcp-server --access-token=TOKEN --scopes=org:read,event:read

# Add write permissions to default read-only scopes
npx @sentry/mcp-server --access-token=TOKEN --add-scopes=event:write,project:write

# Via environment variables
export MCP_SCOPES=org:read,project:write  # Overrides defaults
export MCP_ADD_SCOPES=event:write         # Adds to defaults
npx @sentry/mcp-server --access-token=TOKEN
```

Precedence and validation:
- Flags override environment variables. If `--scopes` is provided, `MCP_SCOPES` is ignored. If `--add-scopes` is provided, `MCP_ADD_SCOPES` is ignored.
- Flags and env vars are strict: any invalid scope token causes an error listing allowed scopes.

**Note:** `--scopes` completely replaces the default scopes, while `--add-scopes` adds to them.

## Scope Hierarchy

Higher scopes include lower ones:

```
admin → write → read
```

Examples:
- `team:write` includes `team:read`
- `event:admin` includes `event:write` and `event:read`

## Available Scopes

| Resource | Read | Write | Admin |
|----------|------|-------|-------|
| **Organization** | `org:read` | `org:write` | `org:admin` |
| **Project** | `project:read` | `project:write` | `project:admin` |
| **Team** | `team:read` | `team:write` | `team:admin` |
| **Member** | `member:read` | `member:write` | `member:admin` |
| **Event/Issue** | `event:read` | `event:write` | `event:admin` |
| **Special** | `project:releases` | - | - |

## Tool Requirements

### Always Available (No Scopes)
- `whoami` - User identification
- `search_docs` - Documentation search
- `get_doc` - Documentation retrieval

### Read Operations
- `find_organizations` - `org:read`
- `find_projects` - `project:read`
- `find_teams` - `team:read`
- `find_releases` - `project:read`
- `find_dsns` - `project:read`
- `get_issue_details` - `event:read`
- `get_event_attachment` - `event:read`
- `get_trace_details` - `event:read`
- `search_events` - `event:read`
- `search_issues` - `event:read`
- `analyze_issue_with_seer` - `event:read`

### Write Operations
- `update_issue` - `event:write`
- `create_project` - `project:write`, `team:read`
- `update_project` - `project:write`
- `create_team` - `team:write`
- `create_dsn` - `project:write`

## How It Works

1. **Sentry Authentication**: MCP requests all necessary scopes from Sentry
2. **Permission Selection**: User chooses permission level in approval dialog
3. **Tool Filtering**: MCP filters available tools based on granted scopes
4. **Runtime Validation**: Scopes checked when tools are invoked

## Notes

- Default behavior grants read-only access if no scopes specified
- Embedded agent tools don't require scope binding
- Documentation tools always available regardless of scopes

## Troubleshooting

| Issue | Solution |
|-------|----------|
| Tool not in list | Check required scopes are granted |
| "Tool not allowed" error | Re-authenticate with higher permission level |
| Invalid scope | Use lowercase with colon separator (e.g., `event:write`) |

## References

- Adding Tools: @docs/adding-tools.mdc — Add tools with scope requirements
- Testing: @docs/testing.mdc — Test with different scope configurations

```

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

```typescript
import { tool } from "ai";
import { z } from "zod";
import { UserInputError } from "../../../errors";
import { ApiClientError, ApiServerError } from "../../../api-client";
import { logIssue, logWarn } from "../../../telem/logging";

/**
 * Standard response schema for all embedded agent tools.
 * Tools return either an error message or the result data, never both.
 */
const AgentToolResponseSchema = z.object({
  error: z
    .string()
    .optional()
    .describe("Error message if the operation failed"),
  result: z.unknown().optional().describe("The successful result data"),
});

export type AgentToolResponse<T = unknown> = {
  error?: string;
  result?: T;
};

/**
 * Handles errors from agent tool execution and returns appropriate error messages.
 *
 * SECURITY: Only returns trusted error messages to prevent prompt injection.
 * We trust: Sentry API errors, our own UserInputError messages, and system templates.
 */
function handleAgentToolError<T>(error: unknown): AgentToolResponse<T> {
  if (error instanceof UserInputError) {
    // Log UserInputError for Sentry logging (as log, not exception)
    logWarn(error, {
      loggerScope: ["agent-tools", "user-input"],
      contexts: {
        agentTool: {
          errorType: "UserInputError",
        },
      },
    });
    return {
      error: `Input Error: ${error.message}. You may be able to resolve this by addressing the concern and trying again.`,
    };
  }

  if (error instanceof ApiClientError) {
    // Log ApiClientError for Sentry logging (as log, not exception)
    const message = error.toUserMessage();
    logWarn(message, {
      loggerScope: ["agent-tools", "api-client"],
      contexts: {
        agentTool: {
          errorType: error.name,
          status: error.status ?? null,
        },
      },
    });
    return {
      error: `Input Error: ${message}. You may be able to resolve this by addressing the concern and trying again.`,
    };
  }

  if (error instanceof ApiServerError) {
    // Log server errors to Sentry and get Event ID
    const eventId = logIssue(error);
    const statusText = error.status ? ` (${error.status})` : "";
    return {
      error: `Server Error${statusText}: ${error.message}. Event ID: ${eventId}. This is a system error that cannot be resolved by retrying.`,
    };
  }

  // Log unexpected errors to Sentry and return safe generic message
  // SECURITY: Don't return untrusted error messages that could enable prompt injection
  const eventId = logIssue(error);
  return {
    error: `System Error: An unexpected error occurred. Event ID: ${eventId}. This is a system error that cannot be resolved by retrying.`,
  };
}

/**
 * Creates an embedded agent tool with automatic error handling and schema wrapping.
 *
 * This wrapper:
 * - Maintains the same API as the AI SDK's tool() function
 * - Automatically wraps the result schema with error/result structure
 * - Handles all error types and returns them as structured responses
 * - Preserves type inference from the original tool implementation
 *
 * @example
 * ```typescript
 * export function createMyTool(apiService: SentryApiService) {
 *   return agentTool({
 *     description: "My tool description",
 *     parameters: z.object({ param: z.string() }),
 *     execute: async (params) => {
 *       // Tool implementation that might throw errors
 *       const result = await apiService.someMethod(params);
 *       return result; // Original return type preserved
 *     }
 *   });
 * }
 * ```
 */
export function agentTool<TParameters, TResult>(config: {
  description: string;
  parameters: z.ZodSchema<TParameters>;
  execute: (params: TParameters) => Promise<TResult>;
}) {
  // Infer the result type from the execute function's return type
  type InferredResult = Awaited<ReturnType<typeof config.execute>>;

  return tool({
    description: config.description,
    parameters: config.parameters,
    execute: async (
      params: TParameters,
    ): Promise<AgentToolResponse<InferredResult>> => {
      try {
        const result = await config.execute(params);
        return { result };
      } catch (error) {
        return handleAgentToolError<InferredResult>(error);
      }
    },
  });
}

```

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

```typescript
import { Header } from "./components/ui/header";
import { useState, useEffect } from "react";
import { Chat } from "./components/chat";
import { useAuth } from "./contexts/auth-context";
import Home from "./pages/home";

export default function App() {
  const { isAuthenticated, handleLogout } = useAuth();

  const [isChatOpen, setIsChatOpen] = useState(() => {
    // Initialize based on URL query string only to avoid hydration issues
    const urlParams = new URLSearchParams(window.location.search);
    const hasQueryParam = urlParams.has("chat");

    if (hasQueryParam) {
      return urlParams.get("chat") !== "0";
    }

    // Default based on screen size to avoid flash on mobile
    // Note: This is safe for SSR since we handle the correction in useEffect
    if (typeof window !== "undefined") {
      return window.innerWidth >= 768; // Desktop: open, Mobile: closed
    }

    // SSR fallback - default to true for desktop-first approach
    return true;
  });

  // Adjust initial state for mobile after component mounts
  useEffect(() => {
    const urlParams = new URLSearchParams(window.location.search);

    // Only adjust state if no URL parameter exists and we're on mobile
    if (!urlParams.has("chat") && window.innerWidth < 768) {
      setIsChatOpen(false);
    }
  }, []);

  // Update URL when chat state changes
  const toggleChat = (open: boolean) => {
    setIsChatOpen(open);

    if (open) {
      // Add ?chat to URL
      const newUrl = new URL(window.location.href);
      newUrl.searchParams.set("chat", "1");
      window.history.pushState({}, "", newUrl.toString());
    } else {
      // Remove query string for home page
      const newUrl = new URL(window.location.href);
      newUrl.search = "";
      window.history.pushState({}, "", newUrl.toString());
    }
  };

  // Handle browser back/forward navigation
  useEffect(() => {
    const handlePopState = () => {
      const urlParams = new URLSearchParams(window.location.search);
      const hasQueryParam = urlParams.has("chat");

      if (hasQueryParam) {
        setIsChatOpen(urlParams.get("chat") !== "0");
      } else {
        // Default to open on desktop, closed on mobile
        setIsChatOpen(window.innerWidth >= 768);
      }
    };

    window.addEventListener("popstate", handlePopState);
    return () => window.removeEventListener("popstate", handlePopState);
  }, []);

  // Handle window resize to adjust chat state appropriately
  useEffect(() => {
    const handleResize = () => {
      // If no explicit URL state, adjust based on screen size
      const urlParams = new URLSearchParams(window.location.search);
      if (!urlParams.has("chat")) {
        const isDesktop = window.innerWidth >= 768;
        setIsChatOpen(isDesktop); // Open on desktop, closed on mobile
      }
    };

    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return (
    <div className="min-h-screen text-white">
      {/* Mobile layout: Single column with overlay chat */}
      <div className="md:hidden h-screen flex flex-col">
        <div className="flex-1 overflow-y-auto sm:p-8 p-4">
          <div className="max-w-3xl mx-auto">
            <Header isAuthenticated={isAuthenticated} onLogout={handleLogout} />
            <Home onChatClick={() => toggleChat(true)} />
          </div>
        </div>
      </div>

      {/* Desktop layout: Main content adjusts width based on chat state */}
      <div className="hidden md:flex h-screen">
        <div
          className={`flex flex-col ${isChatOpen ? "w-1/2" : "flex-1"} md:transition-all md:duration-300`}
        >
          <div className="flex-1 overflow-y-auto sm:p-8 p-4">
            <div className="max-w-3xl mx-auto">
              <Header
                isAuthenticated={isAuthenticated}
                onLogout={handleLogout}
              />
              <Home onChatClick={() => toggleChat(true)} />
            </div>
          </div>
        </div>
      </div>

      {/* Single Chat component - handles both mobile and desktop layouts */}
      <Chat
        isOpen={isChatOpen}
        onClose={() => toggleChat(false)}
        onLogout={handleLogout}
      />
    </div>
  );
}

```

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

```typescript
#!/usr/bin/env node

/**
 * Main CLI entry point for the Sentry MCP server.
 *
 * Handles command-line argument parsing, environment configuration, Sentry
 * initialization, and starts the MCP server with stdio transport. Requires
 * a Sentry access token and optionally accepts host and DSN configuration.
 *
 * @example CLI Usage
 * ```bash
 * npx @sentry/mcp-server --access-token=TOKEN --host=sentry.io
 * npx @sentry/mcp-server --access-token=TOKEN --url=https://sentry.example.com
 * ```
 */

import { buildServer } from "./server";
import { startStdio } from "./transports/stdio";
import * as Sentry from "@sentry/node";
import { LIB_VERSION } from "./version";
import { buildUsage } from "./cli/usage";
import { parseArgv, parseEnv, merge } from "./cli/parse";
import { finalize } from "./cli/resolve";
import { sentryBeforeSend } from "./telem/sentry";
import { ALL_SCOPES } from "./permissions";
import { DEFAULT_SCOPES } from "./constants";
import { configureOpenAIProvider } from "./internal/agents/openai-provider";
import agentTools from "./tools/agent-tools";

const packageName = "@sentry/mcp-server";
const usageText = buildUsage(packageName, DEFAULT_SCOPES, ALL_SCOPES);

function die(message: string): never {
  console.error(message);
  console.error(usageText);
  process.exit(1);
}
const cli = parseArgv(process.argv.slice(2));
if (cli.help) {
  console.log(usageText);
  process.exit(0);
}
if (cli.version) {
  console.log(`${packageName} ${LIB_VERSION}`);
  process.exit(0);
}
if (cli.unknownArgs.length > 0) {
  console.error("Error: Invalid argument(s):", cli.unknownArgs.join(", "));
  console.error(usageText);
  process.exit(1);
}

const env = parseEnv(process.env);
const cfg = (() => {
  try {
    return finalize(merge(cli, env));
  } catch (err) {
    die(err instanceof Error ? err.message : String(err));
  }
})();

// Check for OpenAI API key and warn if missing
if (!process.env.OPENAI_API_KEY) {
  console.warn("Warning: OPENAI_API_KEY environment variable is not set.");
  console.warn("The following AI-powered search tools will be unavailable:");
  console.warn("  - search_events (natural language event search)");
  console.warn("  - search_issues (natural language issue search)");
  console.warn(
    "All other tools will function normally. To enable AI-powered search, set OPENAI_API_KEY.",
  );
  console.warn("");
}

configureOpenAIProvider({ baseUrl: cfg.openaiBaseUrl });

Sentry.init({
  dsn: cfg.sentryDsn,
  sendDefaultPii: true,
  tracesSampleRate: 1,
  beforeSend: sentryBeforeSend,
  initialScope: {
    tags: {
      "mcp.server_version": LIB_VERSION,
      "mcp.transport": "stdio",
      "mcp.agent_mode": cli.agent ? "true" : "false",
      "sentry.host": cfg.sentryHost,
      "mcp.mcp-url": cfg.mcpUrl,
    },
  },
  release: process.env.SENTRY_RELEASE,
  integrations: [
    Sentry.consoleLoggingIntegration(),
    Sentry.zodErrorsIntegration(),
    Sentry.vercelAIIntegration({
      recordInputs: true,
      recordOutputs: true,
    }),
  ],
  environment:
    process.env.SENTRY_ENVIRONMENT ??
    (process.env.NODE_ENV !== "production" ? "development" : "production"),
});

// Log agent mode status
if (cli.agent) {
  console.warn("Agent mode enabled: Only use_sentry tool is available.");
  console.warn(
    "The use_sentry tool provides access to all Sentry operations through natural language.",
  );
  console.warn("");
}

const SENTRY_TIMEOUT = 5000; // 5 seconds

// Build context once for server configuration and runtime
const context = {
  accessToken: cfg.accessToken,
  grantedScopes: cfg.finalScopes,
  constraints: {
    organizationSlug: cfg.organizationSlug ?? null,
    projectSlug: cfg.projectSlug ?? null,
  },
  sentryHost: cfg.sentryHost,
  mcpUrl: cfg.mcpUrl,
  openaiBaseUrl: cfg.openaiBaseUrl,
};

// Build server with context to filter tools based on granted scopes
// Use agentTools when --agent flag is set (only exposes use_sentry tool)
const server = buildServer({
  context,
  tools: cli.agent ? agentTools : undefined,
});

startStdio(server, context).catch((err) => {
  console.error("Server error:", err);
  // ensure we've flushed all events
  Sentry.flush(SENTRY_TIMEOUT);
  process.exit(1);
});

// ensure we've flushed all events
Sentry.flush(SENTRY_TIMEOUT);

```

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

```typescript
import { parseArgs } from "node:util";
import type { CliArgs, EnvArgs, MergedArgs } from "./types";

export function parseArgv(argv: string[]): CliArgs {
  const options = {
    "access-token": { type: "string" as const },
    host: { type: "string" as const },
    url: { type: "string" as const },
    "mcp-url": { type: "string" as const },
    "sentry-dsn": { type: "string" as const },
    "openai-base-url": { type: "string" as const },
    "organization-slug": { type: "string" as const },
    "project-slug": { type: "string" as const },
    scopes: { type: "string" as const },
    "add-scopes": { type: "string" as const },
    "all-scopes": { type: "boolean" as const },
    agent: { type: "boolean" as const },
    help: { type: "boolean" as const, short: "h" as const },
    version: { type: "boolean" as const, short: "v" as const },
  };

  const { values, positionals, tokens } = parseArgs({
    args: argv,
    options,
    allowPositionals: false,
    strict: false,
    tokens: true,
  });

  const knownLong = new Set(Object.keys(options));
  const knownShort = new Set([
    ...(Object.values(options)
      .map((o) => ("short" in o ? (o.short as string | undefined) : undefined))
      .filter(Boolean) as string[]),
  ]);

  const unknownArgs: string[] = [];
  for (const t of (tokens as any[]) || []) {
    if (t.kind === "option") {
      const name = t.name as string | undefined;
      if (name && !(knownLong.has(name) || knownShort.has(name))) {
        unknownArgs.push((t.raw as string) ?? `--${name}`);
      }
    } else if (t.kind === "positional") {
      unknownArgs.push((t.raw as string) ?? String(t.value ?? ""));
    }
  }

  return {
    accessToken: values["access-token"] as string | undefined,
    host: values.host as string | undefined,
    url: values.url as string | undefined,
    mcpUrl: values["mcp-url"] as string | undefined,
    sentryDsn: values["sentry-dsn"] as string | undefined,
    openaiBaseUrl: values["openai-base-url"] as string | undefined,
    organizationSlug: values["organization-slug"] as string | undefined,
    projectSlug: values["project-slug"] as string | undefined,
    scopes: values.scopes as string | undefined,
    addScopes: values["add-scopes"] as string | undefined,
    allScopes: (values["all-scopes"] as boolean | undefined) === true,
    agent: (values.agent as boolean | undefined) === true,
    help: (values.help as boolean | undefined) === true,
    version: (values.version as boolean | undefined) === true,
    unknownArgs:
      unknownArgs.length > 0 ? unknownArgs : (positionals as string[]) || [],
  };
}

export function parseEnv(env: NodeJS.ProcessEnv): EnvArgs {
  const fromEnv: EnvArgs = {};
  if (env.SENTRY_ACCESS_TOKEN) fromEnv.accessToken = env.SENTRY_ACCESS_TOKEN;
  if (env.SENTRY_URL) fromEnv.url = env.SENTRY_URL;
  if (env.SENTRY_HOST) fromEnv.host = env.SENTRY_HOST;
  if (env.MCP_URL) fromEnv.mcpUrl = env.MCP_URL;
  if (env.SENTRY_DSN || env.DEFAULT_SENTRY_DSN)
    fromEnv.sentryDsn = env.SENTRY_DSN || env.DEFAULT_SENTRY_DSN;
  if (env.MCP_SCOPES) fromEnv.scopes = env.MCP_SCOPES;
  if (env.MCP_ADD_SCOPES) fromEnv.addScopes = env.MCP_ADD_SCOPES;
  return fromEnv;
}

export function merge(cli: CliArgs, env: EnvArgs): MergedArgs {
  // CLI wins over env
  const merged: MergedArgs = {
    accessToken: cli.accessToken ?? env.accessToken,
    // If CLI provided url/host, prefer those; else fall back to env
    url: cli.url ?? env.url,
    host: cli.host ?? env.host,
    mcpUrl: cli.mcpUrl ?? env.mcpUrl,
    sentryDsn: cli.sentryDsn ?? env.sentryDsn,
    openaiBaseUrl: cli.openaiBaseUrl,
    // Scopes precedence: CLI scopes/add-scopes override their env counterparts
    scopes: cli.scopes ?? env.scopes,
    addScopes: cli.addScopes ?? env.addScopes,
    allScopes: cli.allScopes === true,
    agent: cli.agent === true,
    organizationSlug: cli.organizationSlug,
    projectSlug: cli.projectSlug,
    help: cli.help === true,
    version: cli.version === true,
    unknownArgs: cli.unknownArgs,
  };

  // If CLI provided scopes, ignore additive env var
  if (cli.scopes) merged.addScopes = cli.addScopes;
  // If CLI provided add-scopes, ensure scopes override isn't pulled from env
  if (cli.addScopes) merged.scopes = cli.scopes;
  return merged;
}

```

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

```typescript
import { z } from "zod";
import { setTag } from "@sentry/core";
import { defineTool } from "../internal/tool-helpers/define";
import { fetchWithTimeout } from "../internal/fetch-utils";
import { UserInputError } from "../errors";
import { ApiError } from "../api-client/index";
import type { ServerContext } from "../types";

export default defineTool({
  name: "get_doc",
  requiredScopes: ["docs"], // Documentation reading requires docs permission
  description: [
    "Fetch the full markdown content of a Sentry documentation page.",
    "",
    "Use this tool when you need to:",
    "- Read the complete documentation for a specific topic",
    "- Get detailed implementation examples or code snippets",
    "- Access the full context of a documentation page",
    "- Extract specific sections from documentation",
    "",
    "<examples>",
    "### Get the Next.js integration guide",
    "",
    "```",
    "get_doc(path='/platforms/javascript/guides/nextjs.md')",
    "```",
    "</examples>",
    "",
    "<hints>",
    "- Use the path from search_docs results for accurate fetching",
    "- Paths should end with .md extension",
    "</hints>",
  ].join("\n"),
  inputSchema: {
    path: z
      .string()
      .trim()
      .describe(
        "The documentation path (e.g., '/platforms/javascript/guides/nextjs.md'). Get this from search_docs results.",
      ),
  },
  annotations: {
    readOnlyHint: true,
    openWorldHint: true,
  },
  async handler(params, context: ServerContext) {
    setTag("doc.path", params.path);

    let output = `# Documentation Content\n\n`;
    output += `**Path**: ${params.path}\n\n`;

    // Validate path format
    if (!params.path.endsWith(".md")) {
      throw new UserInputError(
        "Invalid documentation path. Path must end with .md extension.",
      );
    }

    // Use docs.sentry.io for now - will be configurable via flag in the future
    const baseUrl = "https://docs.sentry.io";

    // Construct the full URL for the markdown file
    const docUrl = new URL(params.path, baseUrl);

    // Validate domain whitelist for security
    const allowedDomains = ["docs.sentry.io", "develop.sentry.io"];
    if (!allowedDomains.includes(docUrl.hostname)) {
      throw new UserInputError(
        `Invalid domain. Documentation can only be fetched from allowed domains: ${allowedDomains.join(", ")}`,
      );
    }

    const response = await fetchWithTimeout(
      docUrl.toString(),
      {
        headers: {
          Accept: "text/plain, text/markdown",
          "User-Agent": "Sentry-MCP/1.0",
        },
      },
      15000, // 15 second timeout
    );

    if (!response.ok) {
      if (response.status === 404) {
        output += `**Error**: Documentation not found at this path.\n\n`;
        output += `Please verify the path is correct. Common issues:\n`;
        output += `- Path should start with / (e.g., /platforms/javascript/guides/nextjs.md)\n`;
        output += `- Path should match exactly what's shown in search_docs results\n`;
        output += `- Some pages may have been moved or renamed\n\n`;
        output += `Try searching again with \`search_docs()\` to find the correct path.\n`;
        return output;
      }

      throw new ApiError(
        `Failed to fetch documentation: ${response.statusText}`,
        response.status,
      );
    }

    const content = await response.text();

    // Check if we got HTML instead of markdown (wrong path format)
    if (
      content.trim().startsWith("<!DOCTYPE") ||
      content.trim().startsWith("<html")
    ) {
      output += `> **Error**: Received HTML instead of markdown. The path may be incorrect.\n\n`;
      output += `Make sure to use the .md extension in the path.\n`;
      output += `Example: /platforms/javascript/guides/nextjs.md\n`;
      return output;
    }

    // Add the markdown content
    output += "---\n\n";
    output += content;
    output += "\n\n---\n\n";

    output += "## Using this documentation\n\n";
    output +=
      "- This is the raw markdown content from Sentry's documentation\n";
    output +=
      "- Code examples and configuration snippets can be copied directly\n";
    output +=
      "- Links in the documentation are relative to https://docs.sentry.io\n";
    output +=
      "- For more related topics, use `search_docs()` to find additional pages\n";

    return output;
  },
});

```

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

```typescript
import { z } from "zod";
import { defineTool } from "../internal/tool-helpers/define";
import { fetchWithTimeout } from "../internal/fetch-utils";
import { ApiError } from "../api-client/index";
import type { ServerContext } from "../types";
import type { SearchResponse } from "./types";
import { ParamSentryGuide } from "../schema";

export default defineTool({
  name: "search_docs",
  requiredScopes: ["docs"], // Documentation search requires docs permission
  description: [
    "Search Sentry documentation for SDK setup, instrumentation, and configuration guidance.",
    "",
    "Use this tool when you need to:",
    "- Set up Sentry SDK or framework integrations (Django, Flask, Express, Next.js, etc.)",
    "- Configure features like performance monitoring, error sampling, or release tracking",
    "- Implement custom instrumentation (spans, transactions, breadcrumbs)",
    "- Configure data scrubbing, filtering, or sampling rules",
    "",
    "Returns snippets only. Use `get_doc(path='...')` to fetch full documentation content.",
    "",
    "<examples>",
    "```",
    "search_docs(query='Django setup configuration SENTRY_DSN', guide='python/django')",
    "search_docs(query='source maps webpack upload', guide='javascript/nextjs')",
    "```",
    "</examples>",
    "",
    "<hints>",
    "- Use guide parameter to filter to specific technologies (e.g., 'javascript/nextjs')",
    "- Include specific feature names like 'beforeSend', 'tracesSampleRate', 'SENTRY_DSN'",
    "</hints>",
  ].join("\n"),
  inputSchema: {
    query: z
      .string()
      .trim()
      .min(
        2,
        "Search query is too short. Please provide at least 2 characters.",
      )
      .max(
        200,
        "Search query is too long. Please keep your query under 200 characters.",
      )
      .describe(
        "The search query in natural language. Be specific about what you're looking for.",
      ),
    maxResults: z
      .number()
      .int()
      .min(1)
      .max(10)
      .default(3)
      .describe("Maximum number of results to return (1-10)")
      .optional(),
    guide: ParamSentryGuide.optional(),
  },
  annotations: {
    readOnlyHint: true,
    openWorldHint: true,
  },
  async handler(params, context: ServerContext) {
    let output = `# Documentation Search Results\n\n`;
    output += `**Query**: "${params.query}"\n`;
    if (params.guide) {
      output += `**Guide**: ${params.guide}\n`;
    }
    output += `\n`;

    // Determine the URL - use context.mcpUrl if available, otherwise default to production
    const host = context.mcpUrl || "https://mcp.sentry.dev";
    const searchUrl = new URL("/api/search", host);

    const response = await fetchWithTimeout(
      searchUrl.toString(),
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          query: params.query,
          maxResults: params.maxResults,
          guide: params.guide,
        }),
      },
      15000, // 15 second timeout
    );

    if (!response.ok) {
      // TODO: improve error responses with types
      const errorData = (await response.json().catch(() => null)) as {
        error?: string;
      } | null;

      const errorMessage =
        errorData?.error || `Search failed with status ${response.status}`;
      throw new ApiError(errorMessage, response.status);
    }

    const data = (await response.json()) as SearchResponse;

    // Handle error in response
    if ("error" in data && data.error) {
      output += `**Error**: ${data.error}\n\n`;
      return output;
    }

    // Display results
    if (data.results.length === 0) {
      output += "No documentation found matching your query.\n\n";
      return output;
    }

    output += `Found ${data.results.length} match${data.results.length === 1 ? "" : "es"}\n\n`;

    output += `These are just snippets. Use \`get_doc(path='...')\` to fetch the full content.\n\n`;

    for (const [index, result] of data.results.entries()) {
      output += `## ${index + 1}. ${result.url}\n\n`;
      output += `**Path**: ${result.id}\n`;
      output += `**Relevance**: ${(result.relevance * 100).toFixed(1)}%\n\n`;
      if (index < 3) {
        output += "**Matching Context**\n";
        output += `> ${result.snippet.replace(/\n/g, "\n> ")}\n\n`;
      }
    }

    return output;
  },
});

```

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

```typescript
/**
 * MCP Handler using experimental_createMcpHandler from Cloudflare agents library.
 *
 * Stateless request handling approach:
 * - Uses experimental_createMcpHandler to wrap the MCP server
 * - Extracts auth props directly from ExecutionContext (set by OAuth provider)
 * - Context captured in tool handler closures during buildServer()
 * - No session state required - each request is independent
 */

import * as Sentry from "@sentry/cloudflare";
import { experimental_createMcpHandler as createMcpHandler } from "agents/mcp";
import { buildServer } from "@sentry/mcp-server/server";
import {
  expandScopes,
  parseScopes,
  type Scope,
} from "@sentry/mcp-server/permissions";
import { logWarn } from "@sentry/mcp-server/telem/logging";
import type { ServerContext } from "@sentry/mcp-server/types";
import type { Env } from "../types";
import { verifyConstraintsAccess } from "./constraint-utils";
import type { ExportedHandler } from "@cloudflare/workers-types";
import agentTools from "@sentry/mcp-server/tools/agent-tools";

/**
 * ExecutionContext with OAuth props injected by the OAuth provider.
 */
type OAuthExecutionContext = ExecutionContext & {
  props?: Record<string, unknown>;
};

/**
 * Main request handler that:
 * 1. Extracts auth props from ExecutionContext
 * 2. Parses org/project constraints from URL
 * 3. Verifies user has access to the constraints
 * 4. Builds complete ServerContext
 * 5. Creates and configures MCP server per-request (context captured in closures)
 * 6. Runs MCP handler
 */
const mcpHandler: ExportedHandler<Env> = {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext,
  ): Promise<Response> {
    const url = new URL(request.url);

    // Parse constraints from URL pattern /mcp/:org?/:project?
    const pattern = new URLPattern({ pathname: "/mcp/:org?/:project?" });
    const result = pattern.exec(url);

    if (!result) {
      return new Response("Not found", { status: 404 });
    }

    const { groups } = result.pathname;
    const organizationSlug = groups?.org || null;
    const projectSlug = groups?.project || null;

    // Check for agent mode query parameter
    const isAgentMode = url.searchParams.get("agent") === "1";

    // Extract OAuth props from ExecutionContext (set by OAuth provider)
    const oauthCtx = ctx as OAuthExecutionContext;
    if (!oauthCtx.props) {
      throw new Error("No authentication context available");
    }

    const sentryHost = env.SENTRY_HOST || "sentry.io";

    // Verify user has access to the requested org/project
    const verification = await verifyConstraintsAccess(
      { organizationSlug, projectSlug },
      {
        accessToken: oauthCtx.props.accessToken as string,
        sentryHost,
      },
    );

    if (!verification.ok) {
      return new Response(verification.message, {
        status: verification.status ?? 500,
      });
    }

    // Parse and expand granted scopes
    let expandedScopes: Set<Scope> | undefined;
    if (oauthCtx.props.grantedScopes) {
      const { valid, invalid } = parseScopes(
        oauthCtx.props.grantedScopes as string[],
      );
      if (invalid.length > 0) {
        logWarn("Ignoring invalid scopes from OAuth provider", {
          loggerScope: ["cloudflare", "mcp-handler"],
          extra: {
            invalidScopes: invalid,
          },
        });
      }
      expandedScopes = expandScopes(new Set(valid));
    }

    // Build complete ServerContext from OAuth props + verified constraints
    const serverContext: ServerContext = {
      userId: oauthCtx.props.id as string | undefined,
      clientId: oauthCtx.props.clientId as string,
      accessToken: oauthCtx.props.accessToken as string,
      grantedScopes: expandedScopes,
      constraints: verification.constraints,
      sentryHost,
      mcpUrl: env.MCP_URL,
    };

    // Create and configure MCP server with tools filtered by context
    // Context is captured in tool handler closures during buildServer()
    const server = buildServer({
      context: serverContext,
      tools: isAgentMode ? agentTools : undefined,
      onToolComplete: () => {
        // Flush Sentry events after tool execution
        Sentry.flush(2000);
      },
    });

    // Run MCP handler - context already captured in closures
    return createMcpHandler(server, {
      route: url.pathname,
    })(request, env, ctx);
  },
};

export default mcpHandler;

```

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

```yaml
name: Smoke Tests (Local)

permissions:
  contents: read
  checks: write

on:
  push:
    branches: [main]
  pull_request:

jobs:
  smoke-tests:
    name: Run Smoke Tests Against Local Server
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      # pnpm/action-setup@v4
      - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda
        name: Install pnpm
        with:
          run_install: false

      - name: Get pnpm store directory
        shell: bash
        run: |
          echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

      - uses: actions/cache@v4
        name: Setup pnpm cache
        with:
          path: ${{ env.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-

      - name: Install dependencies
        run: pnpm install --no-frozen-lockfile
      
      - name: Build
        run: pnpm build

      - name: Start local dev server
        working-directory: packages/mcp-cloudflare
        run: |
          # Start wrangler in background and capture output
          pnpm exec wrangler dev --port 8788 --local > wrangler.log 2>&1 &
          WRANGLER_PID=$!
          echo "WRANGLER_PID=$WRANGLER_PID" >> $GITHUB_ENV
          echo "Waiting for server to start (PID: $WRANGLER_PID)..."
          
          # Wait for server to be ready (up to 2 minutes)
          MAX_ATTEMPTS=24
          ATTEMPT=0
          while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
            # Check if wrangler process is still running
            if ! kill -0 $WRANGLER_PID 2>/dev/null; then
              echo "❌ Wrangler process died unexpectedly!"
              echo "📋 Last 50 lines of wrangler.log:"
              tail -50 wrangler.log
              exit 1
            fi
            
            if curl -s -f -o /dev/null http://localhost:8788/; then
              echo "✅ Server is ready!"
              echo "📋 Wrangler startup log:"
              cat wrangler.log
              break
            else
              echo "⏳ Waiting for server to start (attempt $((ATTEMPT+1))/$MAX_ATTEMPTS)..."
              # Show partial log every 5 attempts
              if [ $((ATTEMPT % 5)) -eq 0 ] && [ $ATTEMPT -gt 0 ]; then
                echo "📋 Current wrangler.log output:"
                tail -20 wrangler.log
              fi
            fi
            
            ATTEMPT=$((ATTEMPT+1))
            sleep 5
          done
          
          if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
            echo "❌ Server failed to start after $MAX_ATTEMPTS attempts"
            echo "📋 Full wrangler.log:"
            cat wrangler.log
            exit 1
          fi

      - name: Run smoke tests against local server
        env:
          PREVIEW_URL: http://localhost:8788
        working-directory: packages/smoke-tests
        run: |
          echo "🧪 Running smoke tests against local server at $PREVIEW_URL"
          
          # Give server a bit more time to stabilize after startup
          echo "⏳ Waiting 5 seconds for server to stabilize..."
          sleep 5
          
          # Verify server is still responding before running tests
          if ! curl -s -f -o /dev/null http://localhost:8788/; then
            echo "❌ Server is not responding before tests!"
            echo "📋 Wrangler log:"
            cat ../mcp-cloudflare/wrangler.log
            exit 1
          fi
          
          echo "✅ Server is responding, running tests..."
          pnpm test:ci || TEST_EXIT_CODE=$?
          
          # If tests failed, show server logs for debugging
          if [ "${TEST_EXIT_CODE:-0}" -ne 0 ]; then
            echo "❌ Tests failed with exit code ${TEST_EXIT_CODE}"
            echo "📋 Wrangler log at time of failure:"
            cat ../mcp-cloudflare/wrangler.log
            exit ${TEST_EXIT_CODE}
          fi

      - name: Publish Smoke Test Report
        uses: mikepenz/action-junit-report@cf701569b05ccdd861a76b8607a66d76f6fd4857
        if: always()
        with:
          report_paths: "packages/smoke-tests/tests.junit.xml"
          check_name: "Local Smoke Test Results"
          fail_on_failure: true

      - name: Stop local server
        if: always()
        run: |
          if [ ! -z "$WRANGLER_PID" ]; then
            kill $WRANGLER_PID || true
          fi
```

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

```typescript
/**
 * MCP Metadata API endpoint
 *
 * Provides immediate access to MCP server metadata including tools
 * without requiring a chat stream to be initialized.
 */
import { Hono } from "hono";
import { experimental_createMCPClient } from "ai";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { Env } from "../types";
import { logIssue, logWarn } from "@sentry/mcp-server/telem/logging";
import type { ErrorResponse } from "../types/chat";
import { analyzeAuthError, getAuthErrorResponse } from "../utils/auth-errors";
import { z } from "zod";

type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient>>;

function createErrorResponse(errorResponse: ErrorResponse): ErrorResponse {
  return errorResponse;
}

export default new Hono<{ Bindings: Env }>().get("/", async (c) => {
  // Support cookie-based auth (preferred) with fallback to Authorization header
  let accessToken: string | null = null;

  // Try to read from signed cookie set during OAuth
  try {
    const { getCookie } = await import("hono/cookie");
    const authDataCookie = getCookie(c, "sentry_auth_data");
    if (authDataCookie) {
      const AuthDataSchema = z.object({ access_token: z.string() });
      const authData = AuthDataSchema.parse(JSON.parse(authDataCookie));
      accessToken = authData.access_token;
    }
  } catch {
    // Ignore cookie parse errors; we'll check header below
  }

  // Fallback to Authorization header if cookie is not present
  if (!accessToken) {
    const authHeader = c.req.header("Authorization");
    if (authHeader?.startsWith("Bearer ")) {
      accessToken = authHeader.substring(7);
    }
  }

  if (!accessToken) {
    return c.json(
      createErrorResponse({
        error: "Authorization required",
        name: "MISSING_AUTH_TOKEN",
      }),
      401,
    );
  }

  // Declare mcpClient in outer scope for cleanup in catch block
  let mcpClient: MCPClient | undefined;

  try {
    // Get tools by connecting to MCP server
    let tools: string[] = [];
    try {
      const requestUrl = new URL(c.req.url);
      const mcpUrl = `${requestUrl.protocol}//${requestUrl.host}/mcp`;

      const httpTransport = new StreamableHTTPClientTransport(new URL(mcpUrl), {
        requestInit: {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        },
      });

      mcpClient = await experimental_createMCPClient({
        name: "sentry",
        transport: httpTransport,
      });

      const mcpTools = await mcpClient.tools();
      tools = Object.keys(mcpTools);
    } catch (error) {
      // If we can't get tools, return empty array
      logWarn(error, {
        loggerScope: ["cloudflare", "metadata"],
        extra: {
          message: "Failed to fetch tools from MCP server",
        },
      });
    } finally {
      // Ensure the MCP client connection is properly closed to prevent hanging connections
      if (mcpClient && typeof mcpClient.close === "function") {
        try {
          await mcpClient.close();
        } catch (closeError) {
          logWarn(closeError, {
            loggerScope: ["cloudflare", "metadata"],
            extra: {
              message: "Failed to close MCP client connection",
            },
          });
        }
      }
    }

    // Return the metadata
    return c.json({
      type: "mcp-metadata",
      tools,
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    // Cleanup mcpClient if it was created
    if (mcpClient && typeof mcpClient.close === "function") {
      try {
        await mcpClient.close();
      } catch (closeError) {
        logWarn(closeError, {
          loggerScope: ["cloudflare", "metadata"],
          extra: {
            message: "Failed to close MCP client connection in error handler",
          },
        });
      }
    }

    logIssue(error, {
      loggerScope: ["cloudflare", "metadata"],
      extra: {
        message: "Metadata API error",
      },
    });

    // Check if this is an authentication error
    const authInfo = analyzeAuthError(error);
    if (authInfo.isAuthError) {
      return c.json(
        createErrorResponse(getAuthErrorResponse(authInfo)),
        authInfo.statusCode || (401 as any),
      );
    }

    const eventId = logIssue(error);
    return c.json(
      createErrorResponse({
        error: "Failed to fetch MCP metadata",
        name: "METADATA_FETCH_FAILED",
        eventId,
      }),
      500,
    );
  }
});

```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/chat/chat-input.tsx:
--------------------------------------------------------------------------------

```typescript
import { useEffect, useRef } from "react";
import { Send, CircleStop } from "lucide-react";
import { Button } from "../ui/button";

interface ChatInputProps {
  input: string;
  isLoading: boolean;
  isOpen: boolean;
  onInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
  onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
  onStop: () => void;
  onSlashCommand?: (command: string) => void;
}

export function ChatInput({
  input,
  isLoading,
  isOpen,
  onInputChange,
  onSubmit,
  onStop,
  onSlashCommand,
}: ChatInputProps) {
  const inputRef = useRef<HTMLTextAreaElement>(null);

  // Focus when dialog opens (with delay for mobile animation)
  useEffect(() => {
    if (isOpen) {
      // Add delay to ensure the slide-in animation completes on mobile
      const timer = setTimeout(() => {
        // Use requestAnimationFrame to ensure browser has finished layout
        requestAnimationFrame(() => {
          if (inputRef.current && !inputRef.current.disabled) {
            inputRef.current.focus({ preventScroll: false });
          }
        });
      }, 600); // Delay to account for 500ms animation
      return () => clearTimeout(timer);
    }
  }, [isOpen]);

  // Re-focus when loading finishes
  useEffect(() => {
    if (inputRef.current && !isLoading && isOpen) {
      inputRef.current.focus();
    }
  }, [isLoading, isOpen]);

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    // Check if input is a slash command
    if (input.startsWith("/") && onSlashCommand) {
      const command = input.slice(1).toLowerCase().trim();
      // Pass all slash commands to the handler, let it decide what to do
      onSlashCommand(command);
      return;
    }

    // Otherwise, submit normally
    onSubmit(e);
  };

  // Handle keyboard shortcuts
  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    // Ctrl+J or Cmd+J: Insert newline
    if ((e.ctrlKey || e.metaKey) && e.key === "j") {
      e.preventDefault();
      const textarea = e.currentTarget;
      const start = textarea.selectionStart;
      const end = textarea.selectionEnd;
      const newValue = `${input.substring(0, start)}\n${input.substring(end)}`;

      // Create a synthetic event to update the input
      const syntheticEvent = {
        target: { value: newValue },
        currentTarget: { value: newValue },
      } as React.ChangeEvent<HTMLTextAreaElement>;

      onInputChange(syntheticEvent);

      // Move cursor after the inserted newline
      setTimeout(() => {
        if (textarea) {
          textarea.selectionStart = textarea.selectionEnd = start + 1;
        }
      }, 0);
      return;
    }

    // Enter without shift: Submit
    if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
      e.preventDefault();
      const form = e.currentTarget.form;
      if (form && input.trim()) {
        form.requestSubmit();
      }
    }
  };

  // Auto-resize textarea based on content
  // biome-ignore lint/correctness/useExhaustiveDependencies: input is needed to trigger resize when content changes
  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.style.height = "auto";
      inputRef.current.style.height = `${inputRef.current.scrollHeight}px`;
    }
  }, [input]);

  return (
    <form onSubmit={handleSubmit} className="relative flex-1">
      <div className="relative">
        <textarea
          ref={inputRef}
          value={input}
          onChange={onInputChange}
          onKeyDown={handleKeyDown}
          placeholder="Ask me anything about your Sentry data..."
          disabled={isLoading}
          rows={1}
          className="w-full p-4 pr-12 rounded bg-slate-800/50 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-300 focus:border-transparent disabled:opacity-50 resize-none overflow-hidden"
        />
        <Button
          type={isLoading ? "button" : "submit"}
          variant="ghost"
          onClick={isLoading ? onStop : undefined}
          disabled={!isLoading && !input.trim()}
          className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-slate-400 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:text-slate-400 disabled:hover:bg-transparent transition-colors"
          title={isLoading ? "Stop generation" : "Send message"}
        >
          {isLoading ? (
            <CircleStop className="h-4 w-4" />
          ) : (
            <Send className="h-4 w-4" />
          )}
        </Button>
      </div>
    </form>
  );
}

```
Page 3/12FirstPrevNextLast