#
tokens: 49577/50000 39/493 files (page 4/20)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 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
│   │   │   │   │   ├── docs
│   │   │   │   │   │   └── toc.tsx
│   │   │   │   │   ├── 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
│   │   │   │   │   │   │   ├── gemini.tsx
│   │   │   │   │   │   │   └── sentry.tsx
│   │   │   │   │   │   ├── interactive-markdown.tsx
│   │   │   │   │   │   ├── json-schema-params.tsx
│   │   │   │   │   │   ├── markdown.tsx
│   │   │   │   │   │   ├── note.tsx
│   │   │   │   │   │   ├── prose.tsx
│   │   │   │   │   │   ├── section.tsx
│   │   │   │   │   │   ├── slash-command-actions.tsx
│   │   │   │   │   │   ├── slash-command-text.tsx
│   │   │   │   │   │   ├── sliding-panel.tsx
│   │   │   │   │   │   ├── template-vars.tsx
│   │   │   │   │   │   ├── tool-actions.tsx
│   │   │   │   │   │   └── typewriter.tsx
│   │   │   │   │   └── 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

--------------------------------------------------------------------------------
/docs/coding-guidelines.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Coding Guidelines
  2 | 
  3 | Essential patterns and standards for Sentry MCP development.
  4 | 
  5 | ## TypeScript Configuration
  6 | 
  7 | ```typescript
  8 | // tsconfig.json essentials
  9 | {
 10 |   "compilerOptions": {
 11 |     "strict": true,
 12 |     "target": "ES2022",
 13 |     "module": "ESNext",
 14 |     "moduleResolution": "Bundler",
 15 |     "sourceMap": true,
 16 |     "noImplicitAny": true
 17 |   }
 18 | }
 19 | ```
 20 | 
 21 | ## Code Style
 22 | 
 23 | ### Biome Configuration
 24 | - 2 spaces, double quotes, semicolons
 25 | - Max line: 100 chars
 26 | - Trailing commas in multiline
 27 | 
 28 | ### Naming Conventions
 29 | - Files: `kebab-case.ts`
 30 | - Functions: `camelCase`
 31 | - Types/Classes: `PascalCase`
 32 | - Constants: `UPPER_SNAKE_CASE`
 33 | 
 34 | ### Import Order
 35 | ```typescript
 36 | // 1. Node built-ins
 37 | import { readFile } from "node:fs/promises";
 38 | // 2. External deps
 39 | import { z } from "zod";
 40 | // 3. Internal packages
 41 | import { mockData } from "@sentry-mcp/mocks";
 42 | // 4. Relative imports
 43 | import { UserInputError } from "./errors.js";
 44 | ```
 45 | 
 46 | ## Tool Implementation
 47 | 
 48 | ```typescript
 49 | export const toolName = {
 50 |   description: "Clear, concise description",
 51 |   parameters: z.object({
 52 |     required: z.string().describe("Description"),
 53 |     optional: z.string().optional()
 54 |   }),
 55 |   execute: async (params, context) => {
 56 |     // 1. Validate inputs
 57 |     // 2. Call API
 58 |     // 3. Format output
 59 |     return formatResponse(data);
 60 |   }
 61 | };
 62 | ```
 63 | 
 64 | ## Testing Standards
 65 | 
 66 | ```typescript
 67 | describe("Component", () => {
 68 |   it("handles normal case", async () => {
 69 |     // Arrange
 70 |     const input = createTestInput();
 71 |     
 72 |     // Act
 73 |     const result = await method(input);
 74 |     
 75 |     // Assert
 76 |     expect(result).toMatchInlineSnapshot();
 77 |   });
 78 | });
 79 | ```
 80 | 
 81 | Key practices:
 82 | - Use inline snapshots for formatting
 83 | - Mock with MSW
 84 | - Test success and error paths
 85 | - Keep tests isolated
 86 | 
 87 | ## Quality Checklist
 88 | 
 89 | Before committing:
 90 | ```bash
 91 | pnpm -w run lint        # Biome check
 92 | pnpm -w run lint:fix    # Fix issues
 93 | pnpm tsc --noEmit       # Type check
 94 | pnpm test               # Run tests
 95 | pnpm -w run build       # Build all
 96 | ```
 97 | 
 98 | ## JSDoc Pattern
 99 | 
100 | ```typescript
101 | /**
102 |  * Brief description.
103 |  * 
104 |  * @param param - Description
105 |  * @returns What it returns
106 |  * 
107 |  * @example
108 |  * ```typescript
109 |  * const result = func(param);
110 |  * ```
111 |  */
112 | ```
113 | 
114 | ## Security Essentials
115 | 
116 | - Never commit secrets
117 | - Validate all inputs
118 | - Use environment variables
119 | - Sanitize displayed data
120 | 
121 | ## Common Patterns
122 | 
123 | For shared patterns see:
124 | - Error handling: `common-patterns.md#error-handling`
125 | - Zod schemas: `common-patterns.md#zod-schema-patterns`
126 | - API usage: `api-patterns.md`
127 | - Testing: `testing.md`
128 | 
129 | ## Monorepo Commands
130 | 
131 | ```bash
132 | # Workspace-wide (from root)
133 | pnpm -w run lint
134 | 
135 | # Package-specific (from package dir)
136 | pnpm test
137 | ```
138 | 
139 | ## References
140 | 
141 | - Architecture: `architecture.md`
142 | - Testing guide: `testing.md`
143 | - API patterns: `api-patterns.md`
144 | - Common patterns: `common-patterns.md`
```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/utils/rate-limiter.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { RateLimit } from "@cloudflare/workers-types";
  2 | 
  3 | /**
  4 |  * Result from rate limit check
  5 |  */
  6 | export interface RateLimitCheckResult {
  7 |   /**
  8 |    * Whether the request is allowed
  9 |    */
 10 |   allowed: boolean;
 11 | 
 12 |   /**
 13 |    * Error message if rate limited (only present when allowed=false)
 14 |    */
 15 |   errorMessage?: string;
 16 | }
 17 | 
 18 | /**
 19 |  * Hash a string using SHA-256 for privacy-preserving rate limit keys
 20 |  *
 21 |  * @param value - The value to hash (e.g., IP address, access token)
 22 |  * @returns Hex-encoded SHA-256 hash (first 16 characters)
 23 |  */
 24 | async function hashValue(value: string): Promise<string> {
 25 |   const encoder = new TextEncoder();
 26 |   const data = encoder.encode(value);
 27 |   const hashBuffer = await crypto.subtle.digest("SHA-256", data);
 28 |   const hashArray = Array.from(new Uint8Array(hashBuffer));
 29 |   const hashHex = hashArray
 30 |     .map((b) => b.toString(16).padStart(2, "0"))
 31 |     .join("");
 32 |   return hashHex.substring(0, 16); // Use first 16 chars of hash
 33 | }
 34 | 
 35 | /**
 36 |  * Check rate limit for a given identifier
 37 |  *
 38 |  * @param identifier - The identifier to rate limit (e.g., IP address, user ID)
 39 |  * @param rateLimiter - The Cloudflare rate limiter binding
 40 |  * @param options - Configuration options
 41 |  * @returns Result indicating whether the request is allowed
 42 |  *
 43 |  * @example
 44 |  * ```typescript
 45 |  * const result = await checkRateLimit(
 46 |  *   clientIP,
 47 |  *   env.MCP_RATE_LIMITER,
 48 |  *   {
 49 |  *     keyPrefix: "mcp:ip",
 50 |  *     errorMessage: "Rate limit exceeded. Please wait before trying again."
 51 |  *   }
 52 |  * );
 53 |  *
 54 |  * if (!result.allowed) {
 55 |  *   return new Response(result.errorMessage, { status: 429 });
 56 |  * }
 57 |  * ```
 58 |  */
 59 | export async function checkRateLimit(
 60 |   identifier: string,
 61 |   rateLimiter: RateLimit | undefined,
 62 |   options: {
 63 |     /**
 64 |      * Prefix for the rate limit key (e.g., "mcp:ip", "chat:user")
 65 |      */
 66 |     keyPrefix: string;
 67 | 
 68 |     /**
 69 |      * Error message to return when rate limited
 70 |      */
 71 |     errorMessage: string;
 72 |   },
 73 | ): Promise<RateLimitCheckResult> {
 74 |   // If rate limiter binding is not available (e.g., in development),
 75 |   // allow the request to proceed
 76 |   if (!rateLimiter) {
 77 |     return { allowed: true };
 78 |   }
 79 | 
 80 |   try {
 81 |     // Hash the identifier for privacy
 82 |     const hashedIdentifier = await hashValue(identifier);
 83 |     const rateLimitKey = `${options.keyPrefix}:${hashedIdentifier}`;
 84 | 
 85 |     // Check rate limit
 86 |     const { success } = await rateLimiter.limit({ key: rateLimitKey });
 87 | 
 88 |     if (!success) {
 89 |       return {
 90 |         allowed: false,
 91 |         errorMessage: options.errorMessage,
 92 |       };
 93 |     }
 94 | 
 95 |     return { allowed: true };
 96 |   } catch (error) {
 97 |     // If rate limiter fails, log error but allow request to proceed
 98 |     // This prevents rate limiter issues from breaking the service
 99 |     console.error("Rate limiter error:", error);
100 |     return { allowed: true };
101 |   }
102 | }
103 | 
```

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

```typescript
  1 | import type { Constraints } from "@sentry/mcp-core/types";
  2 | import { SentryApiService, ApiError } from "@sentry/mcp-core/api-client";
  3 | import { logIssue } from "@sentry/mcp-core/telem/logging";
  4 | 
  5 | /**
  6 |  * Verify that provided org/project constraints exist and the user has access
  7 |  * by querying Sentry's API using the provided OAuth access token.
  8 |  */
  9 | export async function verifyConstraintsAccess(
 10 |   { organizationSlug, projectSlug }: Constraints,
 11 |   {
 12 |     accessToken,
 13 |     sentryHost = "sentry.io",
 14 |   }: {
 15 |     accessToken: string | undefined | null;
 16 |     sentryHost?: string;
 17 |   },
 18 | ): Promise<
 19 |   | {
 20 |       ok: true;
 21 |       constraints: Constraints;
 22 |     }
 23 |   | { ok: false; status?: number; message: string; eventId?: string }
 24 | > {
 25 |   if (!organizationSlug) {
 26 |     // No constraints specified, nothing to verify
 27 |     return {
 28 |       ok: true,
 29 |       constraints: {
 30 |         organizationSlug: null,
 31 |         projectSlug: null,
 32 |         regionUrl: null,
 33 |       },
 34 |     };
 35 |   }
 36 | 
 37 |   if (!accessToken) {
 38 |     return {
 39 |       ok: false,
 40 |       status: 401,
 41 |       message: "Missing access token for constraint verification",
 42 |     };
 43 |   }
 44 | 
 45 |   // Use shared API client for consistent behavior and error handling
 46 |   const api = new SentryApiService({ accessToken, host: sentryHost });
 47 | 
 48 |   // Verify organization using API client
 49 |   let regionUrl: string | null | undefined = null;
 50 |   try {
 51 |     const org = await api.getOrganization(organizationSlug);
 52 |     regionUrl = org.links?.regionUrl || null;
 53 |   } catch (error) {
 54 |     if (error instanceof ApiError) {
 55 |       const message =
 56 |         error.status === 404
 57 |           ? `Organization '${organizationSlug}' not found`
 58 |           : error.message;
 59 |       return { ok: false, status: error.status, message };
 60 |     }
 61 |     const eventId = logIssue(error);
 62 |     return {
 63 |       ok: false,
 64 |       status: 502,
 65 |       message: "Failed to verify organization",
 66 |       eventId,
 67 |     };
 68 |   }
 69 | 
 70 |   // Verify project access if specified
 71 |   if (projectSlug) {
 72 |     try {
 73 |       await api.getProject(
 74 |         {
 75 |           organizationSlug,
 76 |           projectSlugOrId: projectSlug,
 77 |         },
 78 |         regionUrl ? { host: new URL(regionUrl).host } : undefined,
 79 |       );
 80 |     } catch (error) {
 81 |       if (error instanceof ApiError) {
 82 |         const message =
 83 |           error.status === 404
 84 |             ? `Project '${projectSlug}' not found in organization '${organizationSlug}'`
 85 |             : error.message;
 86 |         return { ok: false, status: error.status, message };
 87 |       }
 88 |       const eventId = logIssue(error);
 89 |       return {
 90 |         ok: false,
 91 |         status: 502,
 92 |         message: "Failed to verify project",
 93 |         eventId,
 94 |       };
 95 |     }
 96 |   }
 97 | 
 98 |   return {
 99 |     ok: true,
100 |     constraints: {
101 |       organizationSlug,
102 |       projectSlug: projectSlug || null,
103 |       regionUrl: regionUrl || null,
104 |     },
105 |   };
106 | }
107 | 
```

--------------------------------------------------------------------------------
/packages/mcp-core/src/internal/agents/tools/otel-semantics.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from "vitest";
  2 | import type { SentryApiService } from "../../../api-client";
  3 | 
  4 | vi.mock("../logging", () => ({
  5 |   logIssue: vi.fn(),
  6 | }));
  7 | 
  8 | // Import the actual function - no mocking needed since build runs first
  9 | import { lookupOtelSemantics } from "./otel-semantics";
 10 | 
 11 | describe("otel-semantics-lookup", () => {
 12 |   beforeEach(() => {
 13 |     vi.clearAllMocks();
 14 |   });
 15 | 
 16 |   const mockApiService = {} as SentryApiService;
 17 | 
 18 |   describe("lookupOtelSemantics", () => {
 19 |     it("should return namespace information for valid namespace", async () => {
 20 |       const result = await lookupOtelSemantics(
 21 |         "gen_ai",
 22 |         "spans",
 23 |         mockApiService,
 24 |         "test-org",
 25 |       );
 26 | 
 27 |       expect(result).toContain("# OpenTelemetry Semantic Conventions: gen_ai");
 28 |       expect(result).toContain("## Attributes");
 29 |       expect(result).toContain("`gen_ai.usage.input_tokens`");
 30 |       expect(result).toContain("`gen_ai.usage.output_tokens`");
 31 |       expect(result).toContain("- **Type:**");
 32 |       expect(result).toContain("- **Description:**");
 33 |     });
 34 | 
 35 |     it("should handle namespace with underscore and dash interchangeably", async () => {
 36 |       const result1 = await lookupOtelSemantics(
 37 |         "gen_ai",
 38 |         "spans",
 39 |         mockApiService,
 40 |         "test-org",
 41 |       );
 42 |       const result2 = await lookupOtelSemantics(
 43 |         "gen-ai",
 44 |         "spans",
 45 |         mockApiService,
 46 |         "test-org",
 47 |       );
 48 | 
 49 |       expect(result1).toBe(result2);
 50 |     });
 51 | 
 52 |     it("should return all attributes for a namespace", async () => {
 53 |       const result = await lookupOtelSemantics(
 54 |         "http",
 55 |         "spans",
 56 |         mockApiService,
 57 |         "test-org",
 58 |       );
 59 | 
 60 |       expect(result).toContain("total)");
 61 |       expect(result).toContain("`http.request.method`");
 62 |       expect(result).toContain("`http.response.status_code`");
 63 |     });
 64 | 
 65 |     it("should show custom namespace note for mcp", async () => {
 66 |       const result = await lookupOtelSemantics(
 67 |         "mcp",
 68 |         "spans",
 69 |         mockApiService,
 70 |         "test-org",
 71 |       );
 72 | 
 73 |       expect(result).toContain("**Note:** This is a custom namespace");
 74 |     });
 75 | 
 76 |     it("should handle invalid namespace", async () => {
 77 |       const result = await lookupOtelSemantics(
 78 |         "totally_invalid_namespace_that_does_not_exist",
 79 |         "spans",
 80 |         mockApiService,
 81 |         "test-org",
 82 |       );
 83 | 
 84 |       expect(result).toContain(
 85 |         "Namespace 'totally_invalid_namespace_that_does_not_exist' not found",
 86 |       );
 87 |     });
 88 | 
 89 |     it("should suggest similar namespaces", async () => {
 90 |       const result = await lookupOtelSemantics(
 91 |         "gen",
 92 |         "spans",
 93 |         mockApiService,
 94 |         "test-org",
 95 |       );
 96 | 
 97 |       expect(result).toContain("Did you mean:");
 98 |       expect(result).toContain("gen_ai");
 99 |     });
100 |   });
101 | });
102 | 
```

--------------------------------------------------------------------------------
/packages/mcp-core/src/utils/slug-validation.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Slug validation utilities to prevent path traversal and injection attacks.
  3 |  *
  4 |  * Provides reusable validation functions for use with Zod's superRefine()
  5 |  * to add security validation for URL parameters.
  6 |  */
  7 | 
  8 | import { z } from "zod";
  9 | 
 10 | /**
 11 |  * Maximum reasonable length for a slug.
 12 |  */
 13 | const MAX_SLUG_LENGTH = 100;
 14 | 
 15 | /**
 16 |  * Maximum reasonable length for a numeric ID.
 17 |  */
 18 | const MAX_ID_LENGTH = 20;
 19 | 
 20 | /**
 21 |  * Helper to check if a string is a numeric ID.
 22 |  */
 23 | export function isNumericId(value: string): boolean {
 24 |   return /^\d+$/.test(value);
 25 | }
 26 | 
 27 | /**
 28 |  * Valid slug pattern: alphanumeric, hyphens, underscores, and dots.
 29 |  * Must start with alphanumeric character.
 30 |  */
 31 | const VALID_SLUG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
 32 | 
 33 | /**
 34 |  * Validates a slug to prevent path traversal and injection attacks.
 35 |  * Designed to be used with Zod's superRefine() method.
 36 |  *
 37 |  * @example
 38 |  * ```typescript
 39 |  * const OrganizationSlug = z.string()
 40 |  *   .toLowerCase()
 41 |  *   .trim()
 42 |  *   .superRefine(validateSlug)
 43 |  *   .describe("Organization slug");
 44 |  *
 45 |  * const TeamSlug = z.string()
 46 |  *   .toLowerCase()
 47 |  *   .trim()
 48 |  *   .superRefine(validateSlug)
 49 |  *   .describe("Team slug");
 50 |  * ```
 51 |  */
 52 | export function validateSlug(val: string, ctx: z.RefinementCtx): void {
 53 |   // Check for empty string
 54 |   if (val.length === 0) {
 55 |     ctx.addIssue({
 56 |       code: z.ZodIssueCode.custom,
 57 |       message: "Slug cannot be empty",
 58 |     });
 59 |     return;
 60 |   }
 61 | 
 62 |   // Check length
 63 |   if (val.length > MAX_SLUG_LENGTH) {
 64 |     ctx.addIssue({
 65 |       code: z.ZodIssueCode.custom,
 66 |       message: `Slug exceeds maximum length of ${MAX_SLUG_LENGTH} characters`,
 67 |     });
 68 |     return;
 69 |   }
 70 | 
 71 |   // Validate pattern - this implicitly blocks all dangerous characters and patterns
 72 |   if (!VALID_SLUG_PATTERN.test(val)) {
 73 |     ctx.addIssue({
 74 |       code: z.ZodIssueCode.custom,
 75 |       message:
 76 |         "Slug must contain only alphanumeric characters, hyphens, underscores, and dots, and must start with an alphanumeric character",
 77 |     });
 78 |   }
 79 | }
 80 | 
 81 | /**
 82 |  * Validates a parameter that can be either a slug or numeric ID.
 83 |  * Designed to be used with Zod's superRefine() method.
 84 |  *
 85 |  * @example
 86 |  * ```typescript
 87 |  * const ProjectSlugOrId = z.string()
 88 |  *   .toLowerCase()
 89 |  *   .trim()
 90 |  *   .superRefine(validateSlugOrId)
 91 |  *   .describe("Project slug or numeric ID");
 92 |  *
 93 |  * const IssueSlugOrId = z.string()
 94 |  *   .trim()
 95 |  *   .superRefine(validateSlugOrId)
 96 |  *   .describe("Issue slug or numeric ID");
 97 |  * ```
 98 |  */
 99 | export function validateSlugOrId(val: string, ctx: z.RefinementCtx): void {
100 |   // Check if it's a numeric ID
101 |   if (isNumericId(val)) {
102 |     if (val.length > MAX_ID_LENGTH) {
103 |       ctx.addIssue({
104 |         code: z.ZodIssueCode.custom,
105 |         message: `Numeric ID exceeds maximum length of ${MAX_ID_LENGTH} characters`,
106 |       });
107 |     }
108 |     // Numeric IDs don't need slug validation
109 |     return;
110 |   }
111 | 
112 |   // Otherwise validate as a slug
113 |   validateSlug(val, ctx);
114 | }
115 | 
```

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

```json
 1 | {
 2 |   "namespace": "jvm",
 3 |   "description": "This document defines Java Virtual machine related attributes.\n",
 4 |   "attributes": {
 5 |     "jvm.gc.action": {
 6 |       "description": "Name of the garbage collector action.",
 7 |       "type": "string",
 8 |       "note": "Garbage collector action is generally obtained via [GarbageCollectionNotificationInfo#getGcAction()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcAction()).\n",
 9 |       "stability": "stable",
10 |       "examples": ["end of minor GC", "end of major GC"]
11 |     },
12 |     "jvm.gc.cause": {
13 |       "description": "Name of the garbage collector cause.",
14 |       "type": "string",
15 |       "note": "Garbage collector cause is generally obtained via [GarbageCollectionNotificationInfo#getGcCause()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcCause()).\n",
16 |       "stability": "development",
17 |       "examples": ["System.gc()", "Allocation Failure"]
18 |     },
19 |     "jvm.gc.name": {
20 |       "description": "Name of the garbage collector.",
21 |       "type": "string",
22 |       "note": "Garbage collector name is generally obtained via [GarbageCollectionNotificationInfo#getGcName()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcName()).\n",
23 |       "stability": "stable",
24 |       "examples": ["G1 Young Generation", "G1 Old Generation"]
25 |     },
26 |     "jvm.memory.type": {
27 |       "description": "The type of memory.",
28 |       "type": "string",
29 |       "stability": "stable",
30 |       "examples": ["heap", "non_heap"]
31 |     },
32 |     "jvm.memory.pool.name": {
33 |       "description": "Name of the memory pool.",
34 |       "type": "string",
35 |       "note": "Pool names are generally obtained via [MemoryPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/MemoryPoolMXBean.html#getName()).\n",
36 |       "stability": "stable",
37 |       "examples": ["G1 Old Gen", "G1 Eden space", "G1 Survivor Space"]
38 |     },
39 |     "jvm.thread.daemon": {
40 |       "description": "Whether the thread is daemon or not.",
41 |       "type": "boolean",
42 |       "stability": "stable"
43 |     },
44 |     "jvm.thread.state": {
45 |       "description": "State of the thread.",
46 |       "type": "string",
47 |       "stability": "stable",
48 |       "examples": [
49 |         "new",
50 |         "runnable",
51 |         "blocked",
52 |         "waiting",
53 |         "timed_waiting",
54 |         "terminated"
55 |       ]
56 |     },
57 |     "jvm.buffer.pool.name": {
58 |       "description": "Name of the buffer pool.",
59 |       "type": "string",
60 |       "note": "Pool names are generally obtained via [BufferPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/BufferPoolMXBean.html#getName()).\n",
61 |       "stability": "development",
62 |       "examples": ["mapped", "direct"]
63 |     }
64 |   }
65 | }
66 | 
```

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

```typescript
 1 | import { experimental_createMCPClient } from "ai";
 2 | import { Experimental_StdioMCPTransport } from "ai/mcp-stdio";
 3 | import { fileURLToPath } from "node:url";
 4 | import { dirname, join } from "node:path";
 5 | import { startNewTrace, startSpan } from "@sentry/core";
 6 | import { logSuccess } from "./logger.js";
 7 | import type { MCPConnection, MCPConfig } from "./types.js";
 8 | import { randomUUID } from "node:crypto";
 9 | import { LIB_VERSION } from "./version.js";
10 | 
11 | export async function connectToMCPServer(
12 |   config: MCPConfig,
13 | ): Promise<MCPConnection> {
14 |   const sessionId = randomUUID();
15 | 
16 |   return await startNewTrace(async () => {
17 |     return await startSpan(
18 |       {
19 |         name: "mcp.connect/stdio",
20 |         attributes: {
21 |           "mcp.transport": "stdio",
22 |           "gen_ai.conversation.id": sessionId,
23 |           "service.version": LIB_VERSION,
24 |         },
25 |       },
26 |       async (span) => {
27 |         try {
28 |           const args = [`--access-token=${config.accessToken}`];
29 |           if (config.host) {
30 |             args.push(`--host=${config.host}`);
31 |           }
32 |           if (config.sentryDsn) {
33 |             args.push(`--sentry-dsn=${config.sentryDsn}`);
34 |           }
35 |           if (config.useAgentEndpoint) {
36 |             args.push("--agent");
37 |           }
38 | 
39 |           // Resolve the path to the mcp-server binary
40 |           const __dirname = dirname(fileURLToPath(import.meta.url));
41 |           const mcpServerPath = join(
42 |             __dirname,
43 |             "../../mcp-server/dist/index.js",
44 |           );
45 | 
46 |           const transport = new Experimental_StdioMCPTransport({
47 |             command: "node",
48 |             args: [mcpServerPath, ...args],
49 |             env: {
50 |               ...process.env,
51 |               SENTRY_ACCESS_TOKEN: config.accessToken,
52 |               SENTRY_HOST: config.host || "sentry.io",
53 |               ...(config.sentryDsn && { SENTRY_DSN: config.sentryDsn }),
54 |             },
55 |           });
56 | 
57 |           const client = await experimental_createMCPClient({
58 |             name: "mcp.sentry.dev (test-client)",
59 |             transport,
60 |           });
61 | 
62 |           // Discover available tools
63 |           const toolsMap = await client.tools();
64 |           const tools = new Map<string, any>();
65 | 
66 |           for (const [name, tool] of Object.entries(toolsMap)) {
67 |             tools.set(name, tool);
68 |           }
69 | 
70 |           // Remove custom attributes - let SDK handle standard attributes
71 |           span.setStatus({ code: 1 }); // OK status
72 | 
73 |           logSuccess(
74 |             "Connected to MCP server (stdio)",
75 |             `${tools.size} tools available`,
76 |           );
77 | 
78 |           const disconnect = async () => {
79 |             await client.close();
80 |           };
81 | 
82 |           return {
83 |             client,
84 |             tools,
85 |             disconnect,
86 |             sessionId,
87 |             transport: "stdio" as const,
88 |           };
89 |         } catch (error) {
90 |           span.setStatus({ code: 2 }); // Error status
91 |           throw error;
92 |         }
93 |       },
94 |     );
95 |   });
96 | }
97 | 
```

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

```typescript
 1 | import {
 2 |   SentryApiService,
 3 |   ApiClientError,
 4 |   ApiNotFoundError,
 5 | } from "../../api-client/index";
 6 | import { UserInputError } from "../../errors";
 7 | import type { ServerContext } from "../../types";
 8 | import { validateRegionUrl } from "./validate-region-url";
 9 | 
10 | /**
11 |  * Create a Sentry API service from server context with optional region override
12 |  * @param context - Server context containing host and access token
13 |  * @param opts - Options object containing optional regionUrl override
14 |  * @returns Configured SentryApiService instance (always uses HTTPS)
15 |  * @throws {UserInputError} When regionUrl is provided but invalid
16 |  */
17 | export function apiServiceFromContext(
18 |   context: ServerContext,
19 |   opts: { regionUrl?: string } = {},
20 | ) {
21 |   let host = context.sentryHost;
22 | 
23 |   if (opts.regionUrl?.trim()) {
24 |     // Validate the regionUrl against the base host to prevent SSRF
25 |     // Use default host if context.sentryHost is not set
26 |     const baseHost = context.sentryHost || "sentry.io";
27 |     host = validateRegionUrl(opts.regionUrl.trim(), baseHost);
28 |   }
29 | 
30 |   return new SentryApiService({
31 |     host,
32 |     accessToken: context.accessToken,
33 |   });
34 | }
35 | 
36 | /**
37 |  * Maps API errors to user-friendly errors based on context
38 |  * @param error - The error to handle
39 |  * @param params - The parameters that were used in the API call
40 |  * @returns Never - always throws an error
41 |  * @throws {UserInputError} For 4xx errors that are likely user input issues
42 |  * @throws {Error} For other errors
43 |  */
44 | export function handleApiError(
45 |   error: unknown,
46 |   params?: Record<string, unknown>,
47 | ): never {
48 |   // Use the new error hierarchy - all 4xx errors extend ApiClientError
49 |   if (error instanceof ApiClientError) {
50 |     let message = `API error (${error.status}): ${error.message}`;
51 | 
52 |     // Special handling for 404s with parameter context
53 |     if (error instanceof ApiNotFoundError && params) {
54 |       const paramsList: string[] = [];
55 |       for (const [key, value] of Object.entries(params)) {
56 |         if (value !== undefined && value !== null && value !== "") {
57 |           paramsList.push(`${key}: '${value}'`);
58 |         }
59 |       }
60 | 
61 |       if (paramsList.length > 0) {
62 |         message = `Resource not found (404): ${error.message}\nPlease verify these parameters are correct:\n${paramsList.map((p) => `  - ${p}`).join("\n")}`;
63 |       }
64 |     }
65 | 
66 |     throw new UserInputError(message, { cause: error });
67 |   }
68 | 
69 |   // All other errors bubble up (including ApiServerError for 5xx)
70 |   throw error;
71 | }
72 | 
73 | /**
74 |  * Wraps an async API call with automatic error handling
75 |  * @param fn - The async function to execute
76 |  * @param params - The parameters that were used in the API call
77 |  * @returns The result of the function
78 |  * @throws {UserInputError} For user input errors
79 |  * @throws {Error} For other errors
80 |  */
81 | export async function withApiErrorHandling<T>(
82 |   fn: () => Promise<T>,
83 |   params?: Record<string, unknown>,
84 | ): Promise<T> {
85 |   try {
86 |     return await fn();
87 |   } catch (error) {
88 |     handleApiError(error, params);
89 |   }
90 | }
91 | 
```

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

```typescript
 1 | /**
 2 |  * Generic tool wrapper for the use_sentry embedded agent.
 3 |  *
 4 |  * Provides a single function that can wrap ANY MCP tool handler
 5 |  * to work with the embedded agent pattern.
 6 |  */
 7 | 
 8 | import { z } from "zod";
 9 | import { agentTool } from "../../internal/agents/tools/utils";
10 | import type { ServerContext } from "../../types";
11 | import type { ToolConfig } from "../types";
12 | 
13 | /**
14 |  * Options for wrapping a tool
15 |  */
16 | export interface WrapToolOptions {
17 |   context: ServerContext;
18 | }
19 | 
20 | /**
21 |  * Helper to inject constrained parameters into tool calls.
22 |  * This applies session-level constraints (org, project, region) to tool parameters.
23 |  */
24 | function injectConstrainedParams(
25 |   params: Record<string, any>,
26 |   constraints: ServerContext["constraints"],
27 | ): Record<string, any> {
28 |   const result = { ...params };
29 | 
30 |   // Apply organization constraint if set
31 |   if (constraints.organizationSlug && !result.organizationSlug) {
32 |     result.organizationSlug = constraints.organizationSlug;
33 |   }
34 | 
35 |   // Apply project constraint (handle both projectSlug and projectSlugOrId)
36 |   if (constraints.projectSlug) {
37 |     if (!result.projectSlug) {
38 |       result.projectSlug = constraints.projectSlug;
39 |     }
40 |     if (!result.projectSlugOrId) {
41 |       result.projectSlugOrId = constraints.projectSlug;
42 |     }
43 |   }
44 | 
45 |   // Apply region constraint if set
46 |   if (constraints.regionUrl && !result.regionUrl) {
47 |     result.regionUrl = constraints.regionUrl;
48 |   }
49 | 
50 |   return result;
51 | }
52 | 
53 | /**
54 |  * Wraps any MCP tool to work with the embedded agent pattern.
55 |  *
56 |  * This function:
57 |  * 1. Takes a tool definition with its handler
58 |  * 2. Creates an agentTool-wrapped version
59 |  * 3. Pre-binds ServerContext so the agent doesn't need it
60 |  * 4. Applies session constraints automatically
61 |  * 5. Handles errors via agentTool's error handling
62 |  *
63 |  * @param tool - The MCP tool to wrap (from defineTool)
64 |  * @param options - Context and configuration for the tool
65 |  * @returns An agentTool-wrapped version ready for use by the embedded agent
66 |  *
67 |  * @example
68 |  * ```typescript
69 |  * const whoami = wrapToolForAgent(whoamiTool, { context });
70 |  * const findOrgs = wrapToolForAgent(findOrganizationsTool, { context });
71 |  * ```
72 |  */
73 | export function wrapToolForAgent<TSchema extends Record<string, z.ZodType>>(
74 |   tool: ToolConfig<TSchema>,
75 |   options: WrapToolOptions,
76 | ) {
77 |   return agentTool({
78 |     description: tool.description,
79 |     parameters: z.object(tool.inputSchema),
80 |     execute: async (params: unknown) => {
81 |       // Type safety: params is validated by agentTool's Zod schema before reaching here
82 |       const fullParams = injectConstrainedParams(
83 |         params as Record<string, unknown>,
84 |         options.context.constraints,
85 |       );
86 | 
87 |       // Call the actual tool handler with full context
88 |       // Type assertion is safe: fullParams matches the tool's input schema (enforced by Zod)
89 |       const result = await tool.handler(fullParams as never, options.context);
90 | 
91 |       // Return the result - agentTool handles error wrapping
92 |       return result;
93 |     },
94 |   });
95 | }
96 | 
```

--------------------------------------------------------------------------------
/docs/specs/subpath-constraints.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Subpath-Based Constraints (End-User Guide)
 2 | 
 3 | ## What constraints do
 4 | 
 5 | Constraints let you scope your Sentry MCP session to a specific organization and optionally a project. When scoped, all tools automatically use that org/project by default and only access data you are permitted to see.
 6 | 
 7 | ## How to connect
 8 | 
 9 | - No scope: connect to `/mcp` (or `/sse` for SSE transport)
10 | - Organization scope: `/mcp/{organizationSlug}`
11 | - Organization + project scope: `/mcp/{organizationSlug}/{projectSlug}`
12 | 
13 | The same pattern applies to the SSE endpoint: `/sse`, `/sse/{org}`, `/sse/{org}/{project}`.
14 | 
15 | Examples:
16 | 
17 | ```
18 | /mcp/sentry
19 | /mcp/sentry/my-project
20 | /sse/sentry
21 | /sse/sentry/my-project
22 | ```
23 | 
24 | ## What you'll experience
25 | 
26 | - Tools automatically use the constrained organization/project as defaults
27 | - You can still pass explicit `organizationSlug`/`projectSlug` to override defaults per call
28 | - If you don't provide a scope, tools work across your accessible organizations when supported
29 | - Some tools are filtered when not useful: `find_organizations` is hidden when scoped to an org, and `find_projects` is hidden when scoped to a project
30 | 
31 | ## Access verification
32 | 
33 | When you connect with a scoped path, we validate that:
34 | - The slugs are well-formed
35 | - The organization exists and you have access
36 | - If a project is included, the project exists and you have access
37 | 
38 | If there’s a problem, you’ll receive a clear HTTP error when connecting:
39 | - 400: Invalid slug format
40 | - 401: Missing authentication
41 | - 403: You don’t have access to the specified org/project
42 | - 404: Organization or project not found
43 | 
44 | ## Region awareness
45 | 
46 | For Sentry Cloud, your organization may be hosted in a regional cluster. When you scope by organization, we automatically determine the region (if available) and use it for API calls. You don’t need to take any action—this happens behind the scenes. For self-hosted Sentry, the region concept doesn’t apply.
47 | 
48 | ## Best practices
49 | 
50 | - Prefer scoping by organization (and project when known) to reduce ambiguity and improve safety
51 | - Use scoped sessions when collaborating across multiple orgs to avoid cross-org access by mistake
52 | - If a tool reports access errors, reconnect with a different scope or verify your permissions in Sentry
53 | 
54 | ## Frequently asked questions
55 | 
56 | - Can I switch scope mid-session?
57 |   - Yes. Open a new connection using a different subpath (e.g., `/mcp/{org}/{project}`) and use that session.
58 | 
59 | - Do I need to specify scope for documentation or metadata endpoints?
60 |   - No. Public metadata endpoints don’t require scope and support CORS.
61 | 
62 | - How do tools know my scope?
63 |   - The MCP session embeds the constraints, and tools read them as defaults for `organizationSlug` and `projectSlug`.
64 | 
65 | ## Reference
66 | 
67 | Supported URL patterns:
68 | ```
69 | /mcp/{organizationSlug}/{projectSlug}
70 | /mcp/{organizationSlug}
71 | /mcp
72 | 
73 | /sse/{organizationSlug}/{projectSlug}
74 | /sse/{organizationSlug}
75 | /sse
76 | ```
77 | 
78 | For implementation details and security notes, see:
79 | - `docs/cloudflare/constraint-flow-verification.md`
80 | - `docs/architecture.md`
81 | 
```

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

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

--------------------------------------------------------------------------------
/packages/mcp-server/src/cli/resolve.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import { finalize } from "./resolve";
  3 | 
  4 | describe("cli/finalize", () => {
  5 |   it("throws on missing access token", () => {
  6 |     expect(() => finalize({ unknownArgs: [] } as any)).toThrow(
  7 |       /No access token was provided/,
  8 |     );
  9 |   });
 10 | 
 11 |   it("normalizes host from URL", () => {
 12 |     const cfg = finalize({
 13 |       accessToken: "tok",
 14 |       url: "https://sentry.example.com",
 15 |       unknownArgs: [],
 16 |     });
 17 |     expect(cfg.sentryHost).toBe("sentry.example.com");
 18 |   });
 19 | 
 20 |   it("accepts valid OpenAI base URL", () => {
 21 |     const cfg = finalize({
 22 |       accessToken: "tok",
 23 |       openaiBaseUrl: "https://api.proxy.example/v1",
 24 |       unknownArgs: [],
 25 |     });
 26 |     expect(cfg.openaiBaseUrl).toBe(
 27 |       new URL("https://api.proxy.example/v1").toString(),
 28 |     );
 29 |   });
 30 | 
 31 |   it("rejects invalid OpenAI base URL", () => {
 32 |     expect(() =>
 33 |       finalize({
 34 |         accessToken: "tok",
 35 |         openaiBaseUrl: "ftp://example.com",
 36 |         unknownArgs: [],
 37 |       }),
 38 |     ).toThrow(/OPENAI base URL must use http or https scheme/);
 39 |   });
 40 | 
 41 |   it("throws on non-https URL", () => {
 42 |     expect(() =>
 43 |       finalize({ accessToken: "tok", url: "http://bad", unknownArgs: [] }),
 44 |     ).toThrow(/must be a full HTTPS URL/);
 45 |   });
 46 | 
 47 |   // Skills tests
 48 |   it("throws on invalid skills", () => {
 49 |     expect(() =>
 50 |       finalize({
 51 |         accessToken: "tok",
 52 |         skills: "invalid-skill",
 53 |         unknownArgs: [],
 54 |       }),
 55 |     ).toThrow(/Invalid skills provided: invalid-skill/);
 56 |   });
 57 | 
 58 |   it("validates multiple skills and reports all invalid ones", () => {
 59 |     expect(() =>
 60 |       finalize({
 61 |         accessToken: "tok",
 62 |         skills: "inspect,invalid1,triage,invalid2",
 63 |         unknownArgs: [],
 64 |       }),
 65 |     ).toThrow(/Invalid skills provided: invalid1, invalid2/);
 66 |   });
 67 | 
 68 |   it("resolves valid skills in override mode (--skills)", () => {
 69 |     const cfg = finalize({
 70 |       accessToken: "tok",
 71 |       skills: "inspect,triage",
 72 |       unknownArgs: [],
 73 |     });
 74 |     expect(cfg.finalSkills?.has("inspect")).toBe(true);
 75 |     expect(cfg.finalSkills?.has("triage")).toBe(true);
 76 |     expect(cfg.finalSkills?.size).toBe(2);
 77 |     // Should not include defaults
 78 |     expect(cfg.finalSkills?.has("docs")).toBe(false);
 79 |   });
 80 | 
 81 |   it("throws on empty skills after validation", () => {
 82 |     expect(() =>
 83 |       finalize({
 84 |         accessToken: "tok",
 85 |         skills: "invalid1,invalid2",
 86 |         unknownArgs: [],
 87 |       }),
 88 |     ).toThrow(/Invalid skills provided/);
 89 |   });
 90 | 
 91 |   it("grants all skills when no skills specified", () => {
 92 |     const cfg = finalize({
 93 |       accessToken: "tok",
 94 |       unknownArgs: [],
 95 |     });
 96 |     expect(cfg.finalSkills).toBeDefined();
 97 |     expect(cfg.finalSkills?.size).toBe(5); // All skills: inspect, triage, project-management, seer, docs
 98 |     expect(cfg.finalSkills?.has("inspect")).toBe(true);
 99 |     expect(cfg.finalSkills?.has("triage")).toBe(true);
100 |     expect(cfg.finalSkills?.has("project-management")).toBe(true);
101 |     expect(cfg.finalSkills?.has("seer")).toBe(true);
102 |     expect(cfg.finalSkills?.has("docs")).toBe(true);
103 |   });
104 | });
105 | 
```

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

```json
 1 | {
 2 |   "namespace": "feature_flags",
 3 |   "description": "This document defines attributes for Feature Flags.\n",
 4 |   "attributes": {
 5 |     "feature_flag.key": {
 6 |       "description": "The lookup key of the feature flag.",
 7 |       "type": "string",
 8 |       "stability": "release_candidate",
 9 |       "examples": ["logo-color"]
10 |     },
11 |     "feature_flag.provider.name": {
12 |       "description": "Identifies the feature flag provider.",
13 |       "type": "string",
14 |       "stability": "release_candidate",
15 |       "examples": ["Flag Manager"]
16 |     },
17 |     "feature_flag.result.variant": {
18 |       "description": "A semantic identifier for an evaluated flag value.\n",
19 |       "type": "string",
20 |       "note": "A semantic identifier, commonly referred to as a variant, provides a means\nfor referring to a value without including the value itself. This can\nprovide additional context for understanding the meaning behind a value.\nFor example, the variant `red` maybe be used for the value `#c05543`.",
21 |       "stability": "release_candidate",
22 |       "examples": ["red", "true", "on"]
23 |     },
24 |     "feature_flag.context.id": {
25 |       "description": "The unique identifier for the flag evaluation context. For example, the targeting key.\n",
26 |       "type": "string",
27 |       "stability": "release_candidate",
28 |       "examples": ["5157782b-2203-4c80-a857-dbbd5e7761db"]
29 |     },
30 |     "feature_flag.version": {
31 |       "description": "The version of the ruleset used during the evaluation. This may be any stable value which uniquely identifies the ruleset.\n",
32 |       "type": "string",
33 |       "stability": "release_candidate",
34 |       "examples": ["1", "01ABCDEF"]
35 |     },
36 |     "feature_flag.set.id": {
37 |       "description": "The identifier of the [flag set](https://openfeature.dev/specification/glossary/#flag-set) to which the feature flag belongs.\n",
38 |       "type": "string",
39 |       "stability": "release_candidate",
40 |       "examples": ["proj-1", "ab98sgs", "service1/dev"]
41 |     },
42 |     "feature_flag.result.reason": {
43 |       "description": "The reason code which shows how a feature flag value was determined.\n",
44 |       "type": "string",
45 |       "stability": "release_candidate",
46 |       "examples": [
47 |         "static",
48 |         "default",
49 |         "targeting_match",
50 |         "split",
51 |         "cached",
52 |         "disabled",
53 |         "unknown",
54 |         "stale",
55 |         "error"
56 |       ]
57 |     },
58 |     "feature_flag.result.value": {
59 |       "description": "The evaluated value of the feature flag.",
60 |       "type": "string",
61 |       "note": "With some feature flag providers, feature flag results can be quite large or contain private or sensitive details.\nBecause of this, `feature_flag.result.variant` is often the preferred attribute if it is available.\n\nIt may be desirable to redact or otherwise limit the size and scope of `feature_flag.result.value` if possible.\nBecause the evaluated flag value is unstructured and may be any type, it is left to the instrumentation author to determine how best to achieve this.\n",
62 |       "stability": "release_candidate",
63 |       "examples": ["#ff0000", "true", "3"]
64 |     }
65 |   }
66 | }
67 | 
```

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

```typescript
 1 | import { describe, it, expect } from "vitest";
 2 | import {
 3 |   isTerminalStatus,
 4 |   isHumanInterventionStatus,
 5 |   getStatusDisplayName,
 6 |   getHumanInterventionGuidance,
 7 | } from "./seer";
 8 | 
 9 | describe("seer-utils", () => {
10 |   describe("isTerminalStatus", () => {
11 |     it("returns true for terminal statuses", () => {
12 |       expect(isTerminalStatus("COMPLETED")).toBe(true);
13 |       expect(isTerminalStatus("FAILED")).toBe(true);
14 |       expect(isTerminalStatus("ERROR")).toBe(true);
15 |       expect(isTerminalStatus("CANCELLED")).toBe(true);
16 |       expect(isTerminalStatus("NEED_MORE_INFORMATION")).toBe(true);
17 |       expect(isTerminalStatus("WAITING_FOR_USER_RESPONSE")).toBe(true);
18 |     });
19 | 
20 |     it("returns false for non-terminal statuses", () => {
21 |       expect(isTerminalStatus("PROCESSING")).toBe(false);
22 |       expect(isTerminalStatus("IN_PROGRESS")).toBe(false);
23 |       expect(isTerminalStatus("PENDING")).toBe(false);
24 |     });
25 |   });
26 | 
27 |   describe("isHumanInterventionStatus", () => {
28 |     it("returns true for human intervention statuses", () => {
29 |       expect(isHumanInterventionStatus("NEED_MORE_INFORMATION")).toBe(true);
30 |       expect(isHumanInterventionStatus("WAITING_FOR_USER_RESPONSE")).toBe(true);
31 |     });
32 | 
33 |     it("returns false for other statuses", () => {
34 |       expect(isHumanInterventionStatus("COMPLETED")).toBe(false);
35 |       expect(isHumanInterventionStatus("PROCESSING")).toBe(false);
36 |       expect(isHumanInterventionStatus("FAILED")).toBe(false);
37 |     });
38 |   });
39 | 
40 |   describe("getStatusDisplayName", () => {
41 |     it("returns friendly names for known statuses", () => {
42 |       expect(getStatusDisplayName("COMPLETED")).toBe("Complete");
43 |       expect(getStatusDisplayName("FAILED")).toBe("Failed");
44 |       expect(getStatusDisplayName("ERROR")).toBe("Failed");
45 |       expect(getStatusDisplayName("CANCELLED")).toBe("Cancelled");
46 |       expect(getStatusDisplayName("NEED_MORE_INFORMATION")).toBe(
47 |         "Needs More Information",
48 |       );
49 |       expect(getStatusDisplayName("WAITING_FOR_USER_RESPONSE")).toBe(
50 |         "Waiting for Response",
51 |       );
52 |       expect(getStatusDisplayName("PROCESSING")).toBe("Processing");
53 |       expect(getStatusDisplayName("IN_PROGRESS")).toBe("In Progress");
54 |     });
55 | 
56 |     it("returns status as-is for unknown statuses", () => {
57 |       expect(getStatusDisplayName("UNKNOWN_STATUS")).toBe("UNKNOWN_STATUS");
58 |     });
59 |   });
60 | 
61 |   describe("getHumanInterventionGuidance", () => {
62 |     it("returns guidance for NEED_MORE_INFORMATION", () => {
63 |       const guidance = getHumanInterventionGuidance("NEED_MORE_INFORMATION");
64 |       expect(guidance).toContain("Seer needs additional information");
65 |     });
66 | 
67 |     it("returns guidance for WAITING_FOR_USER_RESPONSE", () => {
68 |       const guidance = getHumanInterventionGuidance(
69 |         "WAITING_FOR_USER_RESPONSE",
70 |       );
71 |       expect(guidance).toContain("Seer is waiting for your response");
72 |     });
73 | 
74 |     it("returns empty string for other statuses", () => {
75 |       expect(getHumanInterventionGuidance("COMPLETED")).toBe("");
76 |       expect(getHumanInterventionGuidance("PROCESSING")).toBe("");
77 |     });
78 |   });
79 | });
80 | 
```

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

```typescript
 1 | import { parseSkills, SKILLS, type Skill } from "@sentry/mcp-core/skills";
 2 | import {
 3 |   validateAndParseSentryUrlThrows,
 4 |   validateOpenAiBaseUrlThrows,
 5 |   validateSentryHostThrows,
 6 | } from "@sentry/mcp-core/utils/url-utils";
 7 | import type { MergedArgs, ResolvedConfig } from "./types";
 8 | 
 9 | export function formatInvalidSkills(
10 |   invalid: string[],
11 |   envName?: string,
12 | ): string {
13 |   const where = envName ? `${envName} provided` : "Invalid skills provided";
14 |   const allSkills = Object.keys(SKILLS).join(", ");
15 |   return `Error: ${where}: ${invalid.join(", ")}\nAvailable skills: ${allSkills}`;
16 | }
17 | 
18 | export function finalize(input: MergedArgs): ResolvedConfig {
19 |   // Access token required
20 |   if (!input.accessToken) {
21 |     throw new Error(
22 |       "Error: No access token was provided. Pass one with `--access-token` or via `SENTRY_ACCESS_TOKEN`.",
23 |     );
24 |   }
25 | 
26 |   // Determine host from url/host with validation
27 |   let sentryHost = "sentry.io";
28 |   if (input.url) {
29 |     sentryHost = validateAndParseSentryUrlThrows(input.url);
30 |   } else if (input.host) {
31 |     validateSentryHostThrows(input.host);
32 |     sentryHost = input.host;
33 |   }
34 | 
35 |   // Skills resolution
36 |   //
37 |   // IMPORTANT: stdio (CLI) intentionally defaults to ALL skills when no --skills flag is provided
38 |   //
39 |   // This differs from the OAuth flow, which requires explicit user selection:
40 |   // - stdio/CLI: Non-interactive, defaults to ALL skills (inspect, docs, seer, triage, project-management)
41 |   // - OAuth: Interactive, requires user to explicitly select skills (with sensible defaults pre-checked)
42 |   //
43 |   // Rationale:
44 |   // We don't want the MCP to break if users don't specify skills. stdio is typically used in
45 |   // local development and CI/CD environments where maximum access by default is expected.
46 |   // OAuth is used in multi-tenant hosted environments where users should consciously grant
47 |   // permissions on a per-app basis.
48 |   //
49 |   // For OAuth validation that enforces minimum 1 skill selection, see:
50 |   // packages/mcp-cloudflare/src/server/oauth/routes/callback.ts (lines 234-248)
51 |   //
52 |   let finalSkills: Set<Skill> | undefined = undefined;
53 |   if (input.skills) {
54 |     // Override: use only the specified skills
55 |     const { valid, invalid } = parseSkills(input.skills);
56 |     if (invalid.length > 0) {
57 |       throw new Error(formatInvalidSkills(invalid));
58 |     }
59 |     if (valid.size === 0) {
60 |       throw new Error("Error: Invalid skills provided. No valid skills found.");
61 |     }
62 |     finalSkills = valid;
63 |   } else {
64 |     // Default: grant ALL skills when no flag is provided (see comment block above for rationale)
65 |     const allSkills = Object.keys(SKILLS) as Skill[];
66 |     finalSkills = new Set<Skill>(allSkills);
67 |   }
68 | 
69 |   const resolvedOpenAiBaseUrl = input.openaiBaseUrl
70 |     ? validateOpenAiBaseUrlThrows(input.openaiBaseUrl)
71 |     : undefined;
72 | 
73 |   return {
74 |     accessToken: input.accessToken,
75 |     sentryHost,
76 |     mcpUrl: input.mcpUrl,
77 |     sentryDsn: input.sentryDsn,
78 |     openaiBaseUrl: resolvedOpenAiBaseUrl,
79 |     openaiModel: input.openaiModel,
80 |     finalSkills,
81 |     organizationSlug: input.organizationSlug,
82 |     projectSlug: input.projectSlug,
83 |   };
84 | }
85 | 
```

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

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import { ApiNotFoundError, createApiError } from "../../api-client";
  3 | import { UserInputError } from "../../errors";
  4 | import { handleApiError, withApiErrorHandling } from "./api";
  5 | 
  6 | describe("handleApiError", () => {
  7 |   it("converts 404 errors with params to list all parameters", () => {
  8 |     const error = new ApiNotFoundError("Not Found");
  9 | 
 10 |     expect(() =>
 11 |       handleApiError(error, {
 12 |         organizationSlug: "my-org",
 13 |         issueId: "PROJ-123",
 14 |       }),
 15 |     ).toThrow(UserInputError);
 16 | 
 17 |     expect(() =>
 18 |       handleApiError(error, {
 19 |         organizationSlug: "my-org",
 20 |         issueId: "PROJ-123",
 21 |       }),
 22 |     ).toThrow(
 23 |       "Resource not found (404): Not Found\nPlease verify these parameters are correct:\n  - organizationSlug: 'my-org'\n  - issueId: 'PROJ-123'",
 24 |     );
 25 |   });
 26 | 
 27 |   it("converts 404 errors with multiple params including nullish values", () => {
 28 |     const error = new ApiNotFoundError("Not Found");
 29 | 
 30 |     expect(() =>
 31 |       handleApiError(error, {
 32 |         organizationSlug: "my-org",
 33 |         projectSlug: "my-project",
 34 |         query: undefined,
 35 |         sortBy: null,
 36 |         limit: 0,
 37 |         emptyString: "",
 38 |       }),
 39 |     ).toThrow(
 40 |       "Resource not found (404): Not Found\nPlease verify these parameters are correct:\n  - organizationSlug: 'my-org'\n  - projectSlug: 'my-project'\n  - limit: '0'",
 41 |     );
 42 |   });
 43 | 
 44 |   it("converts 404 errors with no params to generic message", () => {
 45 |     const error = new ApiNotFoundError("Not Found");
 46 | 
 47 |     expect(() => handleApiError(error, {})).toThrow(
 48 |       "API error (404): Not Found",
 49 |     );
 50 |   });
 51 | 
 52 |   it("converts 400 errors to UserInputError", () => {
 53 |     const error = createApiError("Invalid parameters", 400);
 54 | 
 55 |     expect(() => handleApiError(error)).toThrow(UserInputError);
 56 | 
 57 |     expect(() => handleApiError(error)).toThrow(
 58 |       "API error (400): Invalid parameters",
 59 |     );
 60 |   });
 61 | 
 62 |   it("converts 403 errors to UserInputError with access message", () => {
 63 |     const error = createApiError("Forbidden", 403);
 64 | 
 65 |     expect(() => handleApiError(error)).toThrow("API error (403): Forbidden");
 66 |   });
 67 | 
 68 |   it("re-throws non-API errors unchanged", () => {
 69 |     const error = new Error("Network error");
 70 | 
 71 |     expect(() => handleApiError(error)).toThrow(error);
 72 |   });
 73 | });
 74 | 
 75 | describe("withApiErrorHandling", () => {
 76 |   it("returns successful results unchanged", async () => {
 77 |     const result = await withApiErrorHandling(
 78 |       async () => ({ id: "123", title: "Test Issue" }),
 79 |       { issueId: "PROJ-123" },
 80 |     );
 81 | 
 82 |     expect(result).toEqual({ id: "123", title: "Test Issue" });
 83 |   });
 84 | 
 85 |   it("handles errors through handleApiError", async () => {
 86 |     const error = new ApiNotFoundError("Not Found");
 87 | 
 88 |     await expect(
 89 |       withApiErrorHandling(
 90 |         async () => {
 91 |           throw error;
 92 |         },
 93 |         {
 94 |           organizationSlug: "my-org",
 95 |           issueId: "PROJ-123",
 96 |         },
 97 |       ),
 98 |     ).rejects.toThrow(
 99 |       "Resource not found (404): Not Found\nPlease verify these parameters are correct:\n  - organizationSlug: 'my-org'\n  - issueId: 'PROJ-123'",
100 |     );
101 |   });
102 | });
103 | 
```

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

```bash
  1 | #!/bin/bash
  2 | 
  3 | # Benchmark script for comparing direct vs agent mode performance
  4 | # Usage: ./benchmark-agent.sh [iterations]
  5 | 
  6 | ITERATIONS=${1:-10}
  7 | QUERY="what organizations do I have access to?"
  8 | 
  9 | echo "=========================================="
 10 | echo "MCP Agent Performance Benchmark"
 11 | echo "=========================================="
 12 | echo "Query: $QUERY"
 13 | echo "Iterations: $ITERATIONS"
 14 | echo ""
 15 | 
 16 | # Arrays to store timings
 17 | declare -a direct_times
 18 | declare -a agent_times
 19 | 
 20 | echo "Running direct mode tests..."
 21 | for i in $(seq 1 $ITERATIONS); do
 22 |     echo -n "  Run $i/$ITERATIONS... "
 23 | 
 24 |     # Run and capture timing (real time in seconds)
 25 |     START=$(date +%s.%N)
 26 |     pnpm -w run cli "$QUERY" > /dev/null 2>&1
 27 |     END=$(date +%s.%N)
 28 | 
 29 |     # Calculate duration
 30 |     DURATION=$(echo "$END - $START" | bc)
 31 |     direct_times+=($DURATION)
 32 | 
 33 |     echo "${DURATION}s"
 34 | done
 35 | 
 36 | echo ""
 37 | echo "Running agent mode tests..."
 38 | for i in $(seq 1 $ITERATIONS); do
 39 |     echo -n "  Run $i/$ITERATIONS... "
 40 | 
 41 |     # Run and capture timing
 42 |     START=$(date +%s.%N)
 43 |     pnpm -w run cli --agent "$QUERY" > /dev/null 2>&1
 44 |     END=$(date +%s.%N)
 45 | 
 46 |     # Calculate duration
 47 |     DURATION=$(echo "$END - $START" | bc)
 48 |     agent_times+=($DURATION)
 49 | 
 50 |     echo "${DURATION}s"
 51 | done
 52 | 
 53 | echo ""
 54 | echo "=========================================="
 55 | echo "Results"
 56 | echo "=========================================="
 57 | 
 58 | # Calculate statistics for direct mode
 59 | direct_sum=0
 60 | direct_min=${direct_times[0]}
 61 | direct_max=${direct_times[0]}
 62 | for time in "${direct_times[@]}"; do
 63 |     direct_sum=$(echo "$direct_sum + $time" | bc)
 64 |     if (( $(echo "$time < $direct_min" | bc -l) )); then
 65 |         direct_min=$time
 66 |     fi
 67 |     if (( $(echo "$time > $direct_max" | bc -l) )); then
 68 |         direct_max=$time
 69 |     fi
 70 | done
 71 | direct_avg=$(echo "scale=2; $direct_sum / $ITERATIONS" | bc)
 72 | 
 73 | # Calculate statistics for agent mode
 74 | agent_sum=0
 75 | agent_min=${agent_times[0]}
 76 | agent_max=${agent_times[0]}
 77 | for time in "${agent_times[@]}"; do
 78 |     agent_sum=$(echo "$agent_sum + $time" | bc)
 79 |     if (( $(echo "$time < $agent_min" | bc -l) )); then
 80 |         agent_min=$time
 81 |     fi
 82 |     if (( $(echo "$time > $agent_max" | bc -l) )); then
 83 |         agent_max=$time
 84 |     fi
 85 | done
 86 | agent_avg=$(echo "scale=2; $agent_sum / $ITERATIONS" | bc)
 87 | 
 88 | # Calculate difference
 89 | diff=$(echo "scale=2; $agent_avg - $direct_avg" | bc)
 90 | percent=$(echo "scale=1; ($agent_avg - $direct_avg) / $direct_avg * 100" | bc)
 91 | 
 92 | echo ""
 93 | echo "Direct Mode:"
 94 | echo "  Min:     ${direct_min}s"
 95 | echo "  Max:     ${direct_max}s"
 96 | echo "  Average: ${direct_avg}s"
 97 | echo ""
 98 | echo "Agent Mode:"
 99 | echo "  Min:     ${agent_min}s"
100 | echo "  Max:     ${agent_max}s"
101 | echo "  Average: ${agent_avg}s"
102 | echo ""
103 | echo "Difference:"
104 | if (( $(echo "$diff > 0" | bc -l) )); then
105 |   echo "  +${diff}s (${percent}% slower)"
106 | elif (( $(echo "$diff < 0" | bc -l) )); then
107 |   abs_diff=$(echo "scale=2; -1 * $diff" | bc)
108 |   abs_percent=$(echo "scale=1; -1 * $percent" | bc)
109 |   echo "  -${abs_diff}s (${abs_percent}% faster)"
110 | else
111 |   echo "  No difference (0%)"
112 | fi
113 | echo ""
114 | 
115 | # Show all individual results
116 | echo "All timings:"
117 | echo "  Direct: ${direct_times[*]}"
118 | echo "  Agent:  ${agent_times[*]}"
119 | 
```

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

```typescript
 1 | /**
 2 |  * Markdown component that makes slash commands clickable
 3 |  */
 4 | import ReactMarkdown from "react-markdown";
 5 | import remarkGfm from "remark-gfm";
 6 | import { cn } from "@/client/lib/utils";
 7 | import { Markdown } from "./markdown";
 8 | 
 9 | interface InteractiveMarkdownProps {
10 |   children: string;
11 |   className?: string;
12 |   hasSlashCommands?: boolean;
13 |   onSlashCommand?: (command: string) => void;
14 | }
15 | 
16 | export function InteractiveMarkdown({
17 |   children,
18 |   className,
19 |   hasSlashCommands,
20 |   onSlashCommand,
21 | }: InteractiveMarkdownProps) {
22 |   // If this content has slash commands and we have a handler, create custom renderer
23 |   if (hasSlashCommands && onSlashCommand) {
24 |     return (
25 |       <ReactMarkdown
26 |         className={cn(
27 |           "prose prose-invert prose-slate max-w-none",
28 |           "prose-p:my-2 prose-p:leading-relaxed",
29 |           "prose-pre:bg-slate-900 prose-pre:border prose-pre:border-slate-700",
30 |           "prose-code:bg-slate-800 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm",
31 |           "prose-code:before:content-none prose-code:after:content-none",
32 |           "prose-strong:text-slate-100",
33 |           "prose-em:text-slate-200",
34 |           "prose-a:text-violet-300",
35 |           "prose-blockquote:border-l-violet-500 prose-blockquote:bg-slate-800/50 prose-blockquote:py-2 prose-blockquote:px-4",
36 |           "prose-h1:text-slate-100 prose-h2:text-slate-100 prose-h3:text-slate-100",
37 |           "prose-h4:text-slate-100 prose-h5:text-slate-100 prose-h6:text-slate-100",
38 |           "prose-ul:my-2 prose-ol:my-2",
39 |           "prose-li:my-1",
40 |           "prose-hr:border-slate-700",
41 |           "prose-table:border-slate-700",
42 |           "prose-th:border-slate-700 prose-td:border-slate-700",
43 |           className,
44 |         )}
45 |         remarkPlugins={[remarkGfm]}
46 |         disallowedElements={["script", "style", "iframe", "object", "embed"]}
47 |         unwrapDisallowed={true}
48 |         components={{
49 |           // Custom renderer for code that might contain slash commands
50 |           code: ({ children, ref, ...props }) => {
51 |             const text = String(children);
52 |             if (text.startsWith("/") && text.match(/^\/[a-zA-Z]+$/)) {
53 |               // This is a slash command, make it clickable
54 |               const command = text.slice(1);
55 |               return (
56 |                 <button
57 |                   onClick={() => onSlashCommand(command)}
58 |                   className="inline-flex items-center gap-1 px-1 py-0.5 text-xs bg-blue-900/50 border border-blue-700/50 rounded text-blue-300 hover:bg-blue-800/50 hover:border-blue-600/50 transition-colors font-mono cursor-pointer"
59 |                   type="button"
60 |                   {...props}
61 |                 >
62 |                   {text}
63 |                 </button>
64 |               );
65 |             }
66 |             // Regular code rendering
67 |             return (
68 |               <code ref={ref as any} {...props}>
69 |                 {children}
70 |               </code>
71 |             );
72 |           },
73 |         }}
74 |       >
75 |         {children}
76 |       </ReactMarkdown>
77 |     );
78 |   }
79 | 
80 |   // Otherwise, render as normal markdown
81 |   return <Markdown className={className}>{children}</Markdown>;
82 | }
83 | 
```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/animation/browser-ui/IssueDetails.tsx:
--------------------------------------------------------------------------------

```typescript
 1 | import { ChevronDown } from "lucide-react";
 2 | 
 3 | export default function IssueDetails({ step }: { step: number }) {
 4 |   return (
 5 |     <>
 6 |       <div
 7 |         className={`${
 8 |           step === 1 ? "opacity-100" : step > 1 ? "opacity-0" : "opacity-40"
 9 |         } rounded-xl border border-white/0 bg-white/0 duration-300`}
10 |         id="stack-trace-container"
11 |       >
12 |         <div className="w-full border-white/5 flex justify-between items-center border-b bg-white/0 p-3">
13 |           Highlights
14 |           <ChevronDown className="h-5 w-5 text-white/50" />
15 |         </div>
16 |         <div className="w-full p-3 flex items-center justify-between">
17 |           Stack Trace
18 |           <ChevronDown className="h-5 w-5 text-white/50 -scale-y-100" />
19 |         </div>
20 |         <div className="relative w-[calc(100%-1rem)] m-2 border border-white/10 bg-white/5 rounded-xl">
21 |           <div
22 |             className={`${
23 |               step === 1
24 |                 ? "motion-reduce:opacity-0 motion-reduce:duration-1000 motion-reduce:delay-800 motion-reduce:!animate-none animate-issue-context opacity-30"
25 |                 : "opacity-0"
26 |             } pb-4 rounded-xl absolute inset-0 border border-white/20 bg-pink-900 text-pink100`}
27 |             style={{ ["--delay" as any]: "0.8s" }}
28 |           >
29 |             <div className="h-full w-full rounded-xl border border-white/20 bg-white/10 pb-4" />
30 |           </div>
31 |           <div
32 |             className={`${
33 |               step === 1
34 |                 ? "motion-reduce:opacity-0 motion-reduce:duration-1000 motion-reduce:delay-1000 motion-reduce:!animate-none animate-issue-context opacity-30"
35 |                 : "opacity-0"
36 |             } pb-4 rounded-xl absolute inset-0 border border-white/20 bg-pink-900 text-pink100`}
37 |             style={{ ["--delay" as any]: "1s" }}
38 |           >
39 |             <div className="h-full w-full rounded-xl border border-white/20 bg-white/10 pb-4" />
40 |           </div>
41 |           <div
42 |             className={`${
43 |               step === 1
44 |                 ? "motion-reduce:opacity-0 motion-reduce:duration-1000 motion-reduce:delay-1200 motion-reduce:!animate-none animate-issue-context opacity-30"
45 |                 : "opacity-0"
46 |             } pb-4 rounded-xl absolute inset-0 border border-white/20 bg-pink-900 text-pink100`}
47 |             style={{ ["--delay" as any]: "1.2s" }}
48 |           >
49 |             <div className="h-full w-full rounded-xl border border-white/20 bg-white/10 pb-4" />
50 |           </div>
51 |           <div
52 |             className={`${
53 |               step === 1
54 |                 ? "motion-reduce:opacity-0 motion-reduce:duration-1000 motion-reduce:delay-675 motion-reduce:!animate-none animate-issue-context"
55 |                 : step > 1
56 |                   ? "opacity-0"
57 |                   : "opacity-100"
58 |             } pb-4 rounded-xl border border-white/20 bg-pink-900 text-pink100`}
59 |             style={{ ["--delay" as any]: "0.675s" }}
60 |           >
61 |             <div className="h-full w-full rounded-xl border border-white/20 bg-white/10 pb-4">
62 |               <pre>
63 |                 {`
64 |   Error: Something went wrong
65 |     at main.js:123
66 |     at index.js:456`}
67 |               </pre>
68 |             </div>
69 |           </div>
70 |         </div>
71 |       </div>
72 |     </>
73 |   );
74 | }
75 | 
```

--------------------------------------------------------------------------------
/packages/mcp-core/src/internal/agents/openai-provider.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from "vitest";
  2 | import { getOpenAIModel, setOpenAIBaseUrl } from "./openai-provider.js";
  3 | 
  4 | describe("openai-provider", () => {
  5 |   const originalEnv = process.env.OPENAI_REASONING_EFFORT;
  6 | 
  7 |   beforeEach(() => {
  8 |     setOpenAIBaseUrl(undefined);
  9 |   });
 10 | 
 11 |   afterEach(() => {
 12 |     if (originalEnv === undefined) {
 13 |       // biome-ignore lint/performance/noDelete: Required to properly unset environment variable
 14 |       delete process.env.OPENAI_REASONING_EFFORT;
 15 |     } else {
 16 |       process.env.OPENAI_REASONING_EFFORT = originalEnv;
 17 |     }
 18 |   });
 19 | 
 20 |   describe("reasoning effort configuration", () => {
 21 |     it("uses default reasoning effort when env var is not set", () => {
 22 |       // biome-ignore lint/performance/noDelete: Required to properly unset environment variable
 23 |       delete process.env.OPENAI_REASONING_EFFORT;
 24 | 
 25 |       const model = getOpenAIModel();
 26 | 
 27 |       // The model object should be created with default reasoning effort
 28 |       expect(model).toBeDefined();
 29 |       expect(model.modelId).toBe("gpt-5");
 30 |     });
 31 | 
 32 |     it("disables reasoning effort when env var is empty string", () => {
 33 |       process.env.OPENAI_REASONING_EFFORT = "";
 34 | 
 35 |       const model = getOpenAIModel();
 36 | 
 37 |       // The model object should be created without reasoning effort
 38 |       expect(model).toBeDefined();
 39 |       expect(model.modelId).toBe("gpt-5");
 40 |     });
 41 | 
 42 |     it("uses specified reasoning effort when env var is set", () => {
 43 |       process.env.OPENAI_REASONING_EFFORT = "high";
 44 | 
 45 |       const model = getOpenAIModel();
 46 | 
 47 |       // The model object should be created with high reasoning effort
 48 |       expect(model).toBeDefined();
 49 |       expect(model.modelId).toBe("gpt-5");
 50 |     });
 51 | 
 52 |     it("throws error for invalid reasoning effort value", () => {
 53 |       process.env.OPENAI_REASONING_EFFORT = "invalid";
 54 | 
 55 |       expect(() => getOpenAIModel()).toThrow(
 56 |         'Invalid OPENAI_REASONING_EFFORT value: "invalid". Must be one of: "low", "medium", "high", or "" (empty string to disable). Default is "low".',
 57 |       );
 58 |     });
 59 |   });
 60 | 
 61 |   describe("base URL configuration", () => {
 62 |     it("uses default base URL when not configured", () => {
 63 |       const model = getOpenAIModel();
 64 | 
 65 |       expect(model).toBeDefined();
 66 |       expect(model.modelId).toBe("gpt-5");
 67 |     });
 68 | 
 69 |     it("uses configured base URL", () => {
 70 |       setOpenAIBaseUrl("https://custom-openai.example.com");
 71 | 
 72 |       const model = getOpenAIModel();
 73 | 
 74 |       expect(model).toBeDefined();
 75 |       expect(model.modelId).toBe("gpt-5");
 76 |     });
 77 |   });
 78 | 
 79 |   describe("model override", () => {
 80 |     it("uses default model when not specified", () => {
 81 |       const model = getOpenAIModel();
 82 | 
 83 |       expect(model.modelId).toBe("gpt-5");
 84 |     });
 85 | 
 86 |     it("uses specified model when provided", () => {
 87 |       const model = getOpenAIModel("gpt-4");
 88 | 
 89 |       expect(model.modelId).toBe("gpt-4");
 90 |     });
 91 | 
 92 |     it("uses OPENAI_MODEL env var when set", () => {
 93 |       process.env.OPENAI_MODEL = "gpt-4o";
 94 | 
 95 |       const model = getOpenAIModel();
 96 | 
 97 |       expect(model.modelId).toBe("gpt-4o");
 98 | 
 99 |       // biome-ignore lint/performance/noDelete: Required to properly unset environment variable
100 |       delete process.env.OPENAI_MODEL;
101 |     });
102 |   });
103 | });
104 | 
```

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

```typescript
 1 | import { defineTool } from "../internal/tool-helpers/define";
 2 | import { apiServiceFromContext } from "../internal/tool-helpers/api";
 3 | import type { ServerContext } from "../types";
 4 | import { ParamSearchQuery } from "../schema";
 5 | import { ALL_SKILLS } from "../skills";
 6 | 
 7 | const RESULT_LIMIT = 25;
 8 | 
 9 | export default defineTool({
10 |   name: "find_organizations",
11 |   skills: ALL_SKILLS, // Foundational tool - available to all skills
12 |   requiredScopes: ["org:read"],
13 |   description: [
14 |     "Find organizations that the user has access to in Sentry.",
15 |     "",
16 |     "Use this tool when you need to:",
17 |     "- View organizations in Sentry",
18 |     "- Find an organization's slug to aid other tool requests",
19 |     "- Search for specific organizations by name or slug",
20 |     "",
21 |     `Returns up to ${RESULT_LIMIT} results. If you hit this limit, use the query parameter to narrow down results.`,
22 |   ].join("\n"),
23 |   inputSchema: {
24 |     query: ParamSearchQuery.nullable().default(null),
25 |   },
26 |   annotations: {
27 |     readOnlyHint: true,
28 |     openWorldHint: true,
29 |   },
30 |   async handler(params, context: ServerContext) {
31 |     // User data endpoints (like /users/me/regions/) should never use regionUrl
32 |     // as they must always query the main API server, not region-specific servers
33 |     const apiService = apiServiceFromContext(context);
34 |     const organizations = await apiService.listOrganizations({
35 |       query: params.query ?? undefined,
36 |     });
37 | 
38 |     let output = "# Organizations\n\n";
39 | 
40 |     if (params.query) {
41 |       output += `**Search query:** "${params.query}"\n\n`;
42 |     }
43 | 
44 |     if (organizations.length === 0) {
45 |       output += params.query
46 |         ? `No organizations found matching "${params.query}".\n`
47 |         : "You don't appear to be a member of any organizations.\n";
48 |       return output;
49 |     }
50 | 
51 |     output += organizations
52 |       .map((org) =>
53 |         [
54 |           `## **${org.slug}**`,
55 |           "",
56 |           `**Web URL:** ${org.links?.organizationUrl || "Not available"}`,
57 |           `**Region URL:** ${org.links?.regionUrl || ""}`,
58 |         ].join("\n"),
59 |       )
60 |       .join("\n\n");
61 | 
62 |     if (organizations.length === RESULT_LIMIT) {
63 |       output += `\n\n---\n\n**Note:** Showing ${RESULT_LIMIT} results (maximum). There may be more organizations available. Use the \`query\` parameter to search for specific organizations.`;
64 |     }
65 | 
66 |     output += "\n\n# Using this information\n\n";
67 |     output += `- The organization's name is the identifier for the organization, and is used in many tools for \`organizationSlug\`.\n`;
68 | 
69 |     const hasValidRegionUrls = organizations.some((org) =>
70 |       org.links?.regionUrl?.trim(),
71 |     );
72 | 
73 |     if (hasValidRegionUrls) {
74 |       output += `- If a tool supports passing in the \`regionUrl\`, you MUST pass in the correct value shown above for each organization.\n`;
75 |       output += `- For Sentry's Cloud Service (sentry.io), always use the regionUrl to ensure requests go to the correct region.\n`;
76 |     } else {
77 |       output += `- This appears to be a self-hosted Sentry installation. You can omit the \`regionUrl\` parameter when using other tools.\n`;
78 |       output += `- For self-hosted Sentry, the regionUrl is typically empty and not needed for API calls.\n`;
79 |     }
80 | 
81 |     return output;
82 |   },
83 | });
84 | 
```

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

```typescript
  1 | import { Chat } from "./components/chat";
  2 | import { useAuth } from "./contexts/auth-context";
  3 | import { useState, useEffect } from "react";
  4 | 
  5 | import { Header } from "./components/ui/header";
  6 | import { HeaderDivider } from "./components/hero/header-divider";
  7 | import { Sidebars } from "./components/home-layout/sidebars";
  8 | 
  9 | import HeroBlock from "./components/hero/hero-block";
 10 | import UseCases from "./components/usecases";
 11 | import GettingStarted from "./components/getting-started";
 12 | 
 13 | import TableOfContents from "./components/docs/toc";
 14 | 
 15 | import Footer from "./components/home-layout/footer";
 16 | 
 17 | export default function App() {
 18 |   const { isAuthenticated, handleLogout } = useAuth();
 19 | 
 20 |   const [isChatOpen, setIsChatOpen] = useState(() => {
 21 |     // Initialize based on URL query string only to avoid hydration issues
 22 |     const urlParams = new URLSearchParams(window.location.search);
 23 |     const hasQueryParam = urlParams.has("chat");
 24 | 
 25 |     if (hasQueryParam) {
 26 |       return urlParams.get("chat") !== "0";
 27 |     }
 28 | 
 29 |     // default to false for mobile and to avoid scroll lock on desktop
 30 |     return false;
 31 |   });
 32 | 
 33 |   // Adjust initial state for mobile after component mounts
 34 |   useEffect(() => {
 35 |     const urlParams = new URLSearchParams(window.location.search);
 36 | 
 37 |     // Only adjust state if no URL parameter exists and we're on mobile
 38 |     if (!urlParams.has("chat") && window.innerWidth < 768) {
 39 |       setIsChatOpen(false);
 40 |     }
 41 |   }, []);
 42 | 
 43 |   // Update URL when chat state changes
 44 |   const toggleChat = (open: boolean) => {
 45 |     setIsChatOpen(open);
 46 | 
 47 |     if (open) {
 48 |       // Add ?chat to URL
 49 |       const newUrl = new URL(window.location.href);
 50 |       newUrl.searchParams.set("chat", "1");
 51 |       window.history.pushState({}, "", newUrl.toString());
 52 |     } else {
 53 |       // Remove query string for home page
 54 |       const newUrl = new URL(window.location.href);
 55 |       newUrl.search = "";
 56 |       window.history.pushState({}, "", newUrl.toString());
 57 |     }
 58 |   };
 59 | 
 60 |   // Handle browser back/forward navigation
 61 |   useEffect(() => {
 62 |     const handlePopState = () => {
 63 |       const urlParams = new URLSearchParams(window.location.search);
 64 |       const hasQueryParam = urlParams.has("chat");
 65 | 
 66 |       if (hasQueryParam) {
 67 |         setIsChatOpen(urlParams.get("chat") !== "0");
 68 |       } else {
 69 |         // default to closed on both desktop and mobile
 70 |         setIsChatOpen(false);
 71 |       }
 72 |     };
 73 | 
 74 |     window.addEventListener("popstate", handlePopState);
 75 |     return () => window.removeEventListener("popstate", handlePopState);
 76 |   }, []);
 77 | 
 78 |   return (
 79 |     <div className="overflow-x-clip max-w-screen relative">
 80 |       {/* //!NOTE: order matters for z- */}
 81 |       <Sidebars isChatOpen={isChatOpen} toggleChat={toggleChat} />
 82 |       <Header toggleChat={toggleChat} isChatOpen={isChatOpen} />
 83 |       <HeaderDivider />
 84 | 
 85 |       <HeroBlock />
 86 |       <UseCases />
 87 |       <GettingStarted />
 88 | 
 89 |       {/* main content */}
 90 |       <div className="relative container mx-auto">
 91 |         <aside className="max-xl:hidden absolute h-full right-15 inset-y-0">
 92 |           <TableOfContents />
 93 |         </aside>
 94 |       </div>
 95 | 
 96 |       <Chat
 97 |         isOpen={isChatOpen}
 98 |         onClose={() => toggleChat(false)}
 99 |         onLogout={handleLogout}
100 |       />
101 | 
102 |       <Footer isChatOpen={isChatOpen} />
103 |     </div>
104 |   );
105 | }
106 | 
```

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

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import { parseArgv, parseEnv, merge } from "./parse";
  3 | 
  4 | describe("cli/parseArgv", () => {
  5 |   it("parses known flags and short aliases", () => {
  6 |     const parsed = parseArgv([
  7 |       "--access-token=tok",
  8 |       "--host=sentry.io",
  9 |       "--url=https://example.com",
 10 |       "--mcp-url=https://mcp.example.com",
 11 |       "--sentry-dsn=dsn",
 12 |       "--openai-base-url=https://api.example.com/v1",
 13 |       "--skills=inspect,triage",
 14 |       "-h",
 15 |       "-v",
 16 |     ]);
 17 |     expect(parsed.accessToken).toBe("tok");
 18 |     expect(parsed.host).toBe("sentry.io");
 19 |     expect(parsed.url).toBe("https://example.com");
 20 |     expect(parsed.mcpUrl).toBe("https://mcp.example.com");
 21 |     expect(parsed.sentryDsn).toBe("dsn");
 22 |     expect(parsed.openaiBaseUrl).toBe("https://api.example.com/v1");
 23 |     expect(parsed.skills).toBe("inspect,triage");
 24 |     expect(parsed.help).toBe(true);
 25 |     expect(parsed.version).toBe(true);
 26 |     expect(parsed.unknownArgs).toEqual([]);
 27 |   });
 28 | 
 29 |   it("parses skills flags", () => {
 30 |     const parsed = parseArgv(["--access-token=tok", "--skills=inspect,triage"]);
 31 |     expect(parsed.accessToken).toBe("tok");
 32 |     expect(parsed.skills).toBe("inspect,triage");
 33 |   });
 34 | 
 35 |   it("collects unknown args", () => {
 36 |     const parsed = parseArgv(["--unknown", "--another=1"]);
 37 |     expect(parsed.unknownArgs.length).toBeGreaterThan(0);
 38 |   });
 39 | });
 40 | 
 41 | describe("cli/parseEnv", () => {
 42 |   it("parses environment variables including skills", () => {
 43 |     const env = parseEnv({
 44 |       SENTRY_ACCESS_TOKEN: "envtok",
 45 |       SENTRY_HOST: "envhost",
 46 |       MCP_URL: "envmcp",
 47 |       SENTRY_DSN: "envdsn",
 48 |       MCP_SKILLS: "inspect,triage",
 49 |     } as any);
 50 |     expect(env.accessToken).toBe("envtok");
 51 |     expect(env.host).toBe("envhost");
 52 |     expect(env.mcpUrl).toBe("envmcp");
 53 |     expect(env.sentryDsn).toBe("envdsn");
 54 |     expect(env.skills).toBe("inspect,triage");
 55 |   });
 56 | });
 57 | 
 58 | describe("cli/merge", () => {
 59 |   it("applies precedence: CLI over env", () => {
 60 |     const env = parseEnv({
 61 |       SENTRY_ACCESS_TOKEN: "envtok",
 62 |       SENTRY_HOST: "envhost",
 63 |       MCP_URL: "envmcp",
 64 |       SENTRY_DSN: "envdsn",
 65 |     } as any);
 66 |     const cli = parseArgv([
 67 |       "--access-token=clitok",
 68 |       "--host=clihost",
 69 |       "--mcp-url=climcp",
 70 |       "--sentry-dsn=clidsn",
 71 |       "--openai-base-url=https://api.cli/v1",
 72 |     ]);
 73 |     const merged = merge(cli, env);
 74 |     expect(merged.accessToken).toBe("clitok");
 75 |     expect(merged.host).toBe("clihost");
 76 |     expect(merged.mcpUrl).toBe("climcp");
 77 |     expect(merged.sentryDsn).toBe("clidsn");
 78 |     expect(merged.openaiBaseUrl).toBe("https://api.cli/v1");
 79 |   });
 80 | 
 81 |   it("applies precedence for skills: CLI over env", () => {
 82 |     const env = parseEnv({
 83 |       SENTRY_ACCESS_TOKEN: "envtok",
 84 |       MCP_SKILLS: "inspect",
 85 |     } as any);
 86 |     const cli = parseArgv(["--access-token=clitok", "--skills=inspect,triage"]);
 87 |     const merged = merge(cli, env);
 88 |     expect(merged.skills).toBe("inspect,triage");
 89 |   });
 90 | 
 91 |   it("falls back to env when CLI skills not provided", () => {
 92 |     const env = parseEnv({
 93 |       SENTRY_ACCESS_TOKEN: "envtok",
 94 |       MCP_SKILLS: "inspect,triage",
 95 |     } as any);
 96 |     const cli = parseArgv(["--access-token=clitok"]);
 97 |     const merged = merge(cli, env);
 98 |     expect(merged.skills).toBe("inspect,triage");
 99 |   });
100 | });
101 | 
```

--------------------------------------------------------------------------------
/packages/mcp-core/src/internal/agents/callEmbeddedAgent.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { generateText, Output, type Tool } from "ai";
  2 | import { getOpenAIModel } from "./openai-provider";
  3 | import { UserInputError } from "../../errors";
  4 | import type { z } from "zod";
  5 | 
  6 | export type ToolCall = {
  7 |   toolName: string;
  8 |   args: unknown;
  9 | };
 10 | 
 11 | interface EmbeddedAgentResult<T> {
 12 |   result: T;
 13 |   toolCalls: ToolCall[];
 14 | }
 15 | 
 16 | /**
 17 |  * Call an embedded agent with tool call capture
 18 |  * This is the standard way to call embedded AI agents within MCP tools
 19 |  *
 20 |  * Error handling:
 21 |  * - Errors are re-thrown for the calling agent to handle
 22 |  * - Each agent can implement its own error handling strategy
 23 |  */
 24 | export async function callEmbeddedAgent<
 25 |   TOutput,
 26 |   TSchema extends z.ZodType<TOutput, z.ZodTypeDef, unknown>,
 27 | >({
 28 |   system,
 29 |   prompt,
 30 |   tools,
 31 |   schema,
 32 | }: {
 33 |   system: string;
 34 |   prompt: string;
 35 |   tools: Record<string, Tool>;
 36 |   schema: TSchema;
 37 | }): Promise<EmbeddedAgentResult<TOutput>> {
 38 |   const capturedToolCalls: ToolCall[] = [];
 39 | 
 40 |   const result = await generateText({
 41 |     model: getOpenAIModel(), // Uses configured default model (gpt-5)
 42 |     system,
 43 |     prompt,
 44 |     tools,
 45 |     maxSteps: 5,
 46 |     temperature: 1, // GPT-5 only supports temperature of 1
 47 |     experimental_output: Output.object({ schema }),
 48 |     experimental_telemetry: {
 49 |       isEnabled: true,
 50 |       functionId: "callEmbeddedAgent",
 51 |     },
 52 |     // Disable strict schema validation for both output and tool parameter schemas.
 53 |     //
 54 |     // OpenAI's structured outputs have limitations:
 55 |     // - structuredOutputs: true (default) enforces strict mode for BOTH output and tool schemas
 56 |     // - This requires ALL properties to be in the "required" array, breaking .optional()/.nullable()
 57 |     //
 58 |     // By setting both to false:
 59 |     // - structuredOutputs: false - Disables strict mode for tool parameter schemas
 60 |     // - strictJsonSchema: false - Disables strict mode for output schema
 61 |     // - We still get valid JSON, just without the strict "all fields required" constraint
 62 |     //
 63 |     // See:
 64 |     // - Issue: https://github.com/getsentry/sentry-mcp/issues/623
 65 |     // - AI SDK docs: https://ai-sdk.dev/providers/ai-sdk-providers/openai#structuredoutputs
 66 |     // - OpenAI docs: https://platform.openai.com/docs/guides/structured-outputs
 67 |     providerOptions: {
 68 |       openai: {
 69 |         structuredOutputs: false,
 70 |         strictJsonSchema: false,
 71 |       },
 72 |     },
 73 |     onStepFinish: (event) => {
 74 |       if (event.toolCalls && event.toolCalls.length > 0) {
 75 |         for (const toolCall of event.toolCalls) {
 76 |           capturedToolCalls.push({
 77 |             toolName: toolCall.toolName,
 78 |             args: toolCall.args,
 79 |           });
 80 |         }
 81 |       }
 82 |     },
 83 |   });
 84 | 
 85 |   if (!result.experimental_output) {
 86 |     throw new Error("Failed to generate output");
 87 |   }
 88 | 
 89 |   const rawOutput = result.experimental_output;
 90 | 
 91 |   if (
 92 |     typeof rawOutput === "object" &&
 93 |     rawOutput !== null &&
 94 |     "error" in rawOutput &&
 95 |     typeof (rawOutput as { error?: unknown }).error === "string"
 96 |   ) {
 97 |     throw new UserInputError((rawOutput as { error: string }).error);
 98 |   }
 99 | 
100 |   const parsedResult = schema.safeParse(rawOutput);
101 | 
102 |   if (!parsedResult.success) {
103 |     throw new UserInputError(
104 |       `Invalid agent response: ${parsedResult.error.message}`,
105 |     );
106 |   }
107 | 
108 |   return {
109 |     result: parsedResult.data,
110 |     toolCalls: capturedToolCalls,
111 |   };
112 | }
113 | 
```

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

```typescript
  1 | import { describeEval } from "vitest-evals";
  2 | import { FIXTURES, NoOpTaskRunner, ToolPredictionScorer } from "./utils";
  3 | 
  4 | // Note: This eval requires OPENAI_API_KEY to be set in the environment
  5 | // The search_events tool uses the AI SDK to translate natural language queries
  6 | describeEval("search-events", {
  7 |   data: async () => {
  8 |     return [
  9 |       // Core test: Basic error event search
 10 |       {
 11 |         input: `Find database timeouts in ${FIXTURES.organizationSlug} from the last week`,
 12 |         expectedTools: [
 13 |           {
 14 |             name: "find_organizations",
 15 |             arguments: {},
 16 |           },
 17 |           {
 18 |             name: "search_events",
 19 |             arguments: {
 20 |               organizationSlug: FIXTURES.organizationSlug,
 21 |               naturalLanguageQuery: "database timeouts from the last week",
 22 |               dataset: "errors",
 23 |             },
 24 |           },
 25 |         ],
 26 |       },
 27 |       // Core test: Performance spans search
 28 |       {
 29 |         input: `Find slow API calls taking over 5 seconds in ${FIXTURES.organizationSlug}`,
 30 |         expectedTools: [
 31 |           {
 32 |             name: "find_organizations",
 33 |             arguments: {},
 34 |           },
 35 |           {
 36 |             name: "search_events",
 37 |             arguments: {
 38 |               organizationSlug: FIXTURES.organizationSlug,
 39 |               naturalLanguageQuery: "slow API calls taking over 5 seconds",
 40 |               dataset: "spans",
 41 |             },
 42 |           },
 43 |         ],
 44 |       },
 45 |       // Core test: Logs search
 46 |       {
 47 |         input: `Show me error logs from the last hour in ${FIXTURES.organizationSlug}`,
 48 |         expectedTools: [
 49 |           {
 50 |             name: "find_organizations",
 51 |             arguments: {},
 52 |           },
 53 |           {
 54 |             name: "search_events",
 55 |             arguments: {
 56 |               organizationSlug: FIXTURES.organizationSlug,
 57 |               naturalLanguageQuery: "error logs from the last hour",
 58 |               dataset: "logs",
 59 |             },
 60 |           },
 61 |         ],
 62 |       },
 63 |       // Core test: Project-specific search
 64 |       {
 65 |         input: `Show me authentication errors in ${FIXTURES.organizationSlug}/${FIXTURES.projectSlug}`,
 66 |         expectedTools: [
 67 |           {
 68 |             name: "find_organizations",
 69 |             arguments: {},
 70 |           },
 71 |           {
 72 |             name: "search_events",
 73 |             arguments: {
 74 |               organizationSlug: FIXTURES.organizationSlug,
 75 |               projectSlug: FIXTURES.projectSlug,
 76 |               naturalLanguageQuery: "authentication errors",
 77 |               dataset: "errors",
 78 |             },
 79 |           },
 80 |         ],
 81 |       },
 82 |       // Core test: Search with 'me' reference
 83 |       {
 84 |         input: `Show me errors affecting me in ${FIXTURES.organizationSlug}`,
 85 |         expectedTools: [
 86 |           {
 87 |             name: "find_organizations",
 88 |             arguments: {},
 89 |           },
 90 |           {
 91 |             name: "whoami",
 92 |             arguments: {},
 93 |           },
 94 |           {
 95 |             name: "search_events",
 96 |             arguments: {
 97 |               organizationSlug: FIXTURES.organizationSlug,
 98 |               naturalLanguageQuery: "errors affecting user.id:12345",
 99 |               dataset: "errors",
100 |             },
101 |           },
102 |         ],
103 |       },
104 |     ];
105 |   },
106 |   task: NoOpTaskRunner(),
107 |   scorers: [ToolPredictionScorer()],
108 |   threshold: 0.6,
109 |   timeout: 30000,
110 | });
111 | 
```

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

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

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

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

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

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

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

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

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

```typescript
  1 | /**
  2 |  * Reusable chat UI component
  3 |  * Extracts the common chat interface used in both mobile and desktop views
  4 |  */
  5 | 
  6 | import ScrollToBottom from "react-scroll-to-bottom";
  7 | import { Button } from "../ui/button";
  8 | import { ChatInput, ChatMessages } from ".";
  9 | import type { Message } from "ai/react";
 10 | 
 11 | // Constant empty function to avoid creating new instances on every render
 12 | const EMPTY_FUNCTION = () => {};
 13 | 
 14 | // Sample prompts for quick access
 15 | const SAMPLE_PROMPTS = [
 16 |   {
 17 |     label: "Help",
 18 |     prompt: "/help",
 19 |   },
 20 |   {
 21 |     label: "React SDK Usage",
 22 |     prompt: "Show me how to set up the React SDK for error monitoring",
 23 |   },
 24 |   {
 25 |     label: "Recent Issues",
 26 |     prompt: "What are my most recent issues?",
 27 |   },
 28 | ] as const;
 29 | 
 30 | interface ChatUIProps {
 31 |   messages: Message[];
 32 |   input: string;
 33 |   error?: Error | null;
 34 |   isChatLoading: boolean;
 35 |   isLocalStreaming?: boolean;
 36 |   isMessageStreaming?: (messageId: string) => boolean;
 37 |   isOpen?: boolean;
 38 |   onInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
 39 |   onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
 40 |   onStop?: () => void;
 41 |   onRetry?: () => void;
 42 |   onSlashCommand?: (command: string) => void;
 43 |   onSendPrompt?: (prompt: string) => void;
 44 | }
 45 | 
 46 | export const ChatUI = ({
 47 |   messages,
 48 |   input,
 49 |   error,
 50 |   isChatLoading,
 51 |   isLocalStreaming,
 52 |   isMessageStreaming,
 53 |   isOpen = true,
 54 |   onInputChange,
 55 |   onSubmit,
 56 |   onStop,
 57 |   onRetry,
 58 |   onSlashCommand,
 59 |   onSendPrompt,
 60 | }: ChatUIProps) => {
 61 |   return (
 62 |     <div className="h-full flex flex-col relative">
 63 |       {/* Chat Messages - Scrollable area */}
 64 |       <ScrollToBottom
 65 |         className="flex-1 mb-18 flex overflow-y-auto"
 66 |         scrollViewClassName="px-0"
 67 |         followButtonClassName="hidden"
 68 |         initialScrollBehavior="smooth"
 69 |       >
 70 |         <ChatMessages
 71 |           messages={messages}
 72 |           isChatLoading={isChatLoading}
 73 |           isLocalStreaming={isLocalStreaming}
 74 |           isMessageStreaming={isMessageStreaming}
 75 |           error={error}
 76 |           onRetry={onRetry}
 77 |           onSlashCommand={onSlashCommand}
 78 |         />
 79 |       </ScrollToBottom>
 80 | 
 81 |       {/* Chat Input - Always pinned at bottom */}
 82 |       <div className="py-4 px-6 bottom-0 left-0 right-0 absolute min-h-34 z-10">
 83 |         <div className="w-full [mask-image:linear-gradient(to_bottom,transparent,red_4.5rem)] pointer-events-none absolute bottom-0 left-0 h-full -z-10 backdrop-blur-md bg-gradient-to-t from-background/80 xl:from-background to-background/20 xl:to-[#160f2433]" />
 84 |         {/* Sample Prompt Buttons - Always visible above input */}
 85 |         {onSendPrompt && (
 86 |           <div className="mb-4 flex flex-wrap gap-2 justify-center xl:justify-end">
 87 |             {SAMPLE_PROMPTS.map((samplePrompt) => (
 88 |               <Button
 89 |                 key={samplePrompt.label}
 90 |                 type="button"
 91 |                 className="backdrop-blur"
 92 |                 onClick={() => onSendPrompt(samplePrompt.prompt)}
 93 |                 size="sm"
 94 |                 variant="outline"
 95 |               >
 96 |                 {samplePrompt.label}
 97 |               </Button>
 98 |             ))}
 99 |           </div>
100 |         )}
101 | 
102 |         <ChatInput
103 |           input={input}
104 |           isLoading={isChatLoading}
105 |           isOpen={isOpen}
106 |           onInputChange={onInputChange}
107 |           onSubmit={onSubmit}
108 |           onStop={onStop || EMPTY_FUNCTION}
109 |           onSlashCommand={onSlashCommand}
110 |         />
111 |       </div>
112 |     </div>
113 |   );
114 | };
115 | 
```

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

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

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/getting-started.tsx:
--------------------------------------------------------------------------------

```typescript
 1 | import { Button } from "./ui/button";
 2 | import { useState } from "react";
 3 | import RemoteSetup, { RemoteSetupTabs } from "./fragments/remote-setup";
 4 | import StdioSetup, { StdioSetupTabs } from "./fragments/stdio-setup";
 5 | import { Cable, Cloud } from "lucide-react";
 6 | 
 7 | export default function Integration() {
 8 |   const [stdio, setStdio] = useState(false);
 9 |   return (
10 |     <section
11 |       id="getting-started"
12 |       className="flex flex-col md:container mx-auto relative mb-12 -scroll-mt-8 border-b border-dashed border-white/20 max-w-full duration-300 will-change-contents"
13 |     >
14 |       <div className="absolute top-0 left-0 right-0 flex justify-start flex-col px-8 pt-4 pointer-events-none">
15 |         <div className="flex items-center text-xs bg-background-3 rounded-full p-1 sticky top-4 size-fit -translate-x-[1.5px] mx-auto z-20 border-[0.5px] border-violet-300/50 pointer-events-auto">
16 |           <Button
17 |             variant={!stdio ? "default" : "secondary"}
18 |             size="xs"
19 |             onClick={() => {
20 |               setStdio(false);
21 |               document
22 |                 .getElementById("getting-started")
23 |                 ?.scrollIntoView({ behavior: "smooth", block: "start" });
24 |               // preserve current query string, only change the hash
25 |               const url = new URL(window.location.href);
26 |               url.hash = "#getting-started";
27 |               window.history.pushState(
28 |                 window.history.state,
29 |                 "",
30 |                 url.toString(),
31 |               );
32 |             }}
33 |             className={`${!stdio && "shadow-sm"} rounded-full !pr-3 !pl-2`}
34 |           >
35 |             <Cloud className="size-4 fill-current" />
36 |             Cloud
37 |           </Button>
38 |           <Button
39 |             variant={stdio ? "default" : "secondary"}
40 |             size="xs"
41 |             onClick={() => {
42 |               setStdio(true);
43 |               document
44 |                 .getElementById("getting-started")
45 |                 ?.scrollIntoView({ behavior: "smooth", block: "start" });
46 |               // preserve current query string, only change the hash
47 |               const url = new URL(window.location.href);
48 |               url.hash = "#getting-started";
49 |               window.history.pushState(
50 |                 window.history.state,
51 |                 "",
52 |                 url.toString(),
53 |               );
54 |             }}
55 |             className={`${stdio && "shadow-sm"} rounded-full !pr-3 !pl-2`}
56 |           >
57 |             <Cable className="size-4" />
58 |             Stdio
59 |           </Button>
60 |         </div>
61 |       </div>
62 | 
63 |       <div className="px-4 sm:px-8 pt-4 sm:pt-8 pb-4 border-b border-dashed border-white/20">
64 |         {/* Client installation tabs first */}
65 |         <div className="bg-dots bg-fixed p-4 sm:p-12 flex items-start justify-center mb-4 border border-dashed border-white/10 rounded-lg">
66 |           {!stdio ? <RemoteSetupTabs /> : <StdioSetupTabs />}
67 |         </div>
68 |       </div>
69 | 
70 |       <div className="px-4 sm:px-8 pt-4 sm:pt-8 pb-4">
71 |         {/* Advanced options after */}
72 |         <div className="relative min-h-0">
73 |           {!stdio ? (
74 |             <div
75 |               key="cloud"
76 |               className="animate-in fade-in motion-safe:slide-in-from-left-4 duration-300"
77 |             >
78 |               <RemoteSetup />
79 |             </div>
80 |           ) : (
81 |             <div
82 |               key="stdio-self-hosted"
83 |               className="animate-in fade-in motion-safe:slide-in-from-right-4 duration-300"
84 |             >
85 |               <StdioSetup />
86 |             </div>
87 |           )}
88 |         </div>
89 |       </div>
90 |     </section>
91 |   );
92 | }
93 | 
```

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

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

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

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

--------------------------------------------------------------------------------
/packages/mcp-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 | 
```
Page 4/20FirstPrevNextLast