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

# Directory Structure

```
├── .claude
│   ├── agents
│   │   └── claude-optimizer.md
│   ├── commands
│   │   ├── gh-pr.md
│   │   └── gh-review.md
│   └── settings.json
├── .craft.yml
├── .cursor
│   └── mcp.json
├── .env.example
├── .github
│   └── workflows
│       ├── deploy.yml
│       ├── eval.yml
│       ├── merge-jobs.yml
│       ├── release.yml
│       ├── smoke-tests.yml
│       ├── test.yml
│       └── token-cost.yml
├── .gitignore
├── .mcp.json
├── .vscode
│   ├── extensions.json
│   ├── mcp.json
│   └── settings.json
├── AGENTS.md
├── benchmark-agent.sh
├── bin
│   └── bump-version.sh
├── biome.json
├── CLAUDE.md
├── codecov.yml
├── core
├── docs
│   ├── adding-tools.md
│   ├── api-patterns.md
│   ├── architecture.md
│   ├── cloudflare
│   │   ├── architecture.md
│   │   ├── oauth-architecture.md
│   │   └── overview.md
│   ├── coding-guidelines.md
│   ├── common-patterns.md
│   ├── error-handling.md
│   ├── github-actions.md
│   ├── llms
│   │   ├── document-scopes.md
│   │   ├── documentation-style-guide.md
│   │   └── README.md
│   ├── logging.md
│   ├── monitoring.md
│   ├── pr-management.md
│   ├── quality-checks.md
│   ├── README.md
│   ├── releases
│   │   ├── cloudflare.md
│   │   └── stdio.md
│   ├── search-events-api-patterns.md
│   ├── security.md
│   ├── specs
│   │   ├── README.md
│   │   ├── search-events.md
│   │   └── subpath-constraints.md
│   ├── testing-remote.md
│   ├── testing-stdio.md
│   ├── testing.md
│   └── token-cost-tracking.md
├── LICENSE.md
├── Makefile
├── package.json
├── packages
│   ├── mcp-cloudflare
│   │   ├── .env.example
│   │   ├── components.json
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── public
│   │   │   ├── demo.cast
│   │   │   ├── favicon.ico
│   │   │   ├── flow-transparent.png
│   │   │   ├── flow.jpg
│   │   │   ├── keycap-⌘.png
│   │   │   ├── keycap-c.png
│   │   │   └── keycap-v.png
│   │   ├── src
│   │   │   ├── client
│   │   │   │   ├── app.tsx
│   │   │   │   ├── components
│   │   │   │   │   ├── animation
│   │   │   │   │   │   ├── browser-ui
│   │   │   │   │   │   │   ├── BrowserWindow.tsx
│   │   │   │   │   │   │   ├── BrowserWindowIconSidebar.tsx
│   │   │   │   │   │   │   ├── DiffBlock.tsx
│   │   │   │   │   │   │   ├── IDEWindow.tsx
│   │   │   │   │   │   │   ├── IssueDetails.tsx
│   │   │   │   │   │   │   ├── keys-copy.tsx
│   │   │   │   │   │   │   ├── LoadingSquares.tsx
│   │   │   │   │   │   │   ├── RootCause.tsx
│   │   │   │   │   │   │   ├── seer-clipmask.tsx
│   │   │   │   │   │   │   ├── seer-noisefilter.tsx
│   │   │   │   │   │   │   ├── seer.tsx
│   │   │   │   │   │   │   └── WindowHeader.tsx
│   │   │   │   │   │   ├── BrowserAnimation.tsx
│   │   │   │   │   │   ├── DataWire.tsx
│   │   │   │   │   │   ├── dracula.css
│   │   │   │   │   │   ├── terminal-ui
│   │   │   │   │   │   │   ├── keys-paste.tsx
│   │   │   │   │   │   │   ├── SpeedDisplay.tsx
│   │   │   │   │   │   │   └── StepsList.tsx
│   │   │   │   │   │   ├── TerminalAnimation.tsx
│   │   │   │   │   │   └── tests.tsx
│   │   │   │   │   ├── 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
│   │   │   │   │   │   ├── install-tabs.tsx
│   │   │   │   │   │   ├── remote-setup.tsx
│   │   │   │   │   │   ├── setup-guide.tsx
│   │   │   │   │   │   └── stdio-setup.tsx
│   │   │   │   │   ├── getting-started.tsx
│   │   │   │   │   ├── hero
│   │   │   │   │   │   ├── header-divider.tsx
│   │   │   │   │   │   └── hero-block.tsx
│   │   │   │   │   ├── home-layout
│   │   │   │   │   │   ├── footer.tsx
│   │   │   │   │   │   └── sidebars.tsx
│   │   │   │   │   ├── ui
│   │   │   │   │   │   ├── accordion.tsx
│   │   │   │   │   │   ├── backdrop.tsx
│   │   │   │   │   │   ├── badge.tsx
│   │   │   │   │   │   ├── base.tsx
│   │   │   │   │   │   ├── button.tsx
│   │   │   │   │   │   ├── code-snippet.tsx
│   │   │   │   │   │   ├── header.tsx
│   │   │   │   │   │   ├── icon.tsx
│   │   │   │   │   │   ├── icons
│   │   │   │   │   │   │   ├── claude.tsx
│   │   │   │   │   │   │   ├── codex.tsx
│   │   │   │   │   │   │   ├── cursor.tsx
│   │   │   │   │   │   │   ├── gemini.tsx
│   │   │   │   │   │   │   ├── sentry.tsx
│   │   │   │   │   │   │   ├── vscode.tsx
│   │   │   │   │   │   │   ├── warp.tsx
│   │   │   │   │   │   │   ├── windsurf.tsx
│   │   │   │   │   │   │   └── zed.tsx
│   │   │   │   │   │   ├── interactive-markdown.tsx
│   │   │   │   │   │   ├── json-schema-params.tsx
│   │   │   │   │   │   ├── key-icon.tsx
│   │   │   │   │   │   ├── key-word.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
│   │   │   │   │   └── usecases
│   │   │   │   │       ├── fix-bugs.tsx
│   │   │   │   │       ├── index.tsx
│   │   │   │   │       ├── instrument.tsx
│   │   │   │   │       ├── search-things.tsx
│   │   │   │   │       └── search-visual.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
│   │   │   │   ├── utils
│   │   │   │   │   ├── chat-error-handler.ts
│   │   │   │   │   ├── cursor-deeplink.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
│   │   │   │       ├── client-ip.test.ts
│   │   │   │       ├── client-ip.ts
│   │   │   │       ├── rate-limiter.test.ts
│   │   │   │       └── rate-limiter.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-core
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── scripts
│   │   │   ├── generate-definitions.ts
│   │   │   ├── generate-otel-namespaces.ts
│   │   │   ├── measure-token-cost.ts
│   │   │   └── validate-skills-mapping.ts
│   │   ├── src
│   │   │   ├── api-client
│   │   │   │   ├── client.test.ts
│   │   │   │   ├── client.ts
│   │   │   │   ├── errors.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── schema.test.ts
│   │   │   │   ├── schema.ts
│   │   │   │   └── types.ts
│   │   │   ├── constants.ts
│   │   │   ├── errors.test.ts
│   │   │   ├── errors.ts
│   │   │   ├── internal
│   │   │   │   ├── agents
│   │   │   │   │   ├── callEmbeddedAgent.ts
│   │   │   │   │   ├── openai-provider.test.ts
│   │   │   │   │   ├── openai-provider.ts
│   │   │   │   │   └── tools
│   │   │   │   │       ├── data
│   │   │   │   │       │   ├── __namespaces.json
│   │   │   │   │       │   ├── android.json
│   │   │   │   │       │   ├── app.json
│   │   │   │   │       │   ├── artifact.json
│   │   │   │   │       │   ├── aspnetcore.json
│   │   │   │   │       │   ├── aws.json
│   │   │   │   │       │   ├── azure.json
│   │   │   │   │       │   ├── browser.json
│   │   │   │   │       │   ├── cassandra.json
│   │   │   │   │       │   ├── cicd.json
│   │   │   │   │       │   ├── CLAUDE.md
│   │   │   │   │       │   ├── client.json
│   │   │   │   │       │   ├── cloud.json
│   │   │   │   │       │   ├── cloudevents.json
│   │   │   │   │       │   ├── cloudfoundry.json
│   │   │   │   │       │   ├── code.json
│   │   │   │   │       │   ├── container.json
│   │   │   │   │       │   ├── cpu.json
│   │   │   │   │       │   ├── cpython.json
│   │   │   │   │       │   ├── database.json
│   │   │   │   │       │   ├── db.json
│   │   │   │   │       │   ├── deployment.json
│   │   │   │   │       │   ├── destination.json
│   │   │   │   │       │   ├── device.json
│   │   │   │   │       │   ├── disk.json
│   │   │   │   │       │   ├── dns.json
│   │   │   │   │       │   ├── dotnet.json
│   │   │   │   │       │   ├── elasticsearch.json
│   │   │   │   │       │   ├── enduser.json
│   │   │   │   │       │   ├── error.json
│   │   │   │   │       │   ├── faas.json
│   │   │   │   │       │   ├── feature_flags.json
│   │   │   │   │       │   ├── file.json
│   │   │   │   │       │   ├── gcp.json
│   │   │   │   │       │   ├── gen_ai.json
│   │   │   │   │       │   ├── geo.json
│   │   │   │   │       │   ├── go.json
│   │   │   │   │       │   ├── graphql.json
│   │   │   │   │       │   ├── hardware.json
│   │   │   │   │       │   ├── heroku.json
│   │   │   │   │       │   ├── host.json
│   │   │   │   │       │   ├── http.json
│   │   │   │   │       │   ├── ios.json
│   │   │   │   │       │   ├── jvm.json
│   │   │   │   │       │   ├── k8s.json
│   │   │   │   │       │   ├── linux.json
│   │   │   │   │       │   ├── log.json
│   │   │   │   │       │   ├── mcp.json
│   │   │   │   │       │   ├── messaging.json
│   │   │   │   │       │   ├── network.json
│   │   │   │   │       │   ├── nodejs.json
│   │   │   │   │       │   ├── oci.json
│   │   │   │   │       │   ├── opentracing.json
│   │   │   │   │       │   ├── os.json
│   │   │   │   │       │   ├── otel.json
│   │   │   │   │       │   ├── peer.json
│   │   │   │   │       │   ├── process.json
│   │   │   │   │       │   ├── profile.json
│   │   │   │   │       │   ├── rpc.json
│   │   │   │   │       │   ├── server.json
│   │   │   │   │       │   ├── service.json
│   │   │   │   │       │   ├── session.json
│   │   │   │   │       │   ├── signalr.json
│   │   │   │   │       │   ├── source.json
│   │   │   │   │       │   ├── system.json
│   │   │   │   │       │   ├── telemetry.json
│   │   │   │   │       │   ├── test.json
│   │   │   │   │       │   ├── thread.json
│   │   │   │   │       │   ├── tls.json
│   │   │   │   │       │   ├── url.json
│   │   │   │   │       │   ├── user.json
│   │   │   │   │       │   ├── v8js.json
│   │   │   │   │       │   ├── vcs.json
│   │   │   │   │       │   ├── webengine.json
│   │   │   │   │       │   └── zos.json
│   │   │   │   │       ├── dataset-fields.test.ts
│   │   │   │   │       ├── dataset-fields.ts
│   │   │   │   │       ├── otel-semantics.test.ts
│   │   │   │   │       ├── otel-semantics.ts
│   │   │   │   │       ├── utils.ts
│   │   │   │   │       ├── whoami.test.ts
│   │   │   │   │       └── whoami.ts
│   │   │   │   ├── constraint-helpers.test.ts
│   │   │   │   ├── constraint-helpers.ts
│   │   │   │   ├── error-handling.ts
│   │   │   │   ├── fetch-utils.test.ts
│   │   │   │   ├── fetch-utils.ts
│   │   │   │   ├── formatting.test.ts
│   │   │   │   ├── formatting.ts
│   │   │   │   ├── issue-helpers.test.ts
│   │   │   │   ├── issue-helpers.ts
│   │   │   │   ├── test-fixtures.ts
│   │   │   │   └── tool-helpers
│   │   │   │       ├── api.test.ts
│   │   │   │       ├── api.ts
│   │   │   │       ├── define.ts
│   │   │   │       ├── enhance-error.ts
│   │   │   │       ├── formatting.ts
│   │   │   │       ├── issue.ts
│   │   │   │       ├── seer.test.ts
│   │   │   │       ├── seer.ts
│   │   │   │       ├── validate-region-url.test.ts
│   │   │   │       └── validate-region-url.ts
│   │   │   ├── permissions.parseScopes.test.ts
│   │   │   ├── permissions.ts
│   │   │   ├── schema.ts
│   │   │   ├── server.ts
│   │   │   ├── skillDefinitions.json
│   │   │   ├── skillDefinitions.ts
│   │   │   ├── skills.test.ts
│   │   │   ├── skills.ts
│   │   │   ├── telem
│   │   │   │   ├── index.ts
│   │   │   │   ├── logging.ts
│   │   │   │   ├── sentry.test.ts
│   │   │   │   └── sentry.ts
│   │   │   ├── test-setup.ts
│   │   │   ├── test-utils
│   │   │   │   └── context.ts
│   │   │   ├── toolDefinitions.json
│   │   │   ├── toolDefinitions.ts
│   │   │   ├── tools
│   │   │   │   ├── analyze-issue-with-seer.test.ts
│   │   │   │   ├── analyze-issue-with-seer.ts
│   │   │   │   ├── create-dsn.test.ts
│   │   │   │   ├── create-dsn.ts
│   │   │   │   ├── create-project.test.ts
│   │   │   │   ├── create-project.ts
│   │   │   │   ├── create-team.test.ts
│   │   │   │   ├── create-team.ts
│   │   │   │   ├── find-dsns.test.ts
│   │   │   │   ├── find-dsns.ts
│   │   │   │   ├── find-organizations.test.ts
│   │   │   │   ├── find-organizations.ts
│   │   │   │   ├── find-projects.test.ts
│   │   │   │   ├── find-projects.ts
│   │   │   │   ├── find-releases.test.ts
│   │   │   │   ├── find-releases.ts
│   │   │   │   ├── find-teams.test.ts
│   │   │   │   ├── find-teams.ts
│   │   │   │   ├── get-doc.test.ts
│   │   │   │   ├── get-doc.ts
│   │   │   │   ├── get-event-attachment.test.ts
│   │   │   │   ├── get-event-attachment.ts
│   │   │   │   ├── get-issue-details.test.ts
│   │   │   │   ├── get-issue-details.ts
│   │   │   │   ├── get-trace-details.test.ts
│   │   │   │   ├── get-trace-details.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── search-docs.test.ts
│   │   │   │   ├── search-docs.ts
│   │   │   │   ├── search-events
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── formatters.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── utils.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── search-events.test.ts
│   │   │   │   ├── search-issues
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── formatters.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── README.md
│   │   │   │   ├── tools.test.ts
│   │   │   │   ├── types.ts
│   │   │   │   ├── update-issue.test.ts
│   │   │   │   ├── update-issue.ts
│   │   │   │   ├── update-project.test.ts
│   │   │   │   ├── update-project.ts
│   │   │   │   ├── 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
│   │   │   ├── 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
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── cli
│   │   │   │   ├── parse.test.ts
│   │   │   │   ├── parse.ts
│   │   │   │   ├── resolve.test.ts
│   │   │   │   ├── resolve.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── usage.ts
│   │   │   ├── index.ts
│   │   │   └── transports
│   │   │       └── stdio.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.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
│   │   │   │   ├── csp-event.json
│   │   │   │   ├── csp-issue.json
│   │   │   │   ├── default-event.json
│   │   │   │   ├── event-attachments.json
│   │   │   │   ├── event.json
│   │   │   │   ├── generic-event.json
│   │   │   │   ├── issue.json
│   │   │   │   ├── performance-event.json
│   │   │   │   ├── performance-issue.json
│   │   │   │   ├── project.json
│   │   │   │   ├── regressed-issue.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
│   │   │   │   ├── unknown-event.json
│   │   │   │   └── unsupported-issue.json
│   │   │   ├── fixtures.ts
│   │   │   ├── 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-core/src/tools/get-event-attachment.test.ts:
--------------------------------------------------------------------------------

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

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

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

--------------------------------------------------------------------------------
/docs/releases/stdio.md:
--------------------------------------------------------------------------------

```markdown
  1 | # stdio Release
  2 | 
  3 | npm package release process for the MCP server stdio transport.
  4 | 
  5 | ## Overview
  6 | 
  7 | The MCP server is published to npm as `@sentry/mcp-server` for use with:
  8 | - Claude Desktop
  9 | - Cursor IDE
 10 | - VS Code with MCP extension
 11 | - Other MCP clients supporting stdio transport
 12 | 
 13 | ## Package Structure
 14 | 
 15 | Published package includes:
 16 | - Compiled TypeScript (`dist/`)
 17 | - stdio transport implementation
 18 | - Type definitions
 19 | - Tool definitions
 20 | 
 21 | ## Release Process
 22 | 
 23 | ### 1. Version Bump
 24 | 
 25 | Update version in `packages/mcp-server/package.json`:
 26 | 
 27 | ```json
 28 | {
 29 |   "name": "@sentry/mcp-server",
 30 |   "version": "1.2.3"
 31 | }
 32 | ```
 33 | 
 34 | Follow semantic versioning:
 35 | - **Major**: Breaking changes to tool interfaces
 36 | - **Minor**: New tools or non-breaking features
 37 | - **Patch**: Bug fixes
 38 | 
 39 | ### 2. Update Changelog
 40 | 
 41 | Document changes in `CHANGELOG.md`:
 42 | 
 43 | ```markdown
 44 | ## [1.2.3] - 2025-01-16
 45 | 
 46 | ### Added
 47 | - New `search_docs` tool for documentation search
 48 | 
 49 | ### Fixed
 50 | - Fix context propagation in tool handlers
 51 | ```
 52 | 
 53 | ### 3. Quality Checks
 54 | 
 55 | **MANDATORY before publishing:**
 56 | 
 57 | ```bash
 58 | pnpm -w run lint:fix    # Fix linting issues
 59 | pnpm tsc --noEmit       # TypeScript type checking
 60 | pnpm test               # Run all tests
 61 | pnpm run build          # Ensure clean build
 62 | ```
 63 | 
 64 | All checks must pass.
 65 | 
 66 | ### 4. Publish to npm
 67 | 
 68 | ```bash
 69 | cd packages/mcp-server
 70 | 
 71 | # Dry run to verify package contents
 72 | npm publish --dry-run
 73 | 
 74 | # Publish to npm
 75 | npm publish
 76 | ```
 77 | 
 78 | ### 5. Tag Release
 79 | 
 80 | ```bash
 81 | git tag v1.2.3
 82 | git push origin v1.2.3
 83 | ```
 84 | 
 85 | ## User Installation
 86 | 
 87 | Users install via npx in their MCP client configuration:
 88 | 
 89 | ### Claude Desktop
 90 | 
 91 | ```json
 92 | {
 93 |   "mcpServers": {
 94 |     "sentry": {
 95 |       "command": "npx",
 96 |       "args": ["-y", "@sentry/mcp-server"],
 97 |       "env": {
 98 |         "SENTRY_ACCESS_TOKEN": "sntrys_...",
 99 |         "SENTRY_HOST": "sentry.io"
100 |       }
101 |     }
102 |   }
103 | }
104 | ```
105 | 
106 | Config location:
107 | - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
108 | - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
109 | 
110 | ### Cursor IDE
111 | 
112 | Add to `.cursor/mcp.json`:
113 | 
114 | ```json
115 | {
116 |   "mcpServers": {
117 |     "sentry": {
118 |       "command": "npx",
119 |       "args": ["-y", "@sentry/mcp-server"],
120 |       "env": {
121 |         "SENTRY_ACCESS_TOKEN": "sntrys_...",
122 |         "SENTRY_HOST": "sentry.io"
123 |       }
124 |     }
125 |   }
126 | }
127 | ```
128 | 
129 | ## Environment Variables
130 | 
131 | Required:
132 | - `SENTRY_ACCESS_TOKEN` - Sentry API access token
133 | - `SENTRY_HOST` - Sentry instance hostname (default: `sentry.io`)
134 | 
135 | Optional:
136 | - `SENTRY_ORG` - Default organization slug
137 | - `SENTRY_PROJECT` - Default project slug
138 | 
139 | ## Version Pinning
140 | 
141 | Users can pin to specific versions:
142 | 
143 | ```json
144 | {
145 |   "args": ["-y", "@sentry/[email protected]"]
146 | }
147 | ```
148 | 
149 | ## Testing Releases
150 | 
151 | ### Local Testing Before Publishing
152 | 
153 | Test the built package locally:
154 | 
155 | ```bash
156 | cd packages/mcp-server
157 | npm pack
158 | # Creates sentry-mcp-server-1.2.3.tgz
159 | 
160 | # Test installation
161 | npm install -g ./sentry-mcp-server-1.2.3.tgz
162 | 
163 | # Run stdio server
164 | SENTRY_ACCESS_TOKEN=... @sentry/mcp-server
165 | ```
166 | 
167 | ### Beta Releases
168 | 
169 | For testing with users before stable release:
170 | 
171 | ```bash
172 | npm publish --tag beta
173 | ```
174 | 
175 | Users install with:
176 | ```json
177 | {
178 |   "args": ["-y", "@sentry/mcp-server@beta"]
179 | }
180 | ```
181 | 
182 | ## Troubleshooting
183 | 
184 | ### Package Not Found
185 | - Verify package name: `@sentry/mcp-server` (with scope)
186 | - Check npm registry: `npm view @sentry/mcp-server`
187 | 
188 | ### Version Mismatch
189 | - Users may have cached version: `npx clear-npx-cache`
190 | - Recommend version pinning for stability
191 | 
192 | ### Build Failures
193 | - Ensure `pnpm run build` succeeds before publishing
194 | - Check TypeScript compilation errors
195 | - Verify all dependencies are listed in package.json
196 | 
197 | ## References
198 | 
199 | - Package config: `packages/mcp-server/package.json`
200 | - stdio transport: `packages/mcp-server/src/transports/stdio.ts`
201 | - Build script: `packages/mcp-server/scripts/build.ts`
202 | - npm publishing docs: https://docs.npmjs.com/cli/publish
203 | 
```

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

```typescript
  1 | import * as Sentry from "@sentry/cloudflare";
  2 | import OAuthProvider from "@cloudflare/workers-oauth-provider";
  3 | import app from "./app";
  4 | import { SCOPES } from "../constants";
  5 | import type { Env } from "./types";
  6 | import getSentryConfig from "./sentry.config";
  7 | import { tokenExchangeCallback } from "./oauth";
  8 | import sentryMcpHandler from "./lib/mcp-handler";
  9 | import { checkRateLimit } from "./utils/rate-limiter";
 10 | import { getClientIp } from "./utils/client-ip";
 11 | 
 12 | // Public metadata endpoints that should be accessible from any origin
 13 | const PUBLIC_METADATA_PATHS = [
 14 |   "/.well-known/", // OAuth discovery endpoints
 15 |   "/robots.txt", // Search engine directives
 16 |   "/llms.txt", // LLM/AI agent directives
 17 | ];
 18 | 
 19 | const isPublicMetadataEndpoint = (pathname: string): boolean => {
 20 |   return PUBLIC_METADATA_PATHS.some((path) =>
 21 |     path.endsWith("/") ? pathname.startsWith(path) : pathname === path,
 22 |   );
 23 | };
 24 | 
 25 | const addCorsHeaders = (response: Response): Response => {
 26 |   const newResponse = new Response(response.body, response);
 27 |   newResponse.headers.set("Access-Control-Allow-Origin", "*");
 28 |   newResponse.headers.set("Access-Control-Allow-Methods", "GET, OPTIONS");
 29 |   newResponse.headers.set("Access-Control-Allow-Headers", "Content-Type");
 30 |   return newResponse;
 31 | };
 32 | 
 33 | // Wrap OAuth Provider to restrict CORS headers on public metadata endpoints
 34 | // OAuth Provider v0.0.12 adds overly permissive CORS (allows all methods/headers).
 35 | // We override with secure headers for .well-known endpoints and add CORS to robots.txt/llms.txt.
 36 | const wrappedOAuthProvider = {
 37 |   fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {
 38 |     const url = new URL(request.url);
 39 | 
 40 |     // Handle CORS preflight for public metadata endpoints
 41 |     if (request.method === "OPTIONS") {
 42 |       if (isPublicMetadataEndpoint(url.pathname)) {
 43 |         return addCorsHeaders(new Response(null, { status: 204 }));
 44 |       }
 45 |     }
 46 | 
 47 |     // Apply rate limiting to MCP and OAuth routes
 48 |     // This protects against abuse at the earliest possible point
 49 |     if (url.pathname.startsWith("/mcp") || url.pathname.startsWith("/oauth")) {
 50 |       const clientIP = getClientIp(request);
 51 | 
 52 |       // In local development or when IP can't be extracted, skip rate limiting
 53 |       // Rate limiter is optional and primarily for production abuse prevention
 54 |       if (clientIP) {
 55 |         const rateLimitResult = await checkRateLimit(
 56 |           clientIP,
 57 |           env.MCP_RATE_LIMITER,
 58 |           {
 59 |             keyPrefix: "mcp",
 60 |             errorMessage:
 61 |               "Rate limit exceeded. Please wait before trying again.",
 62 |           },
 63 |         );
 64 | 
 65 |         if (!rateLimitResult.allowed) {
 66 |           return new Response(rateLimitResult.errorMessage, { status: 429 });
 67 |         }
 68 |       }
 69 |       // If no clientIP, allow the request (likely local dev)
 70 |     }
 71 | 
 72 |     const oAuthProvider = new OAuthProvider({
 73 |       apiRoute: "/mcp",
 74 |       // @ts-expect-error - OAuthProvider types don't support specific Env types
 75 |       apiHandler: sentryMcpHandler,
 76 |       // @ts-expect-error - OAuthProvider types don't support specific Env types
 77 |       defaultHandler: app,
 78 |       // must match the routes registered in `app.ts`
 79 |       authorizeEndpoint: "/oauth/authorize",
 80 |       tokenEndpoint: "/oauth/token",
 81 |       clientRegistrationEndpoint: "/oauth/register",
 82 |       tokenExchangeCallback: (options) => tokenExchangeCallback(options, env),
 83 |       scopesSupported: Object.keys(SCOPES),
 84 |     });
 85 | 
 86 |     const response = await oAuthProvider.fetch(request, env, ctx);
 87 | 
 88 |     // Add CORS headers to public metadata endpoints
 89 |     if (isPublicMetadataEndpoint(url.pathname)) {
 90 |       return addCorsHeaders(response);
 91 |     }
 92 | 
 93 |     return response;
 94 |   },
 95 | };
 96 | 
 97 | export default Sentry.withSentry(
 98 |   getSentryConfig,
 99 |   wrappedOAuthProvider,
100 | ) satisfies ExportedHandler<Env>;
101 | 
```

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

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

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

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

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

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

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

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

--------------------------------------------------------------------------------
/docs/logging.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Logging Reference
  2 | 
  3 | How logging works in the Sentry MCP server using LogTape and Sentry. For tracing, spans, or metrics see @docs/monitoring.md.
  4 | 
  5 | ## Overview
  6 | 
  7 | We use [LogTape](https://logtape.org/) for structured logging with two sinks:
  8 | - **Console sink**: JSON Lines format for log aggregation
  9 | - **Sentry sink**: Sends logs to Sentry's Logs product using `Sentry.logger.*` API
 10 | 
 11 | Log levels are controlled by:
 12 | 1. `LOG_LEVEL` environment variable (e.g., `"debug"`, `"info"`, `"warning"`, `"error"`)
 13 | 2. Falls back to `NODE_ENV`: `"debug"` in development, `"info"` in production
 14 | 
 15 | **Important**: We use a custom LogTape sink that calls `Sentry.logger.*` methods to send logs to Sentry's Logs product. The `@logtape/sentry` package uses `captureException/captureMessage` which creates Issues instead of Logs.
 16 | 
 17 | Implementation: @packages/mcp-server/src/telem/logging.ts
 18 | 
 19 | ## Using the Log Helpers
 20 | 
 21 | ### logInfo() - Routine telemetry
 22 | ```typescript
 23 | import { logInfo } from "@sentry/mcp-server/telem/logging";
 24 | 
 25 | logInfo("MCP server started", {
 26 |   loggerScope: ["server", "lifecycle"],
 27 |   extra: { port: 3000 }
 28 | });
 29 | ```
 30 | 
 31 | ### logWarn() - Operational warnings
 32 | ```typescript
 33 | import { logWarn } from "@sentry/mcp-server/telem/logging";
 34 | 
 35 | logWarn("Rate limit approaching", {
 36 |   loggerScope: ["api", "rate-limit"],
 37 |   extra: { remaining: 10, limit: 100 }
 38 | });
 39 | ```
 40 | 
 41 | ### logError() - Operational errors (no Sentry issue)
 42 | ```typescript
 43 | import { logError } from "@sentry/mcp-server/telem/logging";
 44 | 
 45 | logError(error, {
 46 |   loggerScope: ["tools", "fetch-trace"],
 47 |   extra: { traceId: "abc123" }
 48 | });
 49 | ```
 50 | 
 51 | ### logIssue() - System errors (creates Sentry issue)
 52 | ```typescript
 53 | import { logIssue } from "@sentry/mcp-server/telem/logging";
 54 | 
 55 | const eventId = logIssue(error, {
 56 |   loggerScope: ["oauth", "token-refresh"],
 57 |   contexts: {
 58 |     oauth: { client_id: "..." }
 59 |   }
 60 | });
 61 | ```
 62 | 
 63 | ## When to Use Each Helper
 64 | 
 65 | **Use `logIssue()` for:**
 66 | - System errors (5xx, network failures, unexpected exceptions)
 67 | - Critical failures that need investigation
 68 | - Creates a Sentry Issue + emits a log entry with Event ID
 69 | 
 70 | **Use `logError()` for:**
 71 | - Expected operational errors (trace fetch failed, encoding error)
 72 | - Errors that are handled gracefully
 73 | - Sends to Sentry Logs only (no Issue created)
 74 | 
 75 | **Use `logWarn()` for:**
 76 | - Rate limit approaching
 77 | - Deprecated API usage
 78 | - Configuration warnings
 79 | 
 80 | **Use `logInfo()` for:**
 81 | - Request lifecycle (connection established, tool invoked)
 82 | - Configuration output
 83 | - Routine telemetry
 84 | 
 85 | **Skip logging:**
 86 | - `UserInputError` - Expected validation failures
 87 | - 4xx API responses - Client errors (except 429 rate limits)
 88 | - See @docs/error-handling.md for complete rules
 89 | 
 90 | ## Log Options
 91 | 
 92 | All helpers accept:
 93 | 
 94 | ```typescript
 95 | interface LogOptions {
 96 |   loggerScope?: string | readonly string[];  // e.g., ["cloudflare", "oauth"]
 97 |   extra?: Record<string, unknown>;           // Additional context
 98 |   contexts?: Record<string, Record<string, unknown>>;  // Sentry contexts
 99 | }
100 | ```
101 | 
102 | `logIssue()` also accepts `attachments` for files to attach to the Sentry event.
103 | 
104 | ## Configuration
105 | 
106 | **Environment Variables:**
107 | - `LOG_LEVEL` - Override log level (`"debug"`, `"info"`, `"warning"`, `"error"`)
108 | - `NODE_ENV` - Determines default level (`"development"` = debug, else info)
109 | 
110 | **Sinks:**
111 | - Console: JSON Lines format for log aggregation
112 | - Sentry: Sends logs to Sentry for monitoring (uses `Sentry.init` config)
113 | 
114 | ## HTTP Request Logging
115 | 
116 | Cloudflare middleware logs all HTTP requests automatically:
117 | 
118 | ```typescript
119 | // Middleware in packages/mcp-cloudflare/src/server/logging.ts
120 | app.use(createRequestLogger(["cloudflare", "http"]));
121 | 
122 | // Logs: {"level":"INFO","message":"GET /api/search","properties":{"status":200,"duration_ms":42}}
123 | ```
124 | 
125 | ## References
126 | 
127 | - Implementation: @packages/mcp-server/src/telem/logging.ts
128 | - Error handling patterns: @docs/error-handling.md
129 | - Monitoring and tracing: @docs/monitoring.md
130 | - LogTape docs: https://logtape.org/
131 | 
```

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

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

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

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

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

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

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/usecases/fix-bugs.tsx:
--------------------------------------------------------------------------------

```typescript
 1 | import { BugOff } from "lucide-react";
 2 | 
 3 | export default function FixBugs() {
 4 |   return (
 5 |     <div className="p-4 sm:p-8 lg:max-xl:border-r border-b border-dashed border-white/10 overflow-hidden justify-end flex flex-col group relative">
 6 |       <div className="absolute inset-0 pointer-events-none bg-grid [--size:1rem] [mask-image:linear-gradient(to_bottom,red,transparent,red)] group-hover:opacity-50 opacity-30 duration-300 -z-20" />
 7 |       <div className="relative min-h-64 [mask-image:linear-gradient(to_bottom,rgba(0,0,0,1)_60%,rgba(0,0,0,0)_100%)] mb-auto">
 8 |         {/* window 1 (browser) */}
 9 |         <div className="border rounded-2xl border-white/10 overflow-hidden bg-background-2 absolute top-0 left-0 bottom-0 right-8 translate-y-8 group-hover:translate-y-0 duration-200 ease-[cubic-bezier(0.175,0.885,0.32,1.275)]">
10 |           <div className="flex items-center pl-3.5 py-0.5 gap-1.5">
11 |             <div className="size-2 flex-shrink-0 rounded-full border border-white/20 bg-pink-300/50" />
12 |             <div className="size-2 flex-shrink-0 rounded-full border border-white/20 bg-amber-300/50" />
13 |             <div className="mr-2 size-2 flex-shrink-0 rounded-full border border-white/20 bg-emerald-300/50" />
14 |             <div className="rounded-l-lg -mr-1.5 border border-white/10 bg-white/5 pl-2 py-1 text-xs truncate">
15 |               <span className="relative block overflow-hidden truncate">
16 |                 <div className="absolute inset-0 size-full bg-select -translate-x-full group-hover:translate-x-0 group-hover:duration-500 ease-[steps(20)]" />
17 |                 <span className="z-10 relative">
18 |                   https://sentry.sentry.io/issues/6811213890/?environment=cloudflare&project=4509062593708032&query=is%3Aunresolved&referrer=issue-stream&seerDrawer=true
19 |                 </span>
20 |               </span>
21 |             </div>
22 |           </div>
23 |         </div>
24 |         {/* window 2 (terminal) */}
25 |         <div className="border rounded-2xl border-white/10 bg-background absolute top-8 left-8 bottom-0 right-0 p-4 text-xs font-mono translate-y-8 group-hover:translate-y-0 duration-200 group-hover:delay-1000 ease-[cubic-bezier(0.175,0.885,0.32,1.275)]">
26 |           &gt;&nbsp;fix&nbsp;
27 |           <span className="group-hover:text-white inline text-transparent group-hover:delay-1000">
28 |             https://sentry.sentry.io/issues/6811213890/?environment=cloudflare&project=4509062593708032&query=is%3Aunresolved&referrer=issue-stream&seerDrawer=true
29 |           </span>
30 |           <br />
31 |           <span className="group-hover:delay-1200 duration-300 translate-y-8 group-hover:translate-y-0 opacity-0 group-hover:opacity-100 ease-[cubic-bezier(0.175,0.885,0.32,1.275)]">
32 |             <span className="[animation:spinStar_2s_steps(12)_infinite] group-hover:[animation-play-state:running] [animation-play-state:paused] inline-block origin-center text-orange-400">
33 |               *
34 |             </span>
35 |             &nbsp;
36 |             {"fixing...".split("").map((char, i) => (
37 |               <span
38 |                 // biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
39 |                 key={i}
40 |                 style={{ "--delay": `${i * 0.1}s` } as React.CSSProperties}
41 |                 className="text-orange-500 [animation:wave_1s_steps(2)_infinite] [animation-delay:var(--delay)] group-hover:[animation-play-state:running] [animation-play-state:paused]"
42 |               >
43 |                 {char}
44 |               </span>
45 |             ))}
46 |           </span>
47 |         </div>
48 |       </div>
49 |       <div className="flex">
50 |         <div className="flex flex-col">
51 |           <h3 className="md:text-xl font-bold">Fix Bugs</h3>
52 |           <p className="text-balance text-white/70">
53 |             Ship fast, break things—then unbreak them. Sentry MCP makes
54 |             debugging less <span className="text-[#fd918f]">“wtf”</span> and
55 |             more <span className="text-lime-300">“ah, got it.”</span>
56 |           </p>
57 |         </div>
58 |         <BugOff className="size-16 ml-auto text-white/20 group-hover:text-white/40 stroke-[0.5px] duration-300 mt-auto" />
59 |       </div>
60 |     </div>
61 |   );
62 | }
63 | 
```

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

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

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

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

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/home-layout/sidebars.tsx:
--------------------------------------------------------------------------------

```typescript
 1 | interface SidebarProps {
 2 |   toggleChat: (open: boolean) => void;
 3 |   isChatOpen: boolean;
 4 | }
 5 | 
 6 | export function Sidebars({ isChatOpen, toggleChat }: SidebarProps) {
 7 |   return (
 8 |     <>
 9 |       {/* left sidebar */}
10 |       <aside className="group hidden sm:block fixed left-0 inset-y-0 h-full sm:w-[calc((100vw-40rem)/2)] md:w-[calc((100vw-48rem)/2)] lg:w-[calc((100vw-48rem)/2)] xl:w-[calc((100vw-72rem)/2)] 2xl:w-[calc((100vw-90rem)/2)] bg-fixed bg-[repeating-linear-gradient(-45deg,#fff2,#fff2_1px,#fff0_1.5px,#fff0_12px)] z-10 border-r opacity-50 bg-clip-padding border-white/20" />
11 |       {/* right sidebar */}
12 |       <button
13 |         className={`group hidden sm:grid fixed right-0 inset-y-0 h-full w-[50vw] duration-300 cursor-pointer place-items-center z-40 border-l sm:[--x:20rem] md:[--x:24rem] lg:[--x:24rem] xl:[--x:36rem] 2xl:[--x:45rem] transition-colors ${
14 |           isChatOpen
15 |             ? "bg-background-2 -translate-x-[1px] opacity-100 border-white/10 motion-safe:transition-all"
16 |             : "translate-x-[var(--x)] opacity-50 hover:bg-background-2 bg-clip-padding border-white/20 bg-[repeating-linear-gradient(-45deg,#fff2,#fff2_1px,#fff0_1.5px,#fff0_12px)]"
17 |         }`}
18 |         onClick={() => toggleChat(true)}
19 |         onKeyDown={(e) => {
20 |           if (e.key === "Enter" || e.key === " ") {
21 |             toggleChat(true);
22 |           }
23 |         }}
24 |         // the header button to open chat will remain tabbable
25 |         tabIndex={-1}
26 |         type="button"
27 |       >
28 |         <span className="sr-only">Open chat panel</span>
29 |         {!isChatOpen && (
30 |           <div className="font-mono absolute w-8 xl:w-12 min-[1800px]:w-fit min-[1800px]:flex-nowrap text-center flex flex-wrap justify-center md:left-[calc((100vw-48rem)/4)] lg:left-[calc((100vw-48rem)/4)] xl:left-[calc((100vw-72rem)/4)] 2xl:left-[calc((100vw-90rem)/4)] top-1/2 -translate-1/2 opacity-0 group-hover:opacity-100 px-1 gap-0.25 gap-x-2">
31 |             <div className="flex flex-nowrap">
32 |               {"open".split("").map((char, i) => (
33 |                 <span
34 |                   // biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
35 |                   key={`open-${i}`}
36 |                   className="animate-openchat"
37 |                   // per-index delay so animation cascades
38 |                   style={{ ["--delay" as any]: `${i * 80}ms` }}
39 |                 >
40 |                   <span className="relative" aria-hidden>
41 |                     <span className="original inline-block leading-[1]">
42 |                       {char === " " ? "\u00A0" : char}
43 |                     </span>
44 |                     <span
45 |                       className="underscore absolute inset-0 leading-[1]"
46 |                       aria-hidden="true"
47 |                       role="presentation"
48 |                     >
49 |                       _
50 |                     </span>
51 |                   </span>
52 |                 </span>
53 |               ))}
54 |             </div>
55 | 
56 |             <div className="flex flex-nowrap">
57 |               {"chat".split("").map((char, i) => {
58 |                 // offset so "chat" follows "open" + 1ch space
59 |                 const offsetChars = "open".length + 1;
60 |                 return (
61 |                   <span
62 |                     // biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
63 |                     key={`chat-${i}`}
64 |                     className="animate-openchat"
65 |                     style={{
66 |                       ["--delay" as any]: `${(offsetChars + i) * 80}ms`,
67 |                     }}
68 |                   >
69 |                     <span className="relative" aria-hidden>
70 |                       <span className="original inline-block leading-[1]">
71 |                         {char === " " ? "\u00A0" : char}
72 |                       </span>
73 |                       <span
74 |                         className="underscore absolute inset-0 leading-[1]"
75 |                         aria-hidden="true"
76 |                         role="presentation"
77 |                       >
78 |                         _
79 |                       </span>
80 |                     </span>
81 |                   </span>
82 |                 );
83 |               })}
84 |             </div>
85 |           </div>
86 |         )}
87 |       </button>
88 |     </>
89 |   );
90 | }
91 | 
```

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

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

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

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

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/hero/hero-block.tsx:
--------------------------------------------------------------------------------

```typescript
 1 | import TerminalAnimation from "../animation/TerminalAnimation";
 2 | import CodeSnippet from "../ui/code-snippet";
 3 | import { openCursorDeepLink } from "@/client/utils";
 4 | 
 5 | export default function HeroBlock() {
 6 |   const endpoint = new URL("/mcp", window.location.href).href;
 7 | 
 8 |   return (
 9 |     <div className="flex-1 flex flex-col container mx-auto min-h-[min(80rem,calc(100svh-69px))]">
10 |       <div className="grid xl:grid-cols-2 gap-4 sm:gap-8 sm:px-8 sm:py-6 px-4 pt-3 max-sm:text-sm">
11 |         <p className="text-white/70 max-w-[69ch]">
12 |           Sentry MCP plugs Sentry's API directly into your LLM, letting you ask
13 |           questions about your data in natural language. Take a coding agent you
14 |           already use - like Cursor or Claude Code - and pull in information
15 |           from Sentry to help with debugging, fixing production errors, and
16 |           understanding your application's behavior.
17 |         </p>
18 |         <div className="flex h-full items-center xl:justify-end sm:gap-6 gap-2 flex-wrap">
19 |           <div className="w-full sm:contents">
20 |             <CodeSnippet noMargin snippet={endpoint} />
21 |           </div>
22 |           <button
23 |             type="button"
24 |             onClick={() => openCursorDeepLink(endpoint)}
25 |             className="relative hidden md:block size-fit my-2 group cursor-pointer outline-none rounded-xl ring-offset-2 ring-offset-background focus-visible:ring-violet-300 focus-visible:ring-[3px]"
26 |           >
27 |             <div className="absolute inset-0 size-full rounded-xl bg-violet-400/80 bg-[repeating-linear-gradient(-45deg,var(--bg1),var(--bg1)_0.5px,#fff0_0.5px,#fff0_12px)]" />
28 |             <div className="bg-grid absolute inset-0 size-full duration-200 delay-50 opacity-100 [--size:10px] [--grid-color:#0002] bg-pink-400 group-hover:rotate-x-15 group-hover:translate-1 group-hover:-rotate-y-2 !px-6 transform-3d perspective-distant rounded-xl ease-[cubic-bezier(0.175,0.885,0.32,1.275)] origin-bottom-right group-active:rotate-y-1 group-active:translate-0.5 group-active:rotate-x-3" />
29 |             <div className="inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer shadow-xs hover:text-black py-2 has-[>svg]:px-3 h-13 group-hover:rotate-x-30 group-hover:translate-2 group-hover:-rotate-y-4 group-active:rotate-x-6 group-active:translate-1 group-active:rotate-y-2 !px-6 relative rounded-xl bg-white text-black duration-200 hover:bg-white transform-3d perspective-distant backface-hidden ease-[cubic-bezier(0.175,0.885,0.32,1.275)] origin-bottom-right">
30 |               <div className="bg-grid absolute inset-0 opacity-0 duration-300 group-hover:opacity-30 [--size:10px] [--grid-color:#44130644] [mask-image:radial-gradient(ellipse_at_center,transparent,red)]" />
31 |               <svg
32 |                 xmlns="http://www.w3.org/2000/svg"
33 |                 version="1.1"
34 |                 className="size-4"
35 |                 viewBox="0 0 466.73 532.09"
36 |                 aria-hidden="true"
37 |               >
38 |                 <path
39 |                   className="fill-current"
40 |                   d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75,3.32,9.3,9.46,9.3,16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"
41 |                 />
42 |               </svg>
43 |               Install in Cursor
44 |             </div>
45 |           </button>
46 |         </div>
47 |       </div>
48 |       {/* demo */}
49 |       <div
50 |         className="overflow-wrap p-4 sm:p-8 overflow-visible relative grid h-full flex-1 w-full gap-8 rounded-2xl xl:grid-cols-4 bg-gradient-to-r from-400/50 to-500 text-white/70 grid-cols-1 grid-rows-6 xl:grid-rows-1"
51 |         id="demo"
52 |       >
53 |         <TerminalAnimation />
54 |       </div>
55 |     </div>
56 |   );
57 | }
58 | 
```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/home-layout/footer.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { SentryIcon } from "../ui/icons/sentry";
  2 | 
  3 | export default function Footer({ isChatOpen }: { isChatOpen: boolean }) {
  4 |   return (
  5 |     <>
  6 |       <div
  7 |         className={`group inset-x-0 bottom-14 bg-background-2 w-full sm:h-40 max-sm:gap-4 bg-fixed z-10 border-t flex-col bg-clip-padding border-white/20 flex font-mono py-6 justify-around motion-safe:duration-300 [--x:0] xl:[--x:20rem] 2xl:[--x:24rem] ${
  8 |           isChatOpen && "xl:-translate-x-[var(--x)]"
  9 |         }`}
 10 |       >
 11 |         <div className="flex items-center gap-2 flex-shrink-0 sm:mx-auto max-sm:mx-4 font-sans">
 12 |           <SentryIcon className="h-8 w-8" />
 13 |           <div className="flex items-baseline gap-2">
 14 |             <h1 className="text-2xl font-medium whitespace-nowrap">
 15 |               Sentry MCP
 16 |             </h1>
 17 |           </div>
 18 |         </div>
 19 |         <div className="flex gap-6 max-sm:px-4 sm:justify-center">
 20 |           <a
 21 |             href="https://github.com/getsentry/sentry-mcp"
 22 |             target="_blank"
 23 |             rel="noopener noreferrer"
 24 |             className="relative hover:underline opacity-80 hover:opacity-100 group/link"
 25 |           >
 26 |             <div className="absolute inset-0 size-full text-violet-500 group-hover/link:opacity-50 group-hover/link:delay-0 delay-300">
 27 |               Github
 28 |             </div>
 29 |             <div
 30 |               className="absolute inset-0 size-full group-hover/link:opacity-50 group-hover/link:delay-0 duration-300 motion-safe:group-hover/link:translate-y-1 group-hover/link:-translate-x-1"
 31 |               aria-hidden="true"
 32 |             >
 33 |               Github
 34 |             </div>
 35 |             <div
 36 |               className="perspective-distant transform-3d duration-300 motion-safe:group-hover/link:translate-y-2 group-hover/link:-translate-x-2"
 37 |               role="presentation"
 38 |             >
 39 |               Github
 40 |             </div>
 41 |           </a>
 42 |           <a
 43 |             href="https://docs.sentry.io/product/sentry-mcp/"
 44 |             target="_blank"
 45 |             rel="noopener noreferrer"
 46 |             className="relative hover:underline opacity-80 hover:opacity-100 group/link"
 47 |           >
 48 |             <div className="absolute inset-0 size-full text-violet-500 group-hover/link:opacity-50 group-hover/link:delay-0 delay-300">
 49 |               Sentry Docs
 50 |             </div>
 51 |             <div
 52 |               className="absolute inset-0 size-full group-hover/link:opacity-50 group-hover/link:delay-0 duration-300 motion-safe:group-hover/link:translate-y-1"
 53 |               aria-hidden="true"
 54 |             >
 55 |               Sentry Docs
 56 |             </div>
 57 |             <div
 58 |               className="perspective-distant transform-3d duration-300 motion-safe:group-hover/link:translate-y-2"
 59 |               role="presentation"
 60 |             >
 61 |               Sentry Docs
 62 |             </div>
 63 |           </a>
 64 |           <a
 65 |             href="https://discord.com/invite/sentry"
 66 |             target="_blank"
 67 |             rel="noopener noreferrer"
 68 |             className="relative hover:underline opacity-80 hover:opacity-100 group/link"
 69 |           >
 70 |             <div className="absolute inset-0 size-full text-violet-500 group-hover/link:opacity-50 group-hover/link:delay-0 delay-300">
 71 |               Discord
 72 |             </div>
 73 |             <div
 74 |               className="absolute inset-0 size-full group-hover/link:opacity-50 group-hover/link:delay-0 duration-300 motion-safe:group-hover/link:translate-1"
 75 |               aria-hidden="true"
 76 |             >
 77 |               Discord
 78 |             </div>
 79 |             <div
 80 |               className="perspective-distant transform-3d duration-300 motion-safe:group-hover/link:translate-2"
 81 |               role="presentation"
 82 |             >
 83 |               Discord
 84 |             </div>
 85 |           </a>
 86 |         </div>
 87 |       </div>
 88 |       <div
 89 |         className={`group inset-x-0 bottom-0 bg-background-2 w-full h-14 bg-fixed bg-[repeating-linear-gradient(45deg,#fff2,#fff2_4px,#fff0_4.5px,#fff0_12px)] z-10 border-t flex max-sm:px-4 sm:justify-center items-center bg-clip-padding border-white/20 opacity-75 [--x:0] xl:[--x:20rem] 2xl:[--x:24rem] motion-safe:duration-300 ${
 90 |           isChatOpen && "xl:-translate-x-[var(--x)]"
 91 |         }`}
 92 |       >
 93 |         <span className="opacity-50 text-xs sm:max-w-2/3">
 94 |           © {new Date().getFullYear()} Functional Software, Inc. (d.b.a.
 95 |           Sentry). All rights reserved.
 96 |         </span>
 97 |       </div>
 98 |     </>
 99 |   );
100 | }
101 | 
```

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

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

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

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

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

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

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

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

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

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

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

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

--------------------------------------------------------------------------------
/docs/api-patterns.md:
--------------------------------------------------------------------------------

```markdown
  1 | # API Patterns
  2 | 
  3 | Sentry API client usage, mocking, and testing patterns.
  4 | 
  5 | ## API Client Usage
  6 | 
  7 | ### Client Creation
  8 | 
  9 | ```typescript
 10 | // Standard usage with context helper
 11 | const apiService = apiServiceFromContext(context, {
 12 |   regionUrl: params.regionUrl
 13 | });
 14 | 
 15 | // Direct instantiation
 16 | const api = new SentryApiService({
 17 |   host: "sentry.io",
 18 |   accessToken: token
 19 | });
 20 | ```
 21 | 
 22 | See: `packages/mcp-server/src/api-utils.ts` and `adding-tools.md#step-2-implement-the-handler` for usage in tools.
 23 | 
 24 | ### Common Operations
 25 | 
 26 | ```typescript
 27 | // List with filtering
 28 | const issues = await api.issues.list({
 29 |   organizationSlug: "org",
 30 |   query: "is:unresolved",
 31 |   sort: "date"
 32 | });
 33 | 
 34 | // Get specific resource
 35 | const project = await api.projects.get({
 36 |   organizationSlug: "org",
 37 |   projectIdOrSlug: "frontend"
 38 | });
 39 | 
 40 | // Create/update
 41 | await api.issues.update({
 42 |   issueId: "123",
 43 |   status: "resolved"
 44 | });
 45 | ```
 46 | 
 47 | ### Multi-Region Support
 48 | 
 49 | Sentry uses region-specific URLs:
 50 | 
 51 | ```typescript
 52 | // Auto-detect from organization
 53 | const orgs = await api.organizations.list();
 54 | // Returns: { region_url: "https://us.sentry.io" }
 55 | 
 56 | // Use region URL
 57 | const api = apiServiceFromContext(context, {
 58 |   regionUrl: org.region_url
 59 | });
 60 | ```
 61 | 
 62 | ## Schema Patterns
 63 | 
 64 | ### Flexible Sentry Models
 65 | 
 66 | ```typescript
 67 | // Support ID variations
 68 | const IssueIdSchema = z.union([
 69 |   z.string(),  // "PROJ-123"
 70 |   z.number()   // 123456789
 71 | ]);
 72 | 
 73 | // Partial with passthrough for unknowns
 74 | const FlexibleSchema = BaseSchema
 75 |   .partial()
 76 |   .passthrough();
 77 | 
 78 | // Nullable handling
 79 | z.union([DateSchema, z.null()])
 80 | ```
 81 | 
 82 | See Zod patterns: `common-patterns.md#zod-schema-patterns`
 83 | 
 84 | ### Type Safety
 85 | 
 86 | For testing API patterns, see `testing.md#mock-server-setup`
 87 | 
 88 | ```typescript
 89 | // Derive types from schemas
 90 | export type Issue = z.infer<typeof IssueSchema>;
 91 | 
 92 | // Runtime validation
 93 | const issues = IssueListSchema.parse(response);
 94 | ```
 95 | 
 96 | ## Mock Patterns
 97 | 
 98 | ### MSW Handler Structure
 99 | 
100 | ```typescript
101 | export const handlers = [
102 |   {
103 |     method: "get",
104 |     path: "/api/0/organizations/:org/issues/",
105 |     fetch: async ({ request, params }) => {
106 |       // Validate parameters
107 |       if (!params.org) {
108 |         return HttpResponse.json("Invalid org", { status: 400 });
109 |       }
110 |       
111 |       // Return fixture
112 |       return HttpResponse.json(issueListFixture);
113 |     }
114 |   }
115 | ];
116 | ```
117 | 
118 | See: `packages/mcp-server-mocks/src/handlers/`
119 | 
120 | ### Request Validation
121 | 
122 | ```typescript
123 | fetch: async ({ request }) => {
124 |   const url = new URL(request.url);
125 |   const query = url.searchParams.get("query");
126 |   
127 |   // Validate query parameters
128 |   if (query && !isValidQuery(query)) {
129 |     return HttpResponse.json("Invalid query", { status: 400 });
130 |   }
131 |   
132 |   // Filter based on query
133 |   const filtered = fixtures.filter(item => 
134 |     matchesQuery(item, query)
135 |   );
136 |   
137 |   return HttpResponse.json(filtered);
138 | }
139 | ```
140 | 
141 | ### Dynamic Responses
142 | 
143 | ```typescript
144 | // Support pagination
145 | const limit = parseInt(url.searchParams.get("limit") || "100");
146 | const cursor = url.searchParams.get("cursor");
147 | 
148 | const start = cursor ? parseInt(cursor) : 0;
149 | const page = fixtures.slice(start, start + limit);
150 | 
151 | return HttpResponse.json(page, {
152 |   headers: {
153 |     "Link": `<...?cursor=${start + limit}>; rel="next"`
154 |   }
155 | });
156 | ```
157 | 
158 | ## Testing with Mocks
159 | 
160 | ### Setup Pattern
161 | 
162 | ```typescript
163 | import { setupMockServer } from "@sentry-mcp/mocks";
164 | 
165 | const server = setupMockServer();
166 | 
167 | beforeAll(() => server.listen());
168 | afterEach(() => server.resetHandlers());
169 | afterAll(() => server.close());
170 | ```
171 | 
172 | ### Override Handlers
173 | 
174 | ```typescript
175 | it("handles errors", async () => {
176 |   server.use(
177 |     http.get("*/issues/", () => 
178 |       HttpResponse.json({ error: "Server error" }, { status: 500 })
179 |     )
180 |   );
181 |   
182 |   await expect(api.issues.list(params))
183 |     .rejects.toThrow(ApiError);
184 | });
185 | ```
186 | 
187 | ## Error Patterns
188 | 
189 | ### ApiError Handling
190 | 
191 | ```typescript
192 | try {
193 |   const data = await api.issues.list(params);
194 | } catch (error) {
195 |   if (error instanceof ApiError) {
196 |     // Handle specific status codes
197 |     if (error.status === 404) {
198 |       throw new UserInputError("Organization not found");
199 |     }
200 |   }
201 |   throw error;
202 | }
203 | ```
204 | 
205 | See error patterns: `common-patterns.md#error-handling`
206 | 
207 | ## Best Practices
208 | 
209 | 1. **Always use context helper** when in tools/prompts
210 | 2. **Handle region URLs** for multi-region support
211 | 3. **Validate schemas** at API boundaries
212 | 4. **Mock realistically** in tests
213 | 5. **Transform errors** for LLM consumption
214 | 
215 | ## References
216 | 
217 | - API Client: `packages/mcp-server/src/api-client/`
218 | - Mock handlers: `packages/mcp-server-mocks/src/handlers/`
219 | - Fixtures: `packages/mcp-server-mocks/src/fixtures/`
220 | - API Utils: `packages/mcp-server/src/api-utils.ts`
```

--------------------------------------------------------------------------------
/packages/mcp-core/src/schema.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Reusable Zod parameter schemas for MCP tools.
  3 |  *
  4 |  * Shared validation schemas used across tool definitions to ensure consistent
  5 |  * parameter handling and validation. Each schema includes transformation
  6 |  * (e.g., toLowerCase, trim) and LLM-friendly descriptions.
  7 |  */
  8 | import { z } from "zod";
  9 | import { SENTRY_GUIDES } from "./constants";
 10 | import { validateSlug } from "./utils/slug-validation";
 11 | 
 12 | export const ParamOrganizationSlug = z
 13 |   .string()
 14 |   .toLowerCase()
 15 |   .trim()
 16 |   .superRefine(validateSlug)
 17 |   .describe(
 18 |     "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool.",
 19 |   );
 20 | 
 21 | export const ParamTeamSlug = z
 22 |   .string()
 23 |   .toLowerCase()
 24 |   .trim()
 25 |   .superRefine(validateSlug)
 26 |   .describe(
 27 |     "The team's slug. You can find a list of existing teams in an organization using the `find_teams()` tool.",
 28 |   );
 29 | 
 30 | export const ParamProjectSlug = z
 31 |   .string()
 32 |   .toLowerCase()
 33 |   .trim()
 34 |   .superRefine(validateSlug)
 35 |   .describe(
 36 |     "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool.",
 37 |   );
 38 | 
 39 | export const ParamProjectSlugOrAll = z
 40 |   .string()
 41 |   .toLowerCase()
 42 |   .trim()
 43 |   .superRefine(validateSlug)
 44 |   .describe(
 45 |     "The project's slug. This will default to all projects you have access to. It is encouraged to specify this when possible.",
 46 |   );
 47 | 
 48 | export const ParamSearchQuery = z
 49 |   .string()
 50 |   .trim()
 51 |   .describe(
 52 |     "Search query to filter results by name or slug. Use this to narrow down results when there are many items.",
 53 |   );
 54 | 
 55 | export const ParamIssueShortId = z
 56 |   .string()
 57 |   .toUpperCase()
 58 |   .trim()
 59 |   .describe("The Issue ID. e.g. `PROJECT-1Z43`");
 60 | 
 61 | export const ParamIssueUrl = z
 62 |   .string()
 63 |   .url()
 64 |   .trim()
 65 |   .describe(
 66 |     "The URL of the issue. e.g. https://my-organization.sentry.io/issues/PROJECT-1Z43",
 67 |   );
 68 | 
 69 | export const ParamTraceId = z
 70 |   .string()
 71 |   .trim()
 72 |   .regex(
 73 |     /^[0-9a-fA-F]{32}$/,
 74 |     "Trace ID must be a 32-character hexadecimal string",
 75 |   )
 76 |   .describe("The trace ID. e.g. `a4d1aae7216b47ff8117cf4e09ce9d0a`");
 77 | 
 78 | export const ParamPlatform = z
 79 |   .string()
 80 |   .toLowerCase()
 81 |   .trim()
 82 |   .describe(
 83 |     "The platform for the project. e.g., python, javascript, react, etc.",
 84 |   );
 85 | 
 86 | export const ParamTransaction = z
 87 |   .string()
 88 |   .trim()
 89 |   .describe("The transaction name. Also known as the endpoint, or route name.");
 90 | 
 91 | export const ParamQuery = z
 92 |   .string()
 93 |   .trim()
 94 |   .describe(
 95 |     `The search query to apply. Use the \`help(subject="query_syntax")\` tool to get more information about the query syntax rather than guessing.`,
 96 |   );
 97 | 
 98 | /**
 99 |  * Region URL parameter for Sentry API requests.
100 |  *
101 |  * Handles region-specific URLs for Sentry's Cloud Service while gracefully
102 |  * supporting self-hosted Sentry installations that may return empty regionUrl values.
103 |  * This schema accepts both valid URLs and empty strings to ensure compatibility
104 |  * across different Sentry deployment types.
105 |  */
106 | export const ParamRegionUrl = z
107 |   .string()
108 |   .trim()
109 |   .refine((value) => !value || z.string().url().safeParse(value).success, {
110 |     message: "Must be a valid URL or empty string (for self-hosted Sentry)",
111 |   })
112 |   .describe(
113 |     "The region URL for the organization you're querying, if known. " +
114 |       "For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. " +
115 |       "For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. " +
116 |       "You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
117 |   );
118 | 
119 | export const ParamIssueStatus = z
120 |   .enum(["resolved", "resolvedInNextRelease", "unresolved", "ignored"])
121 |   .describe(
122 |     "The new status for the issue. Valid values are 'resolved', 'resolvedInNextRelease', 'unresolved', and 'ignored'.",
123 |   );
124 | 
125 | export const ParamAssignedTo = z
126 |   .string()
127 |   .trim()
128 |   .describe(
129 |     "The assignee in format 'user:ID' or 'team:ID' where ID is numeric. Example: 'user:123456' or 'team:789'. Use the whoami tool to find your user ID.",
130 |   );
131 | 
132 | export const ParamSentryGuide = z
133 |   .enum(SENTRY_GUIDES)
134 |   .describe(
135 |     "Optional guide filter to limit search results to specific documentation sections. " +
136 |       "Use either a platform (e.g., 'javascript', 'python') or platform/guide combination (e.g., 'javascript/nextjs', 'python/django').",
137 |   );
138 | 
139 | export const ParamEventId = z.string().trim().describe("The ID of the event.");
140 | 
141 | export const ParamAttachmentId = z
142 |   .string()
143 |   .trim()
144 |   .describe("The ID of the attachment to download.");
145 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server-mocks/src/fixtures/performance-event.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "id": "a1b2c3d4e5f6789012345678901234567",
  3 |   "groupID": "7890123456",
  4 |   "eventID": "a1b2c3d4e5f6789012345678901234567",
  5 |   "projectID": "4509062593708032",
  6 |   "size": 8432,
  7 |   "type": "transaction",
  8 |   "title": "GET /api/users",
  9 |   "message": "",
 10 |   "platform": "python",
 11 |   "datetime": "2025-08-06T18:00:00.000000Z",
 12 |   "dateCreated": "2025-08-06T18:00:00.000000Z",
 13 |   "contexts": {
 14 |     "trace": {
 15 |       "trace_id": "abcdef1234567890abcdef1234567890",
 16 |       "span_id": "1234567890abcdef",
 17 |       "op": "http.server",
 18 |       "status": "ok",
 19 |       "exclusive_time": 250.5,
 20 |       "data": {
 21 |         "http.request.method": "GET",
 22 |         "url.path": "/api/users"
 23 |       }
 24 |     }
 25 |   },
 26 |   "occurrence": {
 27 |     "id": "occ_123456789",
 28 |     "projectId": 4509062593708032,
 29 |     "eventId": "a1b2c3d4e5f6789012345678901234567",
 30 |     "fingerprint": [
 31 |       "n_plus_one_db_queries",
 32 |       "SELECT * FROM users WHERE id = %s"
 33 |     ],
 34 |     "issueTitle": "N+1 Query: SELECT * FROM users WHERE id = %s",
 35 |     "subtitle": "Database query repeated 25 times",
 36 |     "resourceId": null,
 37 |     "type": 1006,
 38 |     "detectionTime": 1722963600,
 39 |     "level": "warning",
 40 |     "culprit": "SELECT * FROM users WHERE id = %s",
 41 |     "priority": 50,
 42 |     "assignee": null,
 43 |     "evidenceData": {
 44 |       "transactionName": "/api/users",
 45 |       "parentSpan": "GET /api/users",
 46 |       "parentSpanIds": ["parent123"],
 47 |       "repeatingSpans": [
 48 |         "db - INSERT INTO \"sentry_fileblobindex\" (\"offset\", \"file_id\", \"blob_id\") VALUES (%s, %s, %s) RETURNING \"sentry_fileblobindex\".\"id\"",
 49 |         "function - sentry.models.files.abstractfileblob.AbstractFileBlob.from_file",
 50 |         "db - SELECT \"sentry_fileblob\".\"id\", \"sentry_fileblob\".\"path\", \"sentry_fileblob\".\"size\", \"sentry_fileblob\".\"checksum\", \"sentry_fileblob\".\"timestamp\" FROM \"sentry_fileblob\" WHERE \"sentry_fileblob\".\"checksum\" = %s LIMIT 21"
 51 |       ],
 52 |       "numberRepeatingSpans": "3",
 53 |       "numPatternRepetitions": 3,
 54 |       "offenderSpanIds": [
 55 |         "span001",
 56 |         "span002",
 57 |         "span003"
 58 |       ],
 59 |       "op": "db"
 60 |     },
 61 |     "evidenceDisplay": [
 62 |       {
 63 |         "name": "Offending Spans",
 64 |         "value": "SELECT * FROM users WHERE id = %s",
 65 |         "important": true
 66 |       },
 67 |       {
 68 |         "name": "Repeated",
 69 |         "value": "3 times",
 70 |         "important": true
 71 |       }
 72 |     ]
 73 |   },
 74 |   "entries": [
 75 |     {
 76 |       "type": "spans",
 77 |       "data": [
 78 |         {
 79 |           "span_id": "parent123",
 80 |           "trace_id": "abcdef1234567890abcdef1234567890",
 81 |           "parent_span_id": "1234567890abcdef",
 82 |           "op": "http.server",
 83 |           "description": "GET /api/users",
 84 |           "status": "ok",
 85 |           "start_timestamp": 1722963600.0,
 86 |           "timestamp": 1722963600.25,
 87 |           "data": {
 88 |             "http.request.method": "GET",
 89 |             "url.path": "/api/users"
 90 |           }
 91 |         },
 92 |         {
 93 |           "span_id": "span001",
 94 |           "trace_id": "abcdef1234567890abcdef1234567890",
 95 |           "parent_span_id": "parent123",
 96 |           "op": "db.query",
 97 |           "description": "SELECT * FROM users WHERE id = 1",
 98 |           "status": "ok",
 99 |           "start_timestamp": 1722963600.01,
100 |           "timestamp": 1722963600.015,
101 |           "data": {
102 |             "db.system": "postgresql",
103 |             "db.operation": "SELECT"
104 |           }
105 |         },
106 |         {
107 |           "span_id": "span002",
108 |           "trace_id": "abcdef1234567890abcdef1234567890",
109 |           "parent_span_id": "parent123",
110 |           "op": "db.query",
111 |           "description": "SELECT * FROM users WHERE id = 2",
112 |           "status": "ok",
113 |           "start_timestamp": 1722963600.02,
114 |           "timestamp": 1722963600.025,
115 |           "data": {
116 |             "db.system": "postgresql",
117 |             "db.operation": "SELECT"
118 |           }
119 |         },
120 |         {
121 |           "span_id": "span003",
122 |           "trace_id": "abcdef1234567890abcdef1234567890",
123 |           "parent_span_id": "parent123",
124 |           "op": "db.query",
125 |           "description": "SELECT * FROM users WHERE id = 3",
126 |           "status": "ok",
127 |           "start_timestamp": 1722963600.03,
128 |           "timestamp": 1722963600.035,
129 |           "data": {
130 |             "db.system": "postgresql",
131 |             "db.operation": "SELECT"
132 |           }
133 |         }
134 |       ]
135 |     },
136 |     {
137 |       "type": "request",
138 |       "data": {
139 |         "method": "GET",
140 |         "url": "https://api.example.com/api/users",
141 |         "query": [],
142 |         "headers": [["Accept", "application/json"], ["Host", "api.example.com"]]
143 |       }
144 |     }
145 |   ],
146 |   "tags": [
147 |     { "key": "environment", "value": "production" },
148 |     { "key": "transaction", "value": "/api/users" }
149 |   ]
150 | }
151 | 
```

--------------------------------------------------------------------------------
/packages/mcp-core/src/internal/issue-helpers.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Issue parameter parsing and validation utilities.
  3 |  *
  4 |  * Handles flexible input formats for Sentry issues (URLs vs explicit parameters),
  5 |  * extracts organization and issue identifiers, and validates issue ID formats.
  6 |  * Provides robust parsing for LLM-generated parameters that may contain formatting
  7 |  * inconsistencies.
  8 |  */
  9 | 
 10 | import { UserInputError } from "../errors";
 11 | 
 12 | /**
 13 |  * Extracts the Sentry issue ID and organization slug from a full URL
 14 |  *
 15 |  * @param url - A full Sentry issue URL
 16 |  * @returns Object containing the numeric issue ID and organization slug (if found)
 17 |  * @throws Error if the input is invalid
 18 |  */
 19 | export function extractIssueId(url: string): {
 20 |   issueId: string;
 21 |   organizationSlug: string;
 22 | } {
 23 |   if (!url || typeof url !== "string") {
 24 |     throw new UserInputError(
 25 |       "Invalid Sentry issue URL. URL must be a non-empty string.",
 26 |     );
 27 |   }
 28 | 
 29 |   if (!url.startsWith("http://") && !url.startsWith("https://")) {
 30 |     throw new UserInputError(
 31 |       "Invalid Sentry issue URL. Must start with http:// or https://",
 32 |     );
 33 |   }
 34 | 
 35 |   let parsedUrl: URL;
 36 |   try {
 37 |     parsedUrl = new URL(url);
 38 |   } catch (error) {
 39 |     throw new UserInputError(
 40 |       `Invalid Sentry issue URL. Unable to parse URL: ${url}`,
 41 |     );
 42 |   }
 43 | 
 44 |   const pathParts = parsedUrl.pathname.split("/").filter(Boolean);
 45 |   if (pathParts.length < 2 || !pathParts.includes("issues")) {
 46 |     throw new UserInputError(
 47 |       "Invalid Sentry issue URL. Path must contain '/issues/{issue_id}'",
 48 |     );
 49 |   }
 50 | 
 51 |   const issueId = pathParts[pathParts.indexOf("issues") + 1];
 52 |   if (!issueId) {
 53 |     throw new UserInputError("Unable to determine issue ID from URL.");
 54 |   }
 55 | 
 56 |   // Extract organization slug from either the path or subdomain
 57 |   let organizationSlug: string | undefined;
 58 |   if (pathParts.includes("organizations")) {
 59 |     organizationSlug = pathParts[pathParts.indexOf("organizations") + 1];
 60 |   } else if (pathParts.length > 1 && pathParts[0] !== "issues") {
 61 |     // If URL is like sentry.io/sentry/issues/123
 62 |     organizationSlug = pathParts[0];
 63 |   } else {
 64 |     // Check for subdomain
 65 |     const hostParts = parsedUrl.hostname.split(".");
 66 |     if (hostParts.length > 2 && hostParts[0] !== "www") {
 67 |       organizationSlug = hostParts[0];
 68 |     }
 69 |   }
 70 | 
 71 |   if (!organizationSlug) {
 72 |     throw new UserInputError(
 73 |       "Invalid Sentry issue URL. Could not determine organization.",
 74 |     );
 75 |   }
 76 | 
 77 |   return { issueId, organizationSlug };
 78 | }
 79 | 
 80 | /**
 81 |  * Sometimes the LLM will pass in a funky issue shortId. For example it might pass
 82 |  * in "CLOUDFLARE-MCP-41." instead of "CLOUDFLARE-MCP-41". This function attempts to
 83 |  * fix common issues.
 84 |  *
 85 |  * @param issueId - The issue ID to parse
 86 |  * @returns The parsed issue ID
 87 |  */
 88 | export function parseIssueId(issueId: string) {
 89 |   if (!issueId.trim()) {
 90 |     throw new UserInputError("Issue ID cannot be empty");
 91 |   }
 92 | 
 93 |   let finalIssueId = issueId;
 94 |   // remove trailing punctuation
 95 |   finalIssueId = finalIssueId.replace(/[^\w-]/g, "");
 96 | 
 97 |   if (!finalIssueId) {
 98 |     throw new UserInputError(
 99 |       "Issue ID cannot be empty after removing special characters",
100 |     );
101 |   }
102 | 
103 |   // Validate against common Sentry issue ID patterns
104 |   // Either numeric IDs or PROJECT-ABC123 format
105 |   // Allow project codes to start with alphanumeric characters (including numbers)
106 |   const validFormatRegex = /^(\d+|[A-Za-z0-9][\w-]*-[A-Za-z0-9]+)$/;
107 | 
108 |   if (!validFormatRegex.test(finalIssueId)) {
109 |     throw new UserInputError(
110 |       `Invalid issue ID format: "${finalIssueId}". Expected either a numeric ID or a project code followed by an alphanumeric identifier (e.g., "PROJECT-ABC123").`,
111 |     );
112 |   }
113 | 
114 |   return finalIssueId;
115 | }
116 | 
117 | /**
118 |  * Parses issue parameters from a variety of formats.
119 |  *
120 |  * @param params - Object containing issue URL, issue ID, and organization slug
121 |  * @returns Object containing the parsed organization slug and issue ID
122 |  * @throws Error if the input is invalid
123 |  */
124 | export function parseIssueParams({
125 |   issueUrl,
126 |   issueId,
127 |   organizationSlug,
128 | }: {
129 |   issueUrl?: string | null;
130 |   issueId?: string | null;
131 |   organizationSlug?: string | null;
132 | }): {
133 |   organizationSlug: string;
134 |   issueId: string;
135 | } {
136 |   if (issueUrl) {
137 |     const resolved = extractIssueId(issueUrl);
138 |     if (!resolved) {
139 |       throw new Error(
140 |         "Invalid Sentry issue URL. Path should contain '/issues/{issue_id}'",
141 |       );
142 |     }
143 |     return {
144 |       ...resolved,
145 |       issueId: parseIssueId(resolved.issueId),
146 |     };
147 |   }
148 | 
149 |   if (!organizationSlug) {
150 |     throw new UserInputError("Organization slug is required");
151 |   }
152 | 
153 |   if (issueId) {
154 |     return {
155 |       organizationSlug,
156 |       issueId: parseIssueId(issueId),
157 |     };
158 |   }
159 | 
160 |   throw new UserInputError("Either issueId or issueUrl must be provided");
161 | }
162 | 
```
Page 5/20FirstPrevNextLast