#
tokens: 49868/50000 26/501 files (page 6/16)
lines: off (toggle) GitHub
raw markdown copy
This is page 6 of 16. Use http://codebase.md/getsentry/sentry-mcp?page={x} to view the full context.

# Directory Structure

```
├── .claude
│   ├── agents
│   │   └── claude-optimizer.md
│   ├── commands
│   │   ├── gh-pr.md
│   │   └── gh-review.md
│   └── settings.json
├── .craft.yml
├── .cursor
│   └── mcp.json
├── .env.example
├── .github
│   └── workflows
│       ├── deploy.yml
│       ├── eval.yml
│       ├── merge-jobs.yml
│       ├── release.yml
│       ├── smoke-tests.yml
│       ├── test.yml
│       └── token-cost.yml
├── .gitignore
├── .mcp.json
├── .vscode
│   ├── extensions.json
│   ├── mcp.json
│   └── settings.json
├── AGENTS.md
├── benchmark-agent.sh
├── bin
│   └── bump-version.sh
├── biome.json
├── CLAUDE.md
├── codecov.yml
├── core
├── docs
│   ├── adding-tools.md
│   ├── api-patterns.md
│   ├── architecture.md
│   ├── cloudflare
│   │   ├── architecture.md
│   │   ├── oauth-architecture.md
│   │   └── overview.md
│   ├── coding-guidelines.md
│   ├── common-patterns.md
│   ├── error-handling.md
│   ├── github-actions.md
│   ├── llms
│   │   ├── document-scopes.md
│   │   ├── documentation-style-guide.md
│   │   └── README.md
│   ├── logging.md
│   ├── monitoring.md
│   ├── pr-management.md
│   ├── quality-checks.md
│   ├── README.md
│   ├── releases
│   │   ├── cloudflare.md
│   │   └── stdio.md
│   ├── search-events-api-patterns.md
│   ├── security.md
│   ├── specs
│   │   ├── README.md
│   │   ├── search-events.md
│   │   └── subpath-constraints.md
│   ├── testing-remote.md
│   ├── testing-stdio.md
│   ├── testing.md
│   └── token-cost-tracking.md
├── LICENSE.md
├── Makefile
├── package.json
├── packages
│   ├── mcp-cloudflare
│   │   ├── .env.example
│   │   ├── components.json
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── public
│   │   │   ├── demo.cast
│   │   │   ├── favicon.ico
│   │   │   ├── flow-transparent.png
│   │   │   ├── flow.jpg
│   │   │   ├── keycap-⌘.png
│   │   │   ├── keycap-c.png
│   │   │   └── keycap-v.png
│   │   ├── src
│   │   │   ├── client
│   │   │   │   ├── app.tsx
│   │   │   │   ├── components
│   │   │   │   │   ├── animation
│   │   │   │   │   │   ├── browser-ui
│   │   │   │   │   │   │   ├── BrowserWindow.tsx
│   │   │   │   │   │   │   ├── BrowserWindowIconSidebar.tsx
│   │   │   │   │   │   │   ├── DiffBlock.tsx
│   │   │   │   │   │   │   ├── IDEWindow.tsx
│   │   │   │   │   │   │   ├── IssueDetails.tsx
│   │   │   │   │   │   │   ├── keys-copy.tsx
│   │   │   │   │   │   │   ├── LoadingSquares.tsx
│   │   │   │   │   │   │   ├── RootCause.tsx
│   │   │   │   │   │   │   ├── seer-clipmask.tsx
│   │   │   │   │   │   │   ├── seer-noisefilter.tsx
│   │   │   │   │   │   │   ├── seer.tsx
│   │   │   │   │   │   │   └── WindowHeader.tsx
│   │   │   │   │   │   ├── BrowserAnimation.tsx
│   │   │   │   │   │   ├── DataWire.tsx
│   │   │   │   │   │   ├── dracula.css
│   │   │   │   │   │   ├── terminal-ui
│   │   │   │   │   │   │   ├── keys-paste.tsx
│   │   │   │   │   │   │   ├── SpeedDisplay.tsx
│   │   │   │   │   │   │   └── StepsList.tsx
│   │   │   │   │   │   ├── TerminalAnimation.tsx
│   │   │   │   │   │   └── tests.tsx
│   │   │   │   │   ├── chat
│   │   │   │   │   │   ├── auth-form.tsx
│   │   │   │   │   │   ├── chat-input.tsx
│   │   │   │   │   │   ├── chat-message.tsx
│   │   │   │   │   │   ├── chat-messages.tsx
│   │   │   │   │   │   ├── chat-ui.tsx
│   │   │   │   │   │   ├── chat.tsx
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── tool-invocation.tsx
│   │   │   │   │   │   └── types.ts
│   │   │   │   │   ├── fragments
│   │   │   │   │   │   ├── install-tabs.tsx
│   │   │   │   │   │   ├── remote-setup.tsx
│   │   │   │   │   │   ├── setup-guide.tsx
│   │   │   │   │   │   └── stdio-setup.tsx
│   │   │   │   │   ├── getting-started.tsx
│   │   │   │   │   ├── hero
│   │   │   │   │   │   ├── header-divider.tsx
│   │   │   │   │   │   └── hero-block.tsx
│   │   │   │   │   ├── home-layout
│   │   │   │   │   │   ├── footer.tsx
│   │   │   │   │   │   └── sidebars.tsx
│   │   │   │   │   ├── ui
│   │   │   │   │   │   ├── accordion.tsx
│   │   │   │   │   │   ├── backdrop.tsx
│   │   │   │   │   │   ├── badge.tsx
│   │   │   │   │   │   ├── base.tsx
│   │   │   │   │   │   ├── button.tsx
│   │   │   │   │   │   ├── code-snippet.tsx
│   │   │   │   │   │   ├── header.tsx
│   │   │   │   │   │   ├── icon.tsx
│   │   │   │   │   │   ├── icons
│   │   │   │   │   │   │   ├── claude.tsx
│   │   │   │   │   │   │   ├── codex.tsx
│   │   │   │   │   │   │   ├── cursor.tsx
│   │   │   │   │   │   │   ├── gemini.tsx
│   │   │   │   │   │   │   ├── sentry.tsx
│   │   │   │   │   │   │   ├── vscode.tsx
│   │   │   │   │   │   │   ├── warp.tsx
│   │   │   │   │   │   │   ├── windsurf.tsx
│   │   │   │   │   │   │   └── zed.tsx
│   │   │   │   │   │   ├── interactive-markdown.tsx
│   │   │   │   │   │   ├── json-schema-params.tsx
│   │   │   │   │   │   ├── key-icon.tsx
│   │   │   │   │   │   ├── key-word.tsx
│   │   │   │   │   │   ├── markdown.tsx
│   │   │   │   │   │   ├── note.tsx
│   │   │   │   │   │   ├── prose.tsx
│   │   │   │   │   │   ├── section.tsx
│   │   │   │   │   │   ├── slash-command-actions.tsx
│   │   │   │   │   │   ├── slash-command-text.tsx
│   │   │   │   │   │   ├── sliding-panel.tsx
│   │   │   │   │   │   ├── template-vars.tsx
│   │   │   │   │   │   ├── tool-actions.tsx
│   │   │   │   │   │   └── typewriter.tsx
│   │   │   │   │   └── usecases
│   │   │   │   │       ├── fix-bugs.tsx
│   │   │   │   │       ├── index.tsx
│   │   │   │   │       ├── instrument.tsx
│   │   │   │   │       ├── search-things.tsx
│   │   │   │   │       └── search-visual.tsx
│   │   │   │   ├── contexts
│   │   │   │   │   └── auth-context.tsx
│   │   │   │   ├── hooks
│   │   │   │   │   ├── use-endpoint-mode.ts
│   │   │   │   │   ├── use-mcp-metadata.ts
│   │   │   │   │   ├── use-persisted-chat.ts
│   │   │   │   │   ├── use-scroll-lock.ts
│   │   │   │   │   └── use-streaming-simulation.ts
│   │   │   │   ├── index.css
│   │   │   │   ├── instrument.ts
│   │   │   │   ├── lib
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── main.tsx
│   │   │   │   ├── utils
│   │   │   │   │   ├── chat-error-handler.ts
│   │   │   │   │   ├── cursor-deeplink.ts
│   │   │   │   │   └── index.ts
│   │   │   │   └── vite-env.d.ts
│   │   │   ├── constants.ts
│   │   │   ├── server
│   │   │   │   ├── app.test.ts
│   │   │   │   ├── app.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── lib
│   │   │   │   │   ├── approval-dialog.test.ts
│   │   │   │   │   ├── approval-dialog.ts
│   │   │   │   │   ├── constraint-utils.test.ts
│   │   │   │   │   ├── constraint-utils.ts
│   │   │   │   │   ├── html-utils.ts
│   │   │   │   │   ├── mcp-handler.test.ts
│   │   │   │   │   ├── mcp-handler.ts
│   │   │   │   │   └── slug-validation.ts
│   │   │   │   ├── logging.ts
│   │   │   │   ├── oauth
│   │   │   │   │   ├── authorize.test.ts
│   │   │   │   │   ├── callback.test.ts
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── helpers.test.ts
│   │   │   │   │   ├── helpers.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── authorize.ts
│   │   │   │   │   │   ├── callback.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   └── state.ts
│   │   │   │   ├── routes
│   │   │   │   │   ├── chat-oauth.ts
│   │   │   │   │   ├── chat.ts
│   │   │   │   │   ├── mcp.ts
│   │   │   │   │   ├── metadata.ts
│   │   │   │   │   ├── search.test.ts
│   │   │   │   │   └── search.ts
│   │   │   │   ├── sentry.config.ts
│   │   │   │   ├── types
│   │   │   │   │   └── chat.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── utils
│   │   │   │       ├── auth-errors.ts
│   │   │   │       ├── client-ip.test.ts
│   │   │   │       ├── client-ip.ts
│   │   │   │       ├── rate-limiter.test.ts
│   │   │   │       └── rate-limiter.ts
│   │   │   └── test-setup.ts
│   │   ├── tsconfig.client.json
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   ├── tsconfig.server.json
│   │   ├── vite.config.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   ├── wrangler.canary.jsonc
│   │   └── wrangler.jsonc
│   ├── mcp-core
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── scripts
│   │   │   ├── generate-definitions.ts
│   │   │   ├── generate-otel-namespaces.ts
│   │   │   ├── measure-token-cost.ts
│   │   │   └── validate-skills-mapping.ts
│   │   ├── src
│   │   │   ├── api-client
│   │   │   │   ├── client.test.ts
│   │   │   │   ├── client.ts
│   │   │   │   ├── errors.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── schema.test.ts
│   │   │   │   ├── schema.ts
│   │   │   │   └── types.ts
│   │   │   ├── constants.ts
│   │   │   ├── errors.test.ts
│   │   │   ├── errors.ts
│   │   │   ├── internal
│   │   │   │   ├── agents
│   │   │   │   │   ├── callEmbeddedAgent.ts
│   │   │   │   │   ├── openai-provider.test.ts
│   │   │   │   │   ├── openai-provider.ts
│   │   │   │   │   └── tools
│   │   │   │   │       ├── data
│   │   │   │   │       │   ├── __namespaces.json
│   │   │   │   │       │   ├── android.json
│   │   │   │   │       │   ├── app.json
│   │   │   │   │       │   ├── artifact.json
│   │   │   │   │       │   ├── aspnetcore.json
│   │   │   │   │       │   ├── aws.json
│   │   │   │   │       │   ├── azure.json
│   │   │   │   │       │   ├── browser.json
│   │   │   │   │       │   ├── cassandra.json
│   │   │   │   │       │   ├── cicd.json
│   │   │   │   │       │   ├── CLAUDE.md
│   │   │   │   │       │   ├── client.json
│   │   │   │   │       │   ├── cloud.json
│   │   │   │   │       │   ├── cloudevents.json
│   │   │   │   │       │   ├── cloudfoundry.json
│   │   │   │   │       │   ├── code.json
│   │   │   │   │       │   ├── container.json
│   │   │   │   │       │   ├── cpu.json
│   │   │   │   │       │   ├── cpython.json
│   │   │   │   │       │   ├── database.json
│   │   │   │   │       │   ├── db.json
│   │   │   │   │       │   ├── deployment.json
│   │   │   │   │       │   ├── destination.json
│   │   │   │   │       │   ├── device.json
│   │   │   │   │       │   ├── disk.json
│   │   │   │   │       │   ├── dns.json
│   │   │   │   │       │   ├── dotnet.json
│   │   │   │   │       │   ├── elasticsearch.json
│   │   │   │   │       │   ├── enduser.json
│   │   │   │   │       │   ├── error.json
│   │   │   │   │       │   ├── faas.json
│   │   │   │   │       │   ├── feature_flags.json
│   │   │   │   │       │   ├── file.json
│   │   │   │   │       │   ├── gcp.json
│   │   │   │   │       │   ├── gen_ai.json
│   │   │   │   │       │   ├── geo.json
│   │   │   │   │       │   ├── go.json
│   │   │   │   │       │   ├── graphql.json
│   │   │   │   │       │   ├── hardware.json
│   │   │   │   │       │   ├── heroku.json
│   │   │   │   │       │   ├── host.json
│   │   │   │   │       │   ├── http.json
│   │   │   │   │       │   ├── ios.json
│   │   │   │   │       │   ├── jvm.json
│   │   │   │   │       │   ├── k8s.json
│   │   │   │   │       │   ├── linux.json
│   │   │   │   │       │   ├── log.json
│   │   │   │   │       │   ├── mcp.json
│   │   │   │   │       │   ├── messaging.json
│   │   │   │   │       │   ├── network.json
│   │   │   │   │       │   ├── nodejs.json
│   │   │   │   │       │   ├── oci.json
│   │   │   │   │       │   ├── opentracing.json
│   │   │   │   │       │   ├── os.json
│   │   │   │   │       │   ├── otel.json
│   │   │   │   │       │   ├── peer.json
│   │   │   │   │       │   ├── process.json
│   │   │   │   │       │   ├── profile.json
│   │   │   │   │       │   ├── rpc.json
│   │   │   │   │       │   ├── server.json
│   │   │   │   │       │   ├── service.json
│   │   │   │   │       │   ├── session.json
│   │   │   │   │       │   ├── signalr.json
│   │   │   │   │       │   ├── source.json
│   │   │   │   │       │   ├── system.json
│   │   │   │   │       │   ├── telemetry.json
│   │   │   │   │       │   ├── test.json
│   │   │   │   │       │   ├── thread.json
│   │   │   │   │       │   ├── tls.json
│   │   │   │   │       │   ├── url.json
│   │   │   │   │       │   ├── user.json
│   │   │   │   │       │   ├── v8js.json
│   │   │   │   │       │   ├── vcs.json
│   │   │   │   │       │   ├── webengine.json
│   │   │   │   │       │   └── zos.json
│   │   │   │   │       ├── dataset-fields.test.ts
│   │   │   │   │       ├── dataset-fields.ts
│   │   │   │   │       ├── otel-semantics.test.ts
│   │   │   │   │       ├── otel-semantics.ts
│   │   │   │   │       ├── utils.ts
│   │   │   │   │       ├── whoami.test.ts
│   │   │   │   │       └── whoami.ts
│   │   │   │   ├── constraint-helpers.test.ts
│   │   │   │   ├── constraint-helpers.ts
│   │   │   │   ├── error-handling.ts
│   │   │   │   ├── fetch-utils.test.ts
│   │   │   │   ├── fetch-utils.ts
│   │   │   │   ├── formatting.test.ts
│   │   │   │   ├── formatting.ts
│   │   │   │   ├── issue-helpers.test.ts
│   │   │   │   ├── issue-helpers.ts
│   │   │   │   ├── test-fixtures.ts
│   │   │   │   └── tool-helpers
│   │   │   │       ├── api.test.ts
│   │   │   │       ├── api.ts
│   │   │   │       ├── define.ts
│   │   │   │       ├── enhance-error.ts
│   │   │   │       ├── formatting.ts
│   │   │   │       ├── issue.ts
│   │   │   │       ├── seer.test.ts
│   │   │   │       ├── seer.ts
│   │   │   │       ├── validate-region-url.test.ts
│   │   │   │       └── validate-region-url.ts
│   │   │   ├── permissions.parseScopes.test.ts
│   │   │   ├── permissions.ts
│   │   │   ├── schema.ts
│   │   │   ├── server.ts
│   │   │   ├── skillDefinitions.json
│   │   │   ├── skillDefinitions.ts
│   │   │   ├── skills.test.ts
│   │   │   ├── skills.ts
│   │   │   ├── telem
│   │   │   │   ├── index.ts
│   │   │   │   ├── logging.ts
│   │   │   │   ├── sentry.test.ts
│   │   │   │   └── sentry.ts
│   │   │   ├── test-setup.ts
│   │   │   ├── test-utils
│   │   │   │   └── context.ts
│   │   │   ├── toolDefinitions.json
│   │   │   ├── toolDefinitions.ts
│   │   │   ├── tools
│   │   │   │   ├── analyze-issue-with-seer.test.ts
│   │   │   │   ├── analyze-issue-with-seer.ts
│   │   │   │   ├── create-dsn.test.ts
│   │   │   │   ├── create-dsn.ts
│   │   │   │   ├── create-project.test.ts
│   │   │   │   ├── create-project.ts
│   │   │   │   ├── create-team.test.ts
│   │   │   │   ├── create-team.ts
│   │   │   │   ├── find-dsns.test.ts
│   │   │   │   ├── find-dsns.ts
│   │   │   │   ├── find-organizations.test.ts
│   │   │   │   ├── find-organizations.ts
│   │   │   │   ├── find-projects.test.ts
│   │   │   │   ├── find-projects.ts
│   │   │   │   ├── find-releases.test.ts
│   │   │   │   ├── find-releases.ts
│   │   │   │   ├── find-teams.test.ts
│   │   │   │   ├── find-teams.ts
│   │   │   │   ├── get-doc.test.ts
│   │   │   │   ├── get-doc.ts
│   │   │   │   ├── get-event-attachment.test.ts
│   │   │   │   ├── get-event-attachment.ts
│   │   │   │   ├── get-issue-details.test.ts
│   │   │   │   ├── get-issue-details.ts
│   │   │   │   ├── get-trace-details.test.ts
│   │   │   │   ├── get-trace-details.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── search-docs.test.ts
│   │   │   │   ├── search-docs.ts
│   │   │   │   ├── search-events
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── formatters.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── utils.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── search-events.test.ts
│   │   │   │   ├── search-issues
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── formatters.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── README.md
│   │   │   │   ├── tools.test.ts
│   │   │   │   ├── types.ts
│   │   │   │   ├── update-issue.test.ts
│   │   │   │   ├── update-issue.ts
│   │   │   │   ├── update-project.test.ts
│   │   │   │   ├── update-project.ts
│   │   │   │   ├── use-sentry
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── handler.test.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── tool-wrapper.test.ts
│   │   │   │   │   └── tool-wrapper.ts
│   │   │   │   ├── whoami.test.ts
│   │   │   │   └── whoami.ts
│   │   │   ├── types.ts
│   │   │   ├── utils
│   │   │   │   ├── slug-validation.test.ts
│   │   │   │   ├── slug-validation.ts
│   │   │   │   ├── url-utils.test.ts
│   │   │   │   └── url-utils.ts
│   │   │   └── version.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   ├── mcp-server
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── cli
│   │   │   │   ├── parse.test.ts
│   │   │   │   ├── parse.ts
│   │   │   │   ├── resolve.test.ts
│   │   │   │   ├── resolve.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── usage.ts
│   │   │   ├── index.ts
│   │   │   └── transports
│   │   │       └── stdio.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── mcp-server-evals
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── bin
│   │   │   │   └── start-mock-stdio.ts
│   │   │   ├── evals
│   │   │   │   ├── autofix.eval.ts
│   │   │   │   ├── create-dsn.eval.ts
│   │   │   │   ├── create-project.eval.ts
│   │   │   │   ├── create-team.eval.ts
│   │   │   │   ├── get-issue.eval.ts
│   │   │   │   ├── get-trace-details.eval.ts
│   │   │   │   ├── list-dsns.eval.ts
│   │   │   │   ├── list-issues.eval.ts
│   │   │   │   ├── list-organizations.eval.ts
│   │   │   │   ├── list-projects.eval.ts
│   │   │   │   ├── list-releases.eval.ts
│   │   │   │   ├── list-tags.eval.ts
│   │   │   │   ├── list-teams.eval.ts
│   │   │   │   ├── search-docs.eval.ts
│   │   │   │   ├── search-events-agent.eval.ts
│   │   │   │   ├── search-events.eval.ts
│   │   │   │   ├── search-issues-agent.eval.ts
│   │   │   │   ├── search-issues.eval.ts
│   │   │   │   ├── update-issue.eval.ts
│   │   │   │   ├── update-project.eval.ts
│   │   │   │   └── utils
│   │   │   │       ├── fixtures.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── runner.ts
│   │   │   │       ├── structuredOutputScorer.ts
│   │   │   │       └── toolPredictionScorer.ts
│   │   │   └── setup-env.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── mcp-server-mocks
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── fixtures
│   │   │   │   ├── autofix-state.json
│   │   │   │   ├── csp-event.json
│   │   │   │   ├── csp-issue.json
│   │   │   │   ├── default-event.json
│   │   │   │   ├── event-attachments.json
│   │   │   │   ├── event.json
│   │   │   │   ├── generic-event.json
│   │   │   │   ├── issue.json
│   │   │   │   ├── performance-event.json
│   │   │   │   ├── performance-issue.json
│   │   │   │   ├── project.json
│   │   │   │   ├── regressed-issue.json
│   │   │   │   ├── tags.json
│   │   │   │   ├── team.json
│   │   │   │   ├── trace-event.json
│   │   │   │   ├── trace-items-attributes-logs-number.json
│   │   │   │   ├── trace-items-attributes-logs-string.json
│   │   │   │   ├── trace-items-attributes-spans-number.json
│   │   │   │   ├── trace-items-attributes-spans-string.json
│   │   │   │   ├── trace-items-attributes.json
│   │   │   │   ├── trace-meta-with-nulls.json
│   │   │   │   ├── trace-meta.json
│   │   │   │   ├── trace-mixed.json
│   │   │   │   ├── trace.json
│   │   │   │   ├── unknown-event.json
│   │   │   │   └── unsupported-issue.json
│   │   │   ├── fixtures.ts
│   │   │   ├── index.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── mcp-server-tsconfig
│   │   ├── package.json
│   │   ├── tsconfig.base.json
│   │   └── tsconfig.vite.json
│   ├── mcp-test-client
│   │   ├── .env.test
│   │   ├── .gitignore
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── agent.ts
│   │   │   ├── auth
│   │   │   │   ├── config.ts
│   │   │   │   └── oauth.ts
│   │   │   ├── constants.ts
│   │   │   ├── index.ts
│   │   │   ├── logger.test.ts
│   │   │   ├── logger.ts
│   │   │   ├── mcp-test-client-remote.ts
│   │   │   ├── mcp-test-client.ts
│   │   │   ├── types.ts
│   │   │   └── version.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── smoke-tests
│       ├── package.json
│       ├── src
│       │   └── smoke.test.ts
│       └── vitest.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── scripts
│   └── check-doc-links.mjs
├── turbo.json
└── vitest.workspace.ts
```

# Files

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

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

export default defineTool({
  name: "update_project",
  skills: ["project-management"], // Only available in project-management skill
  requiredScopes: ["project:write"],
  description: [
    "Update project settings in Sentry, such as name, slug, platform, and team assignment.",
    "",
    "Be careful when using this tool!",
    "",
    "Use this tool when you need to:",
    "- Update a project's name or slug to fix onboarding mistakes",
    "- Change the platform assigned to a project",
    "- Update team assignment for a project",
    "",
    "<examples>",
    "### Update a project's name and slug",
    "",
    "```",
    "update_project(organizationSlug='my-organization', projectSlug='old-project', name='New Project Name', slug='new-project-slug')",
    "```",
    "",
    "### Assign a project to a different team",
    "",
    "```",
    "update_project(organizationSlug='my-organization', projectSlug='my-project', teamSlug='backend-team')",
    "```",
    "",
    "### Update platform",
    "",
    "```",
    "update_project(organizationSlug='my-organization', projectSlug='my-project', platform='python')",
    "```",
    "",
    "</examples>",
    "",
    "<hints>",
    "- If the user passes a parameter in the form of name/otherName, it's likely in the format of <organizationSlug>/<projectSlug>.",
    "- Team assignment is handled separately from other project settings",
    "- If any parameter is ambiguous, you should clarify with the user what they meant.",
    "- When updating the slug, the project will be accessible at the new slug after the update",
    "</hints>",
  ].join("\n"),
  inputSchema: {
    organizationSlug: ParamOrganizationSlug,
    regionUrl: ParamRegionUrl.nullable().default(null),
    projectSlug: ParamProjectSlug,
    name: z
      .string()
      .trim()
      .describe("The new name for the project")
      .nullable()
      .default(null),
    slug: z
      .string()
      .toLowerCase()
      .trim()
      .describe("The new slug for the project (must be unique)")
      .nullable()
      .default(null),
    platform: ParamPlatform.nullable().default(null),
    teamSlug: ParamTeamSlug.nullable()
      .default(null)
      .describe(
        "The team to assign this project to. Note: this will replace the current team assignment.",
      ),
  },
  annotations: {
    readOnlyHint: false,
    destructiveHint: true,
    idempotentHint: true,
    openWorldHint: true,
  },
  async handler(params, context: ServerContext) {
    const apiService = apiServiceFromContext(context, {
      regionUrl: params.regionUrl ?? undefined,
    });
    const organizationSlug = params.organizationSlug;

    setTag("organization.slug", organizationSlug);
    setTag("project.slug", params.projectSlug);

    // Handle team assignment separately if provided
    if (params.teamSlug) {
      setTag("team.slug", params.teamSlug);
      try {
        await apiService.addTeamToProject({
          organizationSlug,
          projectSlug: params.projectSlug,
          teamSlug: params.teamSlug,
        });
      } catch (err) {
        logIssue(err);
        throw new Error(
          `Failed to assign team ${params.teamSlug} to project ${params.projectSlug}: ${err instanceof Error ? err.message : "Unknown error"}`,
        );
      }
    }

    // Update project settings if any are provided
    const hasProjectUpdates = params.name || params.slug || params.platform;

    let project: Project | undefined;
    if (hasProjectUpdates) {
      try {
        project = await apiService.updateProject({
          organizationSlug,
          projectSlug: params.projectSlug,
          name: params.name,
          slug: params.slug,
          platform: params.platform,
        });
      } catch (err) {
        logIssue(err);
        throw new Error(
          `Failed to update project ${params.projectSlug}: ${err instanceof Error ? err.message : "Unknown error"}`,
        );
      }
    } else {
      // If only team assignment, fetch current project data for display
      const projects = await apiService.listProjects(organizationSlug);
      project = projects.find((p) => p.slug === params.projectSlug);
      if (!project) {
        throw new UserInputError(`Project ${params.projectSlug} not found`);
      }
    }

    let output = `# Updated Project in **${organizationSlug}**\n\n`;
    output += `**ID**: ${project.id}\n`;
    output += `**Slug**: ${project.slug}\n`;
    output += `**Name**: ${project.name}\n`;
    if (project.platform) {
      output += `**Platform**: ${project.platform}\n`;
    }

    // Display what was updated
    const updates: string[] = [];
    if (params.name) updates.push(`name to "${params.name}"`);
    if (params.slug) updates.push(`slug to "${params.slug}"`);
    if (params.platform) updates.push(`platform to "${params.platform}"`);
    if (params.teamSlug)
      updates.push(`team assignment to "${params.teamSlug}"`);

    if (updates.length > 0) {
      output += `\n## Updates Applied\n`;
      output += updates.map((update) => `- Updated ${update}`).join("\n");
      output += `\n`;
    }

    output += "\n# Using this information\n\n";
    output += `- The project is now accessible at slug: \`${project.slug}\`\n`;
    if (params.teamSlug) {
      output += `- The project is now assigned to the \`${params.teamSlug}\` team\n`;
    }
    return output;
  },
});

```

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

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

// Type for flexible event data that can contain any fields
export type FlexibleEventData = Record<string, unknown>;

// Helper to safely get a string value from event data
export function getStringValue(
  event: FlexibleEventData,
  key: string,
  defaultValue = "",
): string {
  const value = event[key];
  return typeof value === "string" ? value : defaultValue;
}

// Helper to safely get a number value from event data
export function getNumberValue(
  event: FlexibleEventData,
  key: string,
): number | undefined {
  const value = event[key];
  return typeof value === "number" ? value : undefined;
}

// Helper to check if fields contain aggregate functions
export function isAggregateQuery(fields: string[]): boolean {
  return fields.some((field) => field.includes("(") && field.includes(")"));
}

// Helper function to fetch custom attributes for a dataset
export async function fetchCustomAttributes(
  apiService: SentryApiService,
  organizationSlug: string,
  dataset: "errors" | "logs" | "spans",
  projectId?: string,
  timeParams?: { statsPeriod?: string; start?: string; end?: string },
): Promise<{
  attributes: Record<string, string>;
  fieldTypes: Record<string, "string" | "number">;
}> {
  const customAttributes: Record<string, string> = {};
  const fieldTypes: Record<string, "string" | "number"> = {};

  if (dataset === "errors") {
    // TODO: For errors dataset, we currently need to use the old listTags API
    // This will be updated in the future to use the new trace-items attributes API
    const tagsResponse = await apiService.listTags({
      organizationSlug,
      dataset: "events",
      project: projectId,
      statsPeriod: "14d",
      useCache: true,
      useFlagsBackend: true,
    });

    for (const tag of tagsResponse) {
      if (tag.key && !tag.key.startsWith("sentry:")) {
        customAttributes[tag.key] = tag.name || tag.key;
      }
    }
  } else {
    // For logs and spans datasets, use the trace-items attributes endpoint
    const itemType = dataset === "logs" ? "logs" : "spans";
    const attributesResponse = await apiService.listTraceItemAttributes({
      organizationSlug,
      itemType,
      project: projectId,
      statsPeriod: "14d",
    });

    for (const attr of attributesResponse) {
      if (attr.key && !attr.key.startsWith("sentry:")) {
        customAttributes[attr.key] = attr.name || attr.key;
        // Track field type from the attribute response with validation
        if (attr.type && (attr.type === "string" || attr.type === "number")) {
          fieldTypes[attr.key] = attr.type;
        }
      }
    }
  }

  return { attributes: customAttributes, fieldTypes };
}

/**
 * Create a tool for the agent to query available attributes by dataset
 * The tool is pre-bound with the API service and organization configured for the appropriate region
 */
export function createDatasetAttributesTool(options: {
  apiService: SentryApiService;
  organizationSlug: string;
  projectId?: string;
}) {
  const { apiService, organizationSlug, projectId } = options;
  return agentTool({
    description:
      "Query available attributes and fields for a specific Sentry dataset to understand what data is available",
    parameters: z.object({
      dataset: z
        .enum(["spans", "errors", "logs"])
        .describe("The dataset to query attributes for"),
    }),
    execute: async ({ dataset }) => {
      const {
        BASE_COMMON_FIELDS,
        DATASET_FIELDS,
        RECOMMENDED_FIELDS,
        NUMERIC_FIELDS,
        DATASET_EXAMPLES,
      } = await import("./config");

      // Get custom attributes for this dataset
      // IMPORTANT: Let ALL errors bubble up to wrapAgentToolExecute
      // UserInputError will be converted to error string for the AI agent
      // Other errors will bubble up to be captured by Sentry
      const { attributes: customAttributes, fieldTypes } =
        await fetchCustomAttributes(
          apiService,
          organizationSlug,
          dataset,
          projectId,
        );

      // Combine all available fields
      const allFields = {
        ...BASE_COMMON_FIELDS,
        ...DATASET_FIELDS[dataset],
        ...customAttributes,
      };

      const recommendedFields = RECOMMENDED_FIELDS[dataset];

      // Combine field types from both static config and dynamic API
      const allFieldTypes = { ...fieldTypes };
      const staticNumericFields = NUMERIC_FIELDS[dataset] || new Set();
      for (const field of staticNumericFields) {
        allFieldTypes[field] = "number";
      }

      return `Dataset: ${dataset}

Available Fields (${Object.keys(allFields).length} total):
${Object.entries(allFields)
  .slice(0, 50) // Limit to first 50 to avoid overwhelming the agent
  .map(([key, desc]) => `- ${key}: ${desc}`)
  .join("\n")}
${Object.keys(allFields).length > 50 ? `\n... and ${Object.keys(allFields).length - 50} more fields` : ""}

Recommended Fields for ${dataset}:
${recommendedFields.basic.map((f) => `- ${f}`).join("\n")}

Field Types (CRITICAL for aggregate functions):
${Object.entries(allFieldTypes)
  .slice(0, 30) // Show more field types since this is critical for validation
  .map(([key, type]) => `- ${key}: ${type}`)
  .join("\n")}
${Object.keys(allFieldTypes).length > 30 ? `\n... and ${Object.keys(allFieldTypes).length - 30} more fields` : ""}

IMPORTANT: Only use numeric aggregate functions (avg, sum, min, max, percentiles) with numeric fields. Use count() or count_unique() for non-numeric fields.

EXAMPLE QUERIES FOR ${dataset.toUpperCase()}:
${DATASET_EXAMPLES[dataset]
  .map((ex) => `- "${ex.description}" →\n  ${JSON.stringify(ex.output)}`)
  .join("\n\n")}

Use these examples as patterns for constructing your query.`;
    },
  });
}

```

--------------------------------------------------------------------------------
/docs/llms/documentation-style-guide.md:
--------------------------------------------------------------------------------

```markdown
# Documentation Style Guide

This guide defines how to write effective documentation for LLMs working with the Sentry MCP codebase.

## Core Principles

### 1. Assume Intelligence
- LLMs understand programming concepts - don't explain basics
- Focus on project-specific patterns and conventions
- Skip obvious steps like "create a file" or "save your changes"

### 2. Optimize for Context Windows
- Keep documents focused on a single topic
- Use code examples instead of verbose explanations
- Every line should provide unique value
- Split large topics across multiple focused docs

### 3. Show, Don't Tell
- Include minimal, focused code examples
- Reference actual implementations: `See @packages/mcp-server/src/server.ts:45`
- Use real patterns from the codebase

## Document Structure

### Required Sections

```markdown
# [Feature/Pattern Name]

Brief one-line description of what this covers.

## When to Use

Bullet points describing specific scenarios.

## Implementation Pattern

```typescript
// Minimal example showing the pattern
const example = {
  // Only include what's unique to this project
};
```

## Key Conventions

Project-specific rules that must be followed.

## Common Patterns

Link to reusable patterns: See "Error Handling" in @docs/common-patterns.md

## References

- Implementation: `@packages/mcp-server/src/[file].ts`
- Tests: `@packages/mcp-server/src/[file].test.ts`
- Examples in codebase: [specific function/tool names]
```

## What to Include

### DO Include:
- **Project-specific patterns** - How THIS codebase does things
- **Architecture decisions** - Why things are structured this way
- **Required conventions** - Must-follow rules for consistency
- **Integration points** - How components interact
- **Validation requirements** - What checks must pass

### DON'T Include:
- **General programming concepts** - How to write TypeScript
- **Tool documentation** - How to use pnpm or Vitest
- **Verbose examples** - Keep code samples minimal
- **Redundant content** - Link to other docs instead
- **Step-by-step tutorials** - LLMs don't need hand-holding

## Code Examples

### Good Example:
```typescript
// Tool parameter pattern used throughout the codebase
export const ParamOrganizationSlug = z
  .string()
  .toLowerCase()
  .trim()
  .describe("The organization's slug. Find using `find_organizations()` tool.");
```

### Bad Example:
```typescript
// First, import the required libraries
import { z } from "zod";

// Define a schema for the organization slug parameter
// This schema will validate that the input is a string
// It will also convert to lowercase and trim whitespace
export const ParamOrganizationSlug = z
  .string() // Ensures the value is a string
  .toLowerCase() // Converts to lowercase
  .trim() // Removes whitespace
  .describe("The organization's slug..."); // Adds description
```

## Cross-References

### File References (MANDATORY):
- Use @path syntax for local files: `@docs/common-patterns.md`
- Always reference from repo root: `@packages/mcp-server/src/server.ts`
- Do NOT use Markdown links for local files (avoid markdown `[text](./...)` patterns)
- Prefer path-only mentions to help agents parse

### Section References:
- Refer to sections by name, not anchors: `See "Error Handling" in @docs/common-patterns.md`
- If multiple sections share a name, include a short hint: `("Zod Patterns" in @docs/common-patterns.md)`

### Code References:
- Use concrete paths and identifiers: `@packages/mcp-server/src/tools/search-events/index.ts:buildQuery`
- Optional line hints for humans: `server.ts:45-52` (agents may ignore)
- Prefer real implementations over fabricated examples

### External Links:
- Keep standard Markdown links for external sites
- Use concise link text; avoid link-only bullets

## Language and Tone

### Use Direct Language:
- ❌ "You might want to consider using..."
- ✅ "Use UserInputError for validation failures"

### Be Specific:
- ❌ "Handle errors appropriately"
- ✅ "Throw UserInputError with a message explaining how to fix it"

### Focus on Requirements:
- ❌ "It's a good practice to run tests"
- ✅ "Run `pnpm test` - all tests must pass"

## Document Length Guidelines

### Context Window Optimization:
- Each document should be consumable in a single context
- Length depends on complexity, not arbitrary limits
- Verbose explanations → concise code examples
- Complex topics → split into focused documents

### Examples:
- **Quality checks**: ~100 lines (simple commands)
- **Adding a tool**: ~300 lines (includes examples)
- **API patterns**: May be longer if examples are valuable
- **Architecture**: Split into overview + detailed sections

## Maintenance

### When Updating Docs:
1. Check for redundancy with other docs
2. Update cross-references if needed
3. Ensure examples still match codebase
4. Keep line count under 400

### Red Flags:
- Verbose prose explaining what code could show
- Repeated content → extract to common-patterns.md
- No code references → add implementation examples
- Generic programming advice → remove it
- Multiple concepts in one doc → split by topic

## Example: Refactoring a Verbose Section

### Before:
```markdown
## Setting Up Your Development Environment

First, make sure you have Node.js installed. You can download it from nodejs.org.
Next, install pnpm globally using npm install -g pnpm. Then clone the repository
using git clone. Navigate to the project directory and run pnpm install to install
all dependencies. Make sure to create your .env file with the required variables.
```

### After:
```markdown
## Environment Setup

Required: Node.js 20+, pnpm

```bash
pnpm install
cp .env.example .env  # Add your API keys
```

See "Development Setup" in @AGENTS.md for environment variables.
```

## Agent Readability Checklist

- Uses @path for all local file references
- Short, focused sections with concrete examples
- Minimal prose; prefers code and commands
- Clear preconditions and environment notes
- Error handling and validation rules are explicit

This style guide ensures documentation remains focused, valuable, and maintainable for LLM consumption.

```

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

```typescript
/**
 * Shared HTML utilities for consistent styling across server-rendered pages
 */

/**
 * Sanitizes HTML content to prevent XSS attacks
 */
export function sanitizeHtml(unsafe: string): string {
  return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

/**
 * Common CSS styles used across all pages
 */
const SHARED_STYLES = `
  /* Modern, responsive styling with system fonts */
  :root {
    --primary-color: oklch(0.205 0 0);
    --highlight-color: oklch(0.811 0.111 293.571);
    --border-color: oklch(0.278 0.033 256.848);
    --error-color: #f44336;
    --success-color: #4caf50;
    --border-color: oklch(0.269 0 0);
    --text-color: oklch(0.872 0.01 258.338);
    --background-color: oklab(0 0 0 / 0.3);
  }
  
  body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 
                 Helvetica, Arial, sans-serif, "Apple Color Emoji", 
                 "Segoe UI Emoji", "Segoe UI Symbol";
    line-height: 1.6;
    color: var(--text-color);
    background: linear-gradient(oklch(0.13 0.028 261.692) 0%, oklch(0.21 0.034 264.665) 50%, oklch(0.13 0.028 261.692) 100%);
    min-height: 100vh;
    margin: 0;
    padding: 0;
  }
  
  .container {
    max-width: 600px;
    margin: 2rem auto;
    padding: 1rem;
  }
  
  .precard {
    padding: 2rem;
    text-align: center;
  }
  
  .card {
    background-color: var(--background-color);
    padding: 2rem;
    text-align: center;
  }
  
  .header {
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 1.5rem;
  }
  
  .logo {
    width: 36px;
    height: 36px;
    margin-right: 1rem;
    color: var(--highlight-color);
  }
  
  .title {
    margin: 0;
    font-size: 26px;
    font-weight: 400;
    color: white;
  }
  
  .status-message {
    margin: 1.5rem 0;
    font-size: 1.5rem;
    font-weight: 400;
    color: white;
  }
  
  .description {
    margin: 1rem 0;
    color: var(--text-color);
    font-size: 1rem;
  }

  .spinner {
    width: 24px;
    height: 24px;
    border: 2px solid var(--border-color);
    border-top: 2px solid var(--highlight-color);
    border-radius: 50%;
    animation: spin 1s linear infinite;
    margin: 1rem auto;
  }
  
  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }
  
  /* Responsive adjustments */
  @media (max-width: 640px) {
    .container {
      margin: 1rem auto;
      padding: 0.5rem;
    }
    
    .card {
      padding: 1.5rem;
    }
    
    .precard {
      padding: 1rem;
    }
  }
`;

/**
 * Sentry logo SVG
 */
const SENTRY_LOGO = `
  <svg class="logo" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" aria-labelledby="icon-title">
    <title id="icon-title">Sentry Logo</title>
    <path d="M17.48 1.996c.45.26.823.633 1.082 1.083l13.043 22.622a2.962 2.962 0 0 1-2.562 4.44h-3.062c.043-.823.039-1.647 0-2.472h3.052a.488.488 0 0 0 .43-.734L16.418 4.315a.489.489 0 0 0-.845 0L12.582 9.51a23.16 23.16 0 0 1 7.703 8.362 23.19 23.19 0 0 1 2.8 11.024v1.234h-7.882v-1.236a15.284 15.284 0 0 0-6.571-12.543l-1.48 2.567a12.301 12.301 0 0 1 5.105 9.987v1.233h-9.3a2.954 2.954 0 0 1-2.56-1.48A2.963 2.963 0 0 1 .395 25.7l1.864-3.26a6.854 6.854 0 0 1 2.15 1.23l-1.883 3.266a.49.49 0 0 0 .43.734h6.758a9.985 9.985 0 0 0-4.83-7.272l-1.075-.618 3.927-6.835 1.075.615a17.728 17.728 0 0 1 6.164 5.956 17.752 17.752 0 0 1 2.653 8.154h2.959a20.714 20.714 0 0 0-3.05-9.627 20.686 20.686 0 0 0-7.236-7.036l-1.075-.618 4.215-7.309a2.958 2.958 0 0 1 4.038-1.083Z" fill="currentColor"></path>
  </svg>
`;

/**
 * Options for creating HTML pages
 */
export interface HtmlPageOptions {
  title: string;
  serverName?: string;
  statusMessage: string;
  description?: string;
  type?: "success" | "error" | "info";
  showSpinner?: boolean;
  additionalStyles?: string;
  bodyScript?: string;
}

/**
 * Creates a consistent HTML page with Sentry branding and styling
 */
export function createHtmlPage(options: HtmlPageOptions): string {
  const {
    title,
    serverName = "Sentry MCP",
    statusMessage,
    description,
    showSpinner = false,
    additionalStyles = "",
    bodyScript = "",
  } = options;

  const safeTitle = sanitizeHtml(title);
  const safeServerName = sanitizeHtml(serverName);
  const safeStatusMessage = sanitizeHtml(statusMessage);
  const safeDescription = description ? sanitizeHtml(description) : "";

  return `
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>${safeTitle}</title>
        <style>
          ${SHARED_STYLES}
          ${additionalStyles}
        </style>
      </head>
      <body>
        <div class="container">
          <div class="precard">
            <div class="header">
              ${SENTRY_LOGO}
              <h1 class="title"><strong>${safeServerName}</strong></h1>
            </div>
          </div>
          
          <div class="card">
            <h2 class="status-message">${safeStatusMessage}</h2>
            
            ${showSpinner ? '<div class="spinner"></div>' : ""}
            
            ${safeDescription ? `<p class="description">${safeDescription}</p>` : ""}
          </div>
        </div>
        
        ${bodyScript ? `<script>${bodyScript}</script>` : ""}
      </body>
    </html>
  `;
}

/**
 * Creates a success page for OAuth flows
 */
export function createSuccessPage(
  options: Partial<HtmlPageOptions> = {},
): string {
  return createHtmlPage({
    title: "Authentication Successful",
    statusMessage: "Authentication Successful",
    description: "Authentication completed successfully.",
    type: "success",
    ...options,
  });
}

/**
 * Creates an error page for OAuth flows
 */
export function createErrorPage(
  title: string,
  message: string,
  options: Partial<HtmlPageOptions> = {},
): string {
  return createHtmlPage({
    title: sanitizeHtml(title),
    statusMessage: sanitizeHtml(title),
    description: sanitizeHtml(message),
    type: "error",
    ...options,
  });
}

```

--------------------------------------------------------------------------------
/packages/mcp-core/src/internal/tool-helpers/validate-region-url.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from "vitest";
import { validateRegionUrl } from "./validate-region-url";
import { UserInputError } from "../../errors";

describe("validateRegionUrl", () => {
  describe("base host validation", () => {
    it("allows exact match for base host", () => {
      const result = validateRegionUrl("https://sentry.io", "sentry.io");
      expect(result).toBe("sentry.io");
    });

    it("allows exact match for self-hosted", () => {
      const result = validateRegionUrl(
        "https://sentry.company.com",
        "sentry.company.com",
      );
      expect(result).toBe("sentry.company.com");
    });

    it("allows exact match for any base host", () => {
      const result = validateRegionUrl("https://example.com", "example.com");
      expect(result).toBe("example.com");
    });
  });

  describe("allowlist validation", () => {
    it("allows us.sentry.io from allowlist", () => {
      const result = validateRegionUrl("https://us.sentry.io", "sentry.io");
      expect(result).toBe("us.sentry.io");
    });

    it("allows de.sentry.io from allowlist", () => {
      const result = validateRegionUrl("https://de.sentry.io", "sentry.io");
      expect(result).toBe("de.sentry.io");
    });

    it("allows sentry.io from allowlist even with different base", () => {
      const result = validateRegionUrl("https://sentry.io", "mycompany.com");
      expect(result).toBe("sentry.io");
    });

    it("allows us.sentry.io even with self-hosted base", () => {
      const result = validateRegionUrl("https://us.sentry.io", "mycompany.com");
      expect(result).toBe("us.sentry.io");
    });

    it("rejects domains not in allowlist", () => {
      expect(() =>
        validateRegionUrl("https://evil.sentry.io", "sentry.io"),
      ).toThrow(UserInputError);
      expect(() =>
        validateRegionUrl("https://evil.sentry.io", "sentry.io"),
      ).toThrow("The domain 'evil.sentry.io' is not allowed");
    });

    it("rejects completely different domains", () => {
      expect(() => validateRegionUrl("https://evil.com", "sentry.io")).toThrow(
        UserInputError,
      );
      expect(() => validateRegionUrl("https://evil.com", "sentry.io")).toThrow(
        "The domain 'evil.com' is not allowed",
      );
    });

    it("rejects subdomains of self-hosted that aren't base host", () => {
      expect(() =>
        validateRegionUrl("https://eu.mycompany.com", "mycompany.com"),
      ).toThrow(UserInputError);
      expect(() =>
        validateRegionUrl("https://eu.mycompany.com", "mycompany.com"),
      ).toThrow("The domain 'eu.mycompany.com' is not allowed");
    });
  });

  describe("protocol validation", () => {
    it("rejects URLs without protocol", () => {
      expect(() => validateRegionUrl("sentry.io", "sentry.io")).toThrow(
        UserInputError,
      );
      expect(() => validateRegionUrl("sentry.io", "sentry.io")).toThrow(
        "Must be a valid URL",
      );
    });

    it("rejects non-https protocols", () => {
      expect(() => validateRegionUrl("ftp://sentry.io", "sentry.io")).toThrow(
        UserInputError,
      );
      expect(() => validateRegionUrl("ftp://sentry.io", "sentry.io")).toThrow(
        "Must use HTTPS protocol for security",
      );
      expect(() => validateRegionUrl("http://sentry.io", "sentry.io")).toThrow(
        "Must use HTTPS protocol for security",
      );
    });

    it("rejects malformed URLs", () => {
      expect(() => validateRegionUrl("https://", "sentry.io")).toThrow(
        UserInputError,
      );
      expect(() => validateRegionUrl("https://", "sentry.io")).toThrow(
        "Must be a valid URL",
      );
    });

    it("rejects protocol-only hosts", () => {
      expect(() => validateRegionUrl("https://https", "sentry.io")).toThrow(
        UserInputError,
      );
      expect(() => validateRegionUrl("https://https", "sentry.io")).toThrow(
        "The host cannot be just a protocol name",
      );
    });
  });

  describe("case sensitivity", () => {
    it("handles case-insensitive matching for sentry.io", () => {
      const result = validateRegionUrl("https://US.SENTRY.IO", "sentry.io");
      expect(result).toBe("us.sentry.io");
    });

    it("handles case-insensitive self-hosted domains", () => {
      const result = validateRegionUrl(
        "https://SENTRY.COMPANY.COM",
        "sentry.company.com",
      );
      expect(result).toBe("sentry.company.com");
    });

    it("handles mixed case base host for sentry.io", () => {
      const result = validateRegionUrl("https://us.sentry.io", "SENTRY.IO");
      expect(result).toBe("us.sentry.io");
    });
  });

  describe("edge cases", () => {
    it("handles trailing slashes in URL", () => {
      const result = validateRegionUrl("https://us.sentry.io/", "sentry.io");
      expect(result).toBe("us.sentry.io");
    });

    it("handles URL with path", () => {
      const result = validateRegionUrl(
        "https://us.sentry.io/api/0/organizations/",
        "sentry.io",
      );
      expect(result).toBe("us.sentry.io");
    });

    it("handles URL with query params", () => {
      const result = validateRegionUrl(
        "https://us.sentry.io?test=1",
        "sentry.io",
      );
      expect(result).toBe("us.sentry.io");
    });

    it("handles URL with port for sentry.io", () => {
      const result = validateRegionUrl("https://us.sentry.io:443", "sentry.io");
      expect(result).toBe("us.sentry.io");
    });

    it("allows self-hosted with matching port", () => {
      const result = validateRegionUrl(
        "https://sentry.company.com:8080",
        "sentry.company.com:8080",
      );
      expect(result).toBe("sentry.company.com:8080");
    });

    it("rejects self-hosted with non-matching port", () => {
      expect(() =>
        validateRegionUrl(
          "https://sentry.company.com:8080",
          "sentry.company.com",
        ),
      ).toThrow(UserInputError);
      expect(() =>
        validateRegionUrl(
          "https://sentry.company.com:8080",
          "sentry.company.com",
        ),
      ).toThrow("The domain 'sentry.company.com:8080' is not allowed");
    });
  });
});

```

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

```json
{
  "namespace": "messaging",
  "description": "Attributes describing telemetry around messaging systems and messaging activities.",
  "attributes": {
    "messaging.batch.message_count": {
      "description": "The number of messages sent, received, or processed in the scope of the batching operation.",
      "type": "number",
      "note": "Instrumentations SHOULD NOT set `messaging.batch.message_count` on spans that operate with a single message. When a messaging client library supports both batch and single-message API for the same operation, instrumentations SHOULD use `messaging.batch.message_count` for batching APIs and SHOULD NOT use it for single-message APIs.\n",
      "stability": "development",
      "examples": ["0", "1", "2"]
    },
    "messaging.client.id": {
      "description": "A unique identifier for the client that consumes or produces a message.\n",
      "type": "string",
      "stability": "development",
      "examples": ["client-5", "myhost@8742@s8083jm"]
    },
    "messaging.consumer.group.name": {
      "description": "The name of the consumer group with which a consumer is associated.\n",
      "type": "string",
      "note": "Semantic conventions for individual messaging systems SHOULD document whether `messaging.consumer.group.name` is applicable and what it means in the context of that system.\n",
      "stability": "development",
      "examples": ["my-group", "indexer"]
    },
    "messaging.destination.name": {
      "description": "The message destination name",
      "type": "string",
      "note": "Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If\nthe broker doesn't have such notion, the destination name SHOULD uniquely identify the broker.\n",
      "stability": "development",
      "examples": ["MyQueue", "MyTopic"]
    },
    "messaging.destination.subscription.name": {
      "description": "The name of the destination subscription from which a message is consumed.",
      "type": "string",
      "note": "Semantic conventions for individual messaging systems SHOULD document whether `messaging.destination.subscription.name` is applicable and what it means in the context of that system.\n",
      "stability": "development",
      "examples": ["subscription-a"]
    },
    "messaging.destination.template": {
      "description": "Low cardinality representation of the messaging destination name",
      "type": "string",
      "note": "Destination names could be constructed from templates. An example would be a destination name involving a user name or product id. Although the destination name in this case is of high cardinality, the underlying template is of low cardinality and can be effectively used for grouping and aggregation.\n",
      "stability": "development",
      "examples": ["/customers/{customerId}"]
    },
    "messaging.destination.anonymous": {
      "description": "A boolean that is true if the message destination is anonymous (could be unnamed or have auto-generated name).",
      "type": "boolean",
      "stability": "development"
    },
    "messaging.destination.temporary": {
      "description": "A boolean that is true if the message destination is temporary and might not exist anymore after messages are processed.",
      "type": "boolean",
      "stability": "development"
    },
    "messaging.destination.partition.id": {
      "description": "The identifier of the partition messages are sent to or received from, unique within the `messaging.destination.name`.\n",
      "type": "string",
      "stability": "development",
      "examples": ["1"]
    },
    "messaging.message.conversation_id": {
      "description": "The conversation ID identifying the conversation to which the message belongs, represented as a string. Sometimes called \"Correlation ID\".\n",
      "type": "string",
      "stability": "development",
      "examples": ["MyConversationId"]
    },
    "messaging.message.envelope.size": {
      "description": "The size of the message body and metadata in bytes.\n",
      "type": "number",
      "note": "This can refer to both the compressed or uncompressed size. If both sizes are known, the uncompressed\nsize should be used.\n",
      "stability": "development",
      "examples": ["2738"]
    },
    "messaging.message.id": {
      "description": "A value used by the messaging system as an identifier for the message, represented as a string.",
      "type": "string",
      "stability": "development",
      "examples": ["452a7c7c7c7048c2f887f61572b18fc2"]
    },
    "messaging.message.body.size": {
      "description": "The size of the message body in bytes.\n",
      "type": "number",
      "note": "This can refer to both the compressed or uncompressed body size. If both sizes are known, the uncompressed\nbody size should be used.\n",
      "stability": "development",
      "examples": ["1439"]
    },
    "messaging.operation.type": {
      "description": "A string identifying the type of the messaging operation.\n",
      "type": "string",
      "note": "If a custom value is used, it MUST be of low cardinality.",
      "stability": "development",
      "examples": [
        "create",
        "send",
        "receive",
        "process",
        "settle",
        "deliver",
        "publish"
      ]
    },
    "messaging.operation.name": {
      "description": "The system-specific name of the messaging operation.\n",
      "type": "string",
      "stability": "development",
      "examples": ["ack", "nack", "send"]
    },
    "messaging.system": {
      "description": "The messaging system as identified by the client instrumentation.",
      "type": "string",
      "note": "The actual messaging system may differ from the one known by the client. For example, when using Kafka client libraries to communicate with Azure Event Hubs, the `messaging.system` is set to `kafka` based on the instrumentation's best knowledge.\n",
      "stability": "development",
      "examples": [
        "activemq",
        "aws_sqs",
        "eventgrid",
        "eventhubs",
        "servicebus",
        "gcp_pubsub",
        "jms",
        "kafka",
        "rabbitmq",
        "rocketmq",
        "pulsar"
      ]
    }
  }
}

```

--------------------------------------------------------------------------------
/docs/releases/cloudflare.md:
--------------------------------------------------------------------------------

```markdown
# Cloudflare Release

Cloudflare Workers deployment configuration and release process.

## Architecture Overview

The deployment consists of:
- **Worker**: Stateless HTTP server with OAuth flow and MCP handler
- **KV Storage**: OAuth token storage
- **Static Assets**: React UI for setup instructions

## Wrangler Configuration

### wrangler.jsonc

```jsonc
{
  "name": "sentry-mcp-oauth",
  "main": "./src/server/index.ts",
  "compatibility_date": "2025-03-21",
  "compatibility_flags": [
    "nodejs_compat",
    "nodejs_compat_populate_process_env"
  ],
  "keep_vars": true,

  // Bindings
  "kv_namespaces": [{
    "binding": "OAUTH_KV",
    "id": "your-kv-namespace-id"
  }],

  // SPA configuration
  "site": {
    "bucket": "./dist/client"
  }
}
```

### Environment Variables

Required in production:
```bash
SENTRY_CLIENT_ID=your_oauth_app_id
SENTRY_CLIENT_SECRET=your_oauth_app_secret
COOKIE_SECRET=32_char_random_string
```

Optional overrides for self-hosted deployments:
```bash
# Leave unset to target the SaaS host
SENTRY_HOST=sentry.example.com     # Hostname only (self-hosted only)
```

Configure these overrides only when your Cloudflare deployment connects to a
self-hosted Sentry instance; no additional host variables are required for the
SaaS service.

Development (.dev.vars):
```bash
SENTRY_CLIENT_ID=dev_client_id
SENTRY_CLIENT_SECRET=dev_secret
COOKIE_SECRET=dev-cookie-secret
```

## MCP Handler Setup

The MCP handler uses a stateless architecture with closure-captured context:

```typescript
import { experimental_createMcpHandler as createMcpHandler } from "agents/mcp";
import { buildServer } from "@sentry/mcp-server/server";

const mcpHandler: ExportedHandler<Env> = {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // Extract auth props from ExecutionContext (set by OAuth provider)
    const oauthCtx = ctx as OAuthExecutionContext;

    // Build complete ServerContext from OAuth props + constraints
    const serverContext: ServerContext = {
      userId: oauthCtx.props.userId,
      clientId: oauthCtx.props.clientId,
      accessToken: oauthCtx.props.accessToken,
      grantedSkills,  // Primary authorization method
      constraints: verification.constraints,
      sentryHost,
      mcpUrl: oauthCtx.props.mcpUrl,
    };

    // Build server with context - context is captured in tool handler closures
    const server = buildServer({ context: serverContext });

    // Run MCP handler - context already available via closures
    return createMcpHandler(server, { route: "/mcp" })(request, env, ctx);
  },
};
```

## OAuth Provider Setup

Configure the OAuth provider with required scopes:

```typescript
const oAuthProvider = new OAuthProvider({
  clientId: env.SENTRY_CLIENT_ID,
  clientSecret: env.SENTRY_CLIENT_SECRET,
  oauthUrl: `https://${env.SENTRY_HOST}/api/0/authorize/`,
  tokenUrl: `https://${env.SENTRY_HOST}/api/0/token/`,
  redirectUrl: `${new URL(request.url).origin}/auth/sentry/callback`,
  scope: ["org:read", "project:read", "issue:read", "issue:write"]
});
```

## Deployment Commands

### Local Development

```bash
# Install dependencies
pnpm install

# Start dev server
pnpm dev

# Access at http://localhost:8787
```

### Production Deployment

#### Automated via GitHub Actions (Recommended)

Production deployments happen automatically when changes are pushed to the main branch:

1. Push to main or merge a PR
2. GitHub Actions runs tests
3. If tests pass, deploys to Cloudflare

Required secrets in GitHub repository settings:
- `CLOUDFLARE_API_TOKEN` - API token with Workers deployment permissions
- `CLOUDFLARE_ACCOUNT_ID` - Your Cloudflare account ID

See `github-actions.md` for detailed setup instructions.

#### Manual Deployment

```bash
# Build client assets
pnpm build

# Deploy to Cloudflare
pnpm deploy

# Or deploy specific environment
pnpm deploy --env production
```

#### Version Uploads (Gradual Rollouts)

For feature branches, GitHub Actions automatically uploads new versions without deploying:

1. Push to any branch (except main)
2. Tests run automatically
3. If tests pass, version is uploaded to Cloudflare
4. Use Cloudflare dashboard to gradually roll out the version

Manual version upload:
```bash
pnpm cf:versions:upload
```

### Creating Resources

First-time setup:
```bash
# Create KV namespace for OAuth token storage
npx wrangler kv:namespace create OAUTH_KV

# Update wrangler.jsonc with the namespace ID
```

## Multi-Region Considerations

Cloudflare Workers run globally, but consider:
- KV is eventually consistent globally
- Workers are stateless and edge-deployed
- Use regional hints for performance

## Security Configuration

### CORS Settings

```typescript
const ALLOWED_ORIGINS = [
  "https://sentry.io",
  "https://*.sentry.io"
];

// Apply to responses
response.headers.set("Access-Control-Allow-Origin", origin);
response.headers.set("Access-Control-Allow-Credentials", "true");
```

### Cookie Configuration

```typescript
// Secure cookie settings
"HttpOnly; Secure; SameSite=Lax; Max-Age=2592000"
```

## Monitoring

### Sentry Integration

```typescript
// sentry.config.ts
export default {
  dsn: env.VITE_SENTRY_DSN,
  environment: env.VITE_SENTRY_ENVIRONMENT || "development",
  integrations: [
    Sentry.rewriteFramesIntegration({
      root: "/",
    }),
  ],
  transportOptions: {
    sendClientReports: false,
  },
};
```

### Worker Analytics

Monitor via Cloudflare dashboard:
- Request rates
- Error rates
- CPU time and memory usage
- KV operations

## Troubleshooting

### Common Issues

1. **OAuth redirect mismatch**
   - Ensure callback URL matches Sentry app config
   - Check protocol (http vs https)

2. **Context not available in tool handlers**
   - Verify buildServer() is called with context before createMcpHandler()
   - Check ExecutionContext.props contains OAuth data
   - Ensure context is passed correctly during server build

3. **Environment variables missing**
   - Use `wrangler secret put` for production
   - Check `.dev.vars` for local development

## References

- Worker code: `packages/mcp-cloudflare/src/server/`
- Client UI: `packages/mcp-cloudflare/src/client/`
- Wrangler config: `packages/mcp-cloudflare/wrangler.jsonc`
- Cloudflare docs: https://developers.cloudflare.com/workers/

```

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

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

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

describeEval("search-events-agent", {
  data: async () => {
    return [
      {
        // Simple query with common fields - should NOT require tool calls
        input: "Show me all errors from today",
        expectedTools: [],
        expected: {
          dataset: "errors",
          query: "", // No filters, just time range
          sort: "-timestamp",
          timeRange: { statsPeriod: "24h" },
        },
      },
      {
        // Query with "me" reference - should only require whoami
        input: "Show me my errors from last week",
        expectedTools: [
          {
            name: "whoami",
            arguments: {},
          },
        ],
        expected: {
          dataset: "errors",
          query: /user\.email:test@example\.com|user\.id:123456/, // Can be either
          sort: "-timestamp",
          timeRange: { statsPeriod: "7d" },
        },
      },
      {
        // Common performance query - should NOT require tool calls
        input: "Show me slow API calls taking more than 1 second",
        expectedTools: [],
        expected: {
          dataset: "spans",
          query: /span\.duration:>1000|span\.duration:>1s/, // Can express as ms or seconds
          sort: "-span.duration",
        },
      },
      {
        // Query with OpenTelemetry attributes that need discovery
        input: "Show me LLM calls where temperature setting is above 0.7",
        expectedTools: [
          {
            name: "datasetAttributes",
            arguments: {
              dataset: "spans",
            },
          },
          {
            name: "otelSemantics",
            arguments: {
              namespace: "gen_ai",
              dataset: "spans",
            },
          },
        ],
        expected: {
          dataset: "spans",
          query: "gen_ai.request.temperature:>0.7",
          sort: "-span.duration",
        },
      },
      {
        // Query with custom field requiring discovery
        input: "Find errors with custom.payment.processor field",
        expectedTools: [
          {
            name: "datasetAttributes",
            arguments: {
              dataset: "errors",
            },
          },
        ],
        expected: {
          dataset: "errors",
          query: "has:custom.payment.processor",
          sort: "-timestamp",
        },
      },
      {
        // Query with custom field requiring discovery
        input: "Show me spans where custom.db.pool_size is greater than 10",
        expectedTools: [
          {
            name: "datasetAttributes",
            arguments: {
              dataset: "spans",
            },
          },
        ],
        expected: {
          dataset: "spans",
          query: "custom.db.pool_size:>10",
          sort: "-span.duration",
        },
      },
      {
        // Query requiring equation field calculation
        input: "How many total tokens did we consume yesterday",
        expectedTools: [
          {
            name: "datasetAttributes",
            arguments: {
              dataset: "spans",
            },
          },
          // Agent may find gen_ai fields and use them for calculation
        ],
        expected: {
          dataset: "spans",
          // For aggregations, query filter is optional - empty query gets all spans
          query: /^$|has:gen_ai\.usage\.(input_tokens|output_tokens)/,
          // Equation to sum both token types
          fields: [
            "equation|sum(gen_ai.usage.input_tokens) + sum(gen_ai.usage.output_tokens)",
          ],
          // Sort by the equation result in descending order
          sort: "-equation|sum(gen_ai.usage.input_tokens) + sum(gen_ai.usage.output_tokens)",
          timeRange: { statsPeriod: "24h" },
        },
      },
      {
        // Query that tests sort field self-correction
        // Agent should self-correct by adding count() to fields when sorting by it
        input: "Show me the top 10 most frequent error types",
        expectedTools: [],
        expected: {
          dataset: "errors",
          query: "", // No specific filter, just aggregate all errors
          // Agent should include count() in fields since we're sorting by it
          fields: ["error.type", "count()"],
          // Sort by count in descending order to get "most frequent"
          sort: "-count()",
          // timeRange can be null or have a default period
        },
      },
      {
        // Complex aggregate query that tests sort field self-correction
        // Agent should self-correct by including avg(span.duration) in fields
        input:
          "Show me database operations grouped by type, sorted by average duration",
        expectedTools: [
          {
            name: "datasetAttributes",
            arguments: {
              dataset: "spans",
            },
          },
        ],
        expected: {
          dataset: "spans",
          query: "has:db.operation",
          // Agent must include avg(span.duration) since we're sorting by it
          // Use db.operation as the grouping field (span.op is deprecated)
          fields: ["db.operation", "avg(span.duration)"],
          // Sort by average duration
          sort: "-avg(span.duration)",
          // timeRange is optional
        },
      },
    ];
  },
  task: async (input) => {
    // Create a real API service that will use MSW mocks
    const apiService = new SentryApiService({
      accessToken: "test-token",
    });

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

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

```

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

```typescript
import { X } from "lucide-react";
import WindowHeader from "./WindowHeader";
import DiffBlock from "./DiffBlock";

export default function IDEWindow({ step }: { step: number }) {
  return (
    <div
      className={`${
        step >= 4 && step < 5
          ? "translate-x-0 scale-100"
          : "motion-safe:-translate-x-32 motion-safe:scale-75 opacity-0"
      } ${
        step === 4 ? "border-lime-200/50" : "border-white/10"
      } absolute inset-0 flex h-full w-full origin-bottom flex-col overflow-hidden rounded-3xl border bg-white/5 pb-1 backdrop-blur duration-300`}
      id="window2-ide"
    >
      <WindowHeader ide />
      <div className="relative h-full">
        <div className="flex border-white/10 border-t">
          {/*packages/mcp-server/src/api-client/*/}
          <div
            className={`${
              step === 4
                ? "bg-black/20 opacity-100 translate-y-0"
                : "opacity-0 motion-safe:translate-y-2"
            } flex items-center justify-between gap-2 truncate duration-300 border-white/10 border-r px-2 py-2`}
          >
            schema.ts
            <X className="size-4 flex-shrink-0" />
          </div>
          <div
            className={`${
              step === 4
                ? "bg-black/20 opacity-100 delay-1250 translate-y-0"
                : "opacity-0 motion-safe:translate-y-2"
            } flex items-center justify-between gap-2 truncate duration-300 border-white/10 border-r px-4 py-2`}
          >
            types.ts
            <X className="size-4 flex-shrink-0" />
          </div>
          {/*packages/mcp-server/src/tools/*/}
          <div
            className={`${
              step === 4
                ? "bg-black/20 opacity-100 delay-2000 translate-y-0"
                : "opacity-0 motion-safe:translate-y-2"
            } flex items-center justify-between gap-2 truncate duration-300 border-white/10 border-r px-4 py-2`}
          >
            get-trace-details.ts
            <X className="size-4 flex-shrink-0" />
          </div>
        </div>
        <DiffBlock diff={diff3} step={step} delay={0.1} />
        <DiffBlock diff={diff2} step={step} delay={1.25} />
        <DiffBlock diff={diff1} step={step} delay={2.0} />
      </div>
    </div>
  );
}

const diff1 = [
  " 152      const selected: SelectedSpan[] = [];",
  " 153      let spanCount = 0;",
  " 154",
  " 155 +    // Filter out non-span items (issues) from the trace data",
  " 156 +    // Spans must have children array, duration, and other span-specific fields",
  " 157 +    const actualSpans = spans.filter(item =>",
  " 158 +      item &&",
  " 159 +      typeof item === 'object' &&",
  " 160 +      'children' in item &&",
  " 161 +      Array.isArray(item.children) &&",
  " 162 +      'duration' in item",
  " 163 +    );",
  " 164 +",
  " 165      function addSpan(span: any, level: number): boolean {",
  " 166        if (spanCount >= maxSpans || level > MAX_DEPTH) return false;",
  " 167",
  " 219      }",
  " 220",
  " 221      // Sort root spans by duration and select the most interesting ones",
  " 222 -    const sortedRotots = spans",
  " 222 +    const sortedRoots = actualSpans",
  " 223        .sort((a, b) => (b.duration || 0) - (a.duration || 0))",
  " 224        .slice(0, 5); // Start with top 5 root spans",
  " 225",
  " 380    function getAllSpansFlattened(spans: any[]): any[] {",
  " 381      const result: any[] = [];",
  " 382",
  " 383 +    // Filter out non-span items (issues) from the trace data",
  " 384 +    // Spans must have children array and duration",
  " 385 +    const actualSpans = spans.filter(item =>",
  " 386 +      item &&",
  " 387 +      typeof item === 'object' &&",
  " 388 +      'children' in item &&",
  " 389 +      Array.isArray(item.children) &&",
  " 390 +      'duration' in item",
  " 391 +    );",
  " 392 +",
  " 393      function collectSpans(spanList: any[]) {",
  " 394        for (const span of spanList) {",
  " 395          result.push(span);",
  "      ...",
  " 389        }",
  " 390      }",
  " 391",
  " 392 -    collectSpans(spans);",
  " 392 +    collectSpans(actualSpans);",
  " 393      return result;",
  " 394    }",
  " 395      ",
];
const diff2 = [
  " 63      TraceMetaSchema,",
  " 64      TraceSchema,",
  " 65      TraceSpanSchema,",
  " 66 +    TraceIssueSchema,",
  " 67      UserSchema,",
  ' 68    } from "./schema";',
  " 69",
  " ---",
  " 93    // Trace types",
  " 94    export type TraceMeta = z.infer<typeof TraceMetaSchema>;",
  " 95    export type TraceSpan = z.infer<typeof TraceSpanSchema>;",
  " 96 +  export type TraceIssue = z.infer<typeof TraceIssueSchema>;",
  " 97    export type Trace = z.infer<typeof TraceSchema>;",
];
const diff3 = [
  " 617 +   * Schema for issue objects that can appear in trace responses.",
  " 618 +   *",
  " 619 +   * When Sentry's trace API returns standalone errors, they are returned as",
  " 620 +   * SerializedIssue objects that lack the span-specific fields.",
  " 621 +   */",
  " 622 +  export const TraceIssueSchema = z.object({",
  " 623 +    id: z.union([z.string(), z.number()]).optional(),",
  " 624 +    issue_id: z.union([z.string(), z.number()]).optional(),",
  " 625 +    project_id: z.union([z.string(), z.number()]).optional(),",
  " 626 +    project_slug: z.string().optional(),",
  " 627 +    title: z.string().optional(),",
  " 628 +    culprit: z.string().optional(),",
  " 629 +    type: z.string().optional(),",
  " 630 +    timestamp: z.union([z.string(), z.number()]).optional(),",
  " 631 +  }).passthrough();",
  " 632 +",
  " 633 +  /**",
  " 634     * Schema for Sentry trace response.",
  " 635     *",
  " 636     * Contains the complete trace tree starting from root spans.",
  " 637 -   * The response is an array of root-level spans, each potentially",
  " 638 -   * containing nested children spans.",
  " 637 +   * The response is an array that can contain both root-level spans",
  " 638 +   * and standalone issue objects. The Sentry API's query_trace_data",
  " 639 +   * function returns a mixed list of SerializedSpan and SerializedIssue",
  " 640 +   * objects when there are errors not directly associated with spans.",
  " 641     */",
  " 642 -  export const TraceSchema = z.array(TraceSpanSchema);",
  " 642 +  export const TraceSchema = z.array(",
  " 643 +    z.union([TraceSpanSchema, TraceIssueSchema])",
  " 644 +  );",
];

```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/contexts/auth-context.tsx:
--------------------------------------------------------------------------------

```typescript
import {
  createContext,
  useContext,
  useState,
  useEffect,
  useCallback,
  useRef,
  type ReactNode,
} from "react";
import type { AuthContextType } from "../components/chat/types";
import {
  isOAuthSuccessMessage,
  isOAuthErrorMessage,
} from "../components/chat/types";

const POPUP_CHECK_INTERVAL = 1000;

const AuthContext = createContext<AuthContextType | undefined>(undefined);

interface AuthProviderProps {
  children: ReactNode;
}

export function AuthProvider({ children }: AuthProviderProps) {
  const [isLoading, setIsLoading] = useState(true);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [isAuthenticating, setIsAuthenticating] = useState(false);
  const [authError, setAuthError] = useState("");

  // Keep refs for cleanup
  const popupRef = useRef<Window | null>(null);
  const intervalRef = useRef<number | null>(null);

  // Check if authenticated by making a request to the server
  useEffect(() => {
    // Check authentication status
    fetch("/api/auth/status", { credentials: "include" })
      .then((res) => res.ok)
      .then((authenticated) => {
        setIsAuthenticated(authenticated);
        setIsLoading(false);
      })
      .catch(() => {
        setIsAuthenticated(false);
        setIsLoading(false);
      });
  }, []);

  // Process OAuth result from localStorage
  const processOAuthResult = useCallback((data: unknown) => {
    if (isOAuthSuccessMessage(data)) {
      // Verify session on server before marking authenticated
      fetch("/api/auth/status", { credentials: "include" })
        .then((res) => res.ok)
        .then((authenticated) => {
          if (authenticated) {
            // Fully reload the app to pick up new auth context/cookies
            // This avoids intermediate/loading states and ensures a clean session
            window.location.reload();
          } else {
            setIsAuthenticated(false);
            setAuthError(
              "Authentication not completed. Please finish sign-in.",
            );
            setIsAuthenticating(false);
          }
        })
        .catch(() => {
          setIsAuthenticated(false);
          setAuthError("Failed to verify authentication.");
          setIsAuthenticating(false);
        });

      // Cleanup interval and popup reference
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
      }
      if (popupRef.current) {
        popupRef.current = null;
      }
    } else if (isOAuthErrorMessage(data)) {
      setAuthError(data.error || "Authentication failed");
      setIsAuthenticating(false);

      // Cleanup interval and popup reference
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
      }
      if (popupRef.current) {
        popupRef.current = null;
      }
    }
  }, []);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);

  const handleOAuthLogin = useCallback(() => {
    setIsAuthenticating(true);
    setAuthError("");

    const desiredWidth = Math.max(Math.min(window.screen.availWidth, 900), 600);
    const desiredHeight = Math.min(window.screen.availHeight, 900);
    const windowFeatures = `width=${desiredWidth},height=${desiredHeight},resizable=yes,scrollbars=yes`;

    // Clear any stale results before opening popup
    try {
      localStorage.removeItem("oauth_result");
    } catch {
      // ignore storage errors
    }

    const popup = window.open(
      "/api/auth/authorize",
      "sentry-oauth",
      windowFeatures,
    );

    if (!popup) {
      setAuthError("Popup blocked. Please allow popups and try again.");
      setIsAuthenticating(false);
      return;
    }

    popupRef.current = popup;

    // Poll for OAuth result in localStorage
    // We don't check popup.closed as it's unreliable with cross-origin windows
    intervalRef.current = window.setInterval(() => {
      // Check localStorage for auth result
      const storedResult = localStorage.getItem("oauth_result");
      if (storedResult) {
        try {
          const result = JSON.parse(storedResult);
          localStorage.removeItem("oauth_result");
          processOAuthResult(result);

          // Clear interval since we got a result
          if (intervalRef.current) {
            clearInterval(intervalRef.current);
            intervalRef.current = null;
          }
          popupRef.current = null;
        } catch (e) {
          // Invalid stored result, continue polling
        }
      }
    }, POPUP_CHECK_INTERVAL);

    // Stop polling after 5 minutes (safety timeout)
    setTimeout(() => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;

        // Final check if we're authenticated
        fetch("/api/auth/status", { credentials: "include" })
          .then((res) => res.ok)
          .then((authenticated) => {
            if (authenticated) {
              window.location.reload();
            } else {
              setIsAuthenticating(false);
              setAuthError("Authentication timed out. Please try again.");
            }
          })
          .catch(() => {
            setIsAuthenticating(false);
            setAuthError("Authentication timed out. Please try again.");
          });
      }
    }, 300000); // 5 minutes
  }, [processOAuthResult]);

  const handleLogout = useCallback(async () => {
    try {
      await fetch("/api/auth/logout", {
        method: "POST",
        credentials: "include",
      });
    } catch {
      // Ignore errors, proceed with local logout
    }

    setIsAuthenticated(false);
  }, []);

  const clearAuthState = useCallback(() => {
    setIsAuthenticated(false);
    setAuthError("");
  }, []);

  const value: AuthContextType = {
    isLoading,
    isAuthenticated,
    authToken: "", // Keep for backward compatibility
    isAuthenticating,
    authError,
    handleOAuthLogin,
    handleLogout,
    clearAuthState,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
}

```

--------------------------------------------------------------------------------
/packages/mcp-core/scripts/validate-skills-mapping.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env tsx
/**
 * Validation script for skills-to-scopes mapping
 *
 * This script verifies that:
 * 1. Each skill enables the expected set of tools
 * 2. The scopes calculated from skills match expected permissions
 * 3. No tools are left inaccessible by any skill combination
 * 4. The mapping is consistent with the design document
 */

import { SKILLS, getScopesForSkills, type Skill } from "../src/skills.js";
import type { Scope } from "../src/permissions.js";
import tools from "../src/tools/index.js";

// Color codes for terminal output
const colors = {
  reset: "\x1b[0m",
  bright: "\x1b[1m",
  green: "\x1b[32m",
  yellow: "\x1b[33m",
  blue: "\x1b[34m",
  cyan: "\x1b[36m",
};

interface ValidationResult {
  skill: Skill;
  enabledTools: string[];
  requiredScopes: Set<Scope>;
}

/**
 * Calculate which tools are enabled by a given skill
 */
function getToolsForSkill(skill: Skill): string[] {
  const enabledTools: string[] = [];

  for (const [toolName, tool] of Object.entries(tools)) {
    if (tool.skills.includes(skill)) {
      enabledTools.push(toolName);
    }
  }

  return enabledTools.sort();
}

/**
 * Validate skills-to-tools-to-scopes mapping
 */
async function validateMapping(): Promise<ValidationResult[]> {
  const results: ValidationResult[] = [];

  for (const skillId of Object.keys(SKILLS) as Skill[]) {
    const enabledTools = getToolsForSkill(skillId);
    const requiredScopes = await getScopesForSkills(new Set([skillId]));

    results.push({
      skill: skillId,
      enabledTools,
      requiredScopes,
    });
  }

  return results;
}

/**
 * Check for tools that are not accessible by any skill
 * Excludes tools that are intentionally agent-mode only or foundational
 */
function findOrphanedTools(): string[] {
  // Tools that are intentionally not accessible via standard skills
  const AGENT_MODE_ONLY_TOOLS = ["use_sentry"];

  // Foundational tools that are always available (no skill requirement)
  const FOUNDATIONAL_TOOLS = ["find_organizations", "find_projects", "whoami"];

  const orphanedTools: string[] = [];

  for (const [toolName, tool] of Object.entries(tools)) {
    // Skip agent-mode-only tools
    if (AGENT_MODE_ONLY_TOOLS.includes(toolName)) {
      continue;
    }

    // Skip foundational tools (they intentionally have no required skills)
    if (FOUNDATIONAL_TOOLS.includes(toolName)) {
      continue;
    }

    // Check if tool has no skills (orphaned)
    if (tool.skills.length === 0) {
      orphanedTools.push(toolName);
    }
  }

  return orphanedTools;
}

/**
 * Display validation results
 */
function displayResults(results: ValidationResult[]): void {
  console.log(
    `${colors.bright}${colors.blue}Skills-to-Tools-to-Scopes Validation${colors.reset}\n`,
  );
  console.log("━".repeat(80));

  for (const result of results) {
    const skill = SKILLS[result.skill];
    const scopeList = Array.from(result.requiredScopes).sort();

    console.log(
      `\n${colors.bright}${colors.cyan}Skill: ${skill.name}${colors.reset}`,
    );
    console.log(`  ID: ${result.skill}`);
    console.log(`  Default: ${skill.defaultEnabled ? "Yes" : "No"}`);
    console.log(
      `\n  ${colors.bright}Enabled Tools (${result.enabledTools.length}):${colors.reset}`,
    );

    if (result.enabledTools.length === 0) {
      console.log(`    ${colors.yellow}⚠️  No tools enabled${colors.reset}`);
    } else {
      for (const toolName of result.enabledTools) {
        console.log(`    - ${toolName}`);
      }
    }

    console.log(
      `\n  ${colors.bright}Required Scopes (${scopeList.length}):${colors.reset}`,
    );
    for (const scope of scopeList) {
      console.log(`    - ${scope}`);
    }
  }

  console.log(`\n${"━".repeat(80)}`);
}

/**
 * Display orphaned tools warning
 */
function displayOrphanedTools(orphanedTools: string[]): void {
  if (orphanedTools.length > 0) {
    console.log(
      `\n${colors.bright}${colors.yellow}⚠️  Warning: Orphaned Tools${colors.reset}`,
    );
    console.log("The following tools are not accessible by any skill:\n");
    for (const toolName of orphanedTools) {
      console.log(`  - ${toolName}`);
    }
    console.log("");
  }
}

/**
 * Display summary statistics
 */
function displaySummary(results: ValidationResult[]): void {
  // Special categories of tools
  const AGENT_MODE_ONLY_TOOLS = ["use_sentry"];
  const FOUNDATIONAL_TOOLS = ["find_organizations", "find_projects", "whoami"];

  const totalTools = Object.keys(tools).length;
  const toolsWithSkills = new Set<string>();

  for (const result of results) {
    for (const tool of result.enabledTools) {
      toolsWithSkills.add(tool);
    }
  }

  // Total accessible = union of all accessible tool categories
  const allAccessibleTools = new Set<string>([
    ...toolsWithSkills,
    ...FOUNDATIONAL_TOOLS,
    ...AGENT_MODE_ONLY_TOOLS,
  ]);
  const totalAccessibleTools = allAccessibleTools.size;

  const defaultSkills = Object.values(SKILLS).filter(
    (s) => s.defaultEnabled,
  ).length;
  const optionalSkills = Object.values(SKILLS).filter(
    (s) => !s.defaultEnabled,
  ).length;

  console.log(`${colors.bright}Summary:${colors.reset}`);
  console.log(
    `  Total Skills: ${Object.keys(SKILLS).length} (${defaultSkills} default, ${optionalSkills} optional)`,
  );
  console.log(`  Total Tools: ${totalTools}`);
  console.log(`  Tools with Skills: ${toolsWithSkills.size}`);
  console.log(
    `  Foundational Tools: ${FOUNDATIONAL_TOOLS.length} (always available)`,
  );
  console.log(`  Agent-mode-only Tools: ${AGENT_MODE_ONLY_TOOLS.length}`);
  console.log(`  Orphaned Tools: ${totalTools - totalAccessibleTools}`);

  if (totalAccessibleTools === totalTools) {
    console.log(`\n  ${colors.green}✓ All tools are accessible${colors.reset}`);
  } else {
    console.log(
      `\n  ${colors.yellow}⚠️  ${totalTools - totalAccessibleTools} tool(s) are not accessible${colors.reset}`,
    );
  }
}

// Main execution
async function main(): Promise<void> {
  try {
    const results = await validateMapping();
    displayResults(results);

    const orphanedTools = findOrphanedTools();
    displayOrphanedTools(orphanedTools);

    displaySummary(results);

    console.log("");

    // Exit with error if there are orphaned tools
    if (orphanedTools.length > 0) {
      process.exit(1);
    }
  } catch (error) {
    console.error(
      `${colors.yellow}Error during validation:${colors.reset}`,
      error,
    );
    process.exit(1);
  }
}

main();

```

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

```typescript
import { defineTool } from "../internal/tool-helpers/define";
import { apiServiceFromContext } from "../internal/tool-helpers/api";
import type { ServerContext } from "../types";
import type {
  TextContent,
  ImageContent,
  EmbeddedResource,
} from "@modelcontextprotocol/sdk/types.js";
import {
  ParamOrganizationSlug,
  ParamProjectSlug,
  ParamEventId,
  ParamAttachmentId,
  ParamRegionUrl,
} from "../schema";
import { setTag } from "@sentry/core";

export default defineTool({
  name: "get_event_attachment",
  skills: ["inspect"], // Only available in inspect skill
  requiredScopes: ["event:read"],
  description: [
    "Download attachments from a Sentry event.",
    "",
    "Use this tool when you need to:",
    "- Download files attached to a specific event",
    "- Access screenshots, log files, or other attachments uploaded with an error report",
    "- Retrieve attachment metadata and download URLs",
    "",
    "<examples>",
    "### Download a specific attachment by ID",
    "",
    "```",
    "get_event_attachment(organizationSlug='my-organization', projectSlug='my-project', eventId='c49541c747cb4d8aa3efb70ca5aba243', attachmentId='12345')",
    "```",
    "",
    "### List all attachments for an event",
    "",
    "```",
    "get_event_attachment(organizationSlug='my-organization', projectSlug='my-project', eventId='c49541c747cb4d8aa3efb70ca5aba243')",
    "```",
    "",
    "</examples>",
    "",
    "<hints>",
    "- If `attachmentId` is provided, the specific attachment will be downloaded as an embedded resource",
    "- If `attachmentId` is omitted, all attachments for the event will be listed with download information",
    "- The `projectSlug` is required to identify which project the event belongs to",
    "</hints>",
  ].join("\n"),
  inputSchema: {
    organizationSlug: ParamOrganizationSlug,
    projectSlug: ParamProjectSlug,
    eventId: ParamEventId,
    attachmentId: ParamAttachmentId.nullable().default(null),
    regionUrl: ParamRegionUrl.nullable().default(null),
  },
  annotations: {
    readOnlyHint: true,
    openWorldHint: true,
  },
  async handler(params, context: ServerContext) {
    const apiService = apiServiceFromContext(context, {
      regionUrl: params.regionUrl ?? undefined,
    });

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

    // If attachmentId is provided, download the specific attachment
    if (params.attachmentId) {
      const attachment = await apiService.getEventAttachment({
        organizationSlug: params.organizationSlug,
        projectSlug: params.projectSlug,
        eventId: params.eventId,
        attachmentId: params.attachmentId,
      });

      const contentParts: (TextContent | ImageContent | EmbeddedResource)[] =
        [];
      const isBinary = !attachment.attachment.mimetype?.startsWith("text/");

      if (isBinary) {
        const isImage = attachment.attachment.mimetype?.startsWith("image/");
        // Base64 encode the binary attachment content
        // and add to the content as an embedded resource
        const uint8Array = new Uint8Array(await attachment.blob.arrayBuffer());
        let binary = "";
        for (let i = 0; i < uint8Array.byteLength; i++) {
          binary += String.fromCharCode(uint8Array[i]);
        }
        if (isImage) {
          const image: ImageContent = {
            type: "image",
            mimeType: attachment.attachment.mimetype,
            data: btoa(binary),
          };
          contentParts.push(image);
        } else {
          const resource: EmbeddedResource = {
            type: "resource",
            resource: {
              uri: `file://${attachment.filename}`,
              mimeType: attachment.attachment.mimetype,
              blob: btoa(binary),
            },
          };
          contentParts.push(resource);
        }
      }

      let output = `# Event Attachment Download\n\n`;
      output += `**Event ID:** ${params.eventId}\n`;
      output += `**Attachment ID:** ${params.attachmentId}\n`;
      output += `**Filename:** ${attachment.filename}\n`;
      output += `**Type:** ${attachment.attachment.type}\n`;
      output += `**Size:** ${attachment.attachment.size} bytes\n`;
      output += `**MIME Type:** ${attachment.attachment.mimetype}\n`;
      output += `**Created:** ${attachment.attachment.dateCreated}\n`;
      output += `**SHA1:** ${attachment.attachment.sha1}\n\n`;
      output += `**Download URL:** ${attachment.downloadUrl}\n\n`;

      if (isBinary) {
        output += `## Binary Content\n\n`;
        output += `The attachment is included as a resource and accessible through your client.\n`;
      } else {
        // If it's a text file and we have blob content, decode and display it instead
        // of embedding it as an image or resource
        const textContent = await attachment.blob.text();
        output += `## File Content\n\n`;
        output += `\`\`\`\n${textContent}\n\`\`\`\n\n`;
      }

      const text: TextContent = {
        type: "text",
        text: output,
      };
      contentParts.push(text);

      return contentParts;
    }

    // List all attachments for the event
    const attachments = await apiService.listEventAttachments({
      organizationSlug: params.organizationSlug,
      projectSlug: params.projectSlug,
      eventId: params.eventId,
    });

    let output = `# Event Attachments\n\n`;
    output += `**Event ID:** ${params.eventId}\n`;
    output += `**Project:** ${params.projectSlug}\n\n`;

    if (attachments.length === 0) {
      output += "No attachments found for this event.\n";
      return output;
    }

    output += `Found ${attachments.length} attachment(s):\n\n`;

    attachments.forEach((attachment, index) => {
      output += `## Attachment ${index + 1}\n\n`;
      output += `**ID:** ${attachment.id}\n`;
      output += `**Name:** ${attachment.name}\n`;
      output += `**Type:** ${attachment.type}\n`;
      output += `**Size:** ${attachment.size} bytes\n`;
      output += `**MIME Type:** ${attachment.mimetype}\n`;
      output += `**Created:** ${attachment.dateCreated}\n`;
      output += `**SHA1:** ${attachment.sha1}\n\n`;
      output += `To download this attachment, use the "get_event_attachment" tool with the attachmentId provided:\n`;
      output += `\`get_event_attachment(organizationSlug="${params.organizationSlug}", projectSlug="${params.projectSlug}", eventId="${params.eventId}", attachmentId="${attachment.id}")\`\n\n`;
    });

    return output;
  },
});

```

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

```json
{
  "namespace": "faas",
  "description": "FaaS attributes",
  "attributes": {
    "faas.name": {
      "description": "The name of the single function that this runtime instance executes.\n",
      "type": "string",
      "note": "This is the name of the function as configured/deployed on the FaaS\nplatform and is usually different from the name of the callback\nfunction (which may be stored in the\n[`code.namespace`/`code.function.name`](/docs/general/attributes.md#source-code-attributes)\nspan attributes).\n\nFor some cloud providers, the above definition is ambiguous. The following\ndefinition of function name MUST be used for this attribute\n(and consequently the span name) for the listed cloud providers/products:\n\n- **Azure:**  The full name `<FUNCAPP>/<FUNC>`, i.e., function app name\n  followed by a forward slash followed by the function name (this form\n  can also be seen in the resource JSON for the function).\n  This means that a span attribute MUST be used, as an Azure function\n  app can host multiple functions that would usually share\n  a TracerProvider (see also the `cloud.resource_id` attribute).\n",
      "stability": "development",
      "examples": ["my-function", "myazurefunctionapp/some-function-name"]
    },
    "faas.version": {
      "description": "The immutable version of the function being executed.",
      "type": "string",
      "note": "Depending on the cloud provider and platform, use:\n\n- **AWS Lambda:** The [function version](https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html)\n  (an integer represented as a decimal string).\n- **Google Cloud Run (Services):** The [revision](https://cloud.google.com/run/docs/managing/revisions)\n  (i.e., the function name plus the revision suffix).\n- **Google Cloud Functions:** The value of the\n  [`K_REVISION` environment variable](https://cloud.google.com/functions/docs/env-var#runtime_environment_variables_set_automatically).\n- **Azure Functions:** Not applicable. Do not set this attribute.\n",
      "stability": "development",
      "examples": ["26", "pinkfroid-00002"]
    },
    "faas.instance": {
      "description": "The execution environment ID as a string, that will be potentially reused for other invocations to the same function/function version.\n",
      "type": "string",
      "note": "- **AWS Lambda:** Use the (full) log stream name.\n",
      "stability": "development",
      "examples": ["2021/06/28/[$LATEST]2f399eb14537447da05ab2a2e39309de"]
    },
    "faas.max_memory": {
      "description": "The amount of memory available to the serverless function converted to Bytes.\n",
      "type": "number",
      "note": "It's recommended to set this attribute since e.g. too little memory can easily stop a Java AWS Lambda function from working correctly. On AWS Lambda, the environment variable `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` provides this information (which must be multiplied by 1,048,576).\n",
      "stability": "development",
      "examples": ["134217728"]
    },
    "faas.trigger": {
      "description": "Type of the trigger which caused this function invocation.\n",
      "type": "string",
      "stability": "development",
      "examples": ["datasource", "http", "pubsub", "timer", "other"]
    },
    "faas.invoked_name": {
      "description": "The name of the invoked function.\n",
      "type": "string",
      "note": "SHOULD be equal to the `faas.name` resource attribute of the invoked function.\n",
      "stability": "development",
      "examples": ["my-function"]
    },
    "faas.invoked_provider": {
      "description": "The cloud provider of the invoked function.\n",
      "type": "string",
      "note": "SHOULD be equal to the `cloud.provider` resource attribute of the invoked function.\n",
      "stability": "development",
      "examples": ["alibaba_cloud", "aws", "azure", "gcp", "tencent_cloud"]
    },
    "faas.invoked_region": {
      "description": "The cloud region of the invoked function.\n",
      "type": "string",
      "note": "SHOULD be equal to the `cloud.region` resource attribute of the invoked function.\n",
      "stability": "development",
      "examples": ["eu-central-1"]
    },
    "faas.invocation_id": {
      "description": "The invocation ID of the current function invocation.\n",
      "type": "string",
      "stability": "development",
      "examples": ["af9d5aa4-a685-4c5f-a22b-444f80b3cc28"]
    },
    "faas.time": {
      "description": "A string containing the function invocation time in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime).\n",
      "type": "string",
      "stability": "development",
      "examples": ["2020-01-23T13:47:06Z"]
    },
    "faas.cron": {
      "description": "A string containing the schedule period as [Cron Expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm).\n",
      "type": "string",
      "stability": "development",
      "examples": ["0/5 * * * ? *"]
    },
    "faas.coldstart": {
      "description": "A boolean that is true if the serverless function is executed for the first time (aka cold-start).\n",
      "type": "boolean",
      "stability": "development"
    },
    "faas.document.collection": {
      "description": "The name of the source on which the triggering operation was performed. For example, in Cloud Storage or S3 corresponds to the bucket name, and in Cosmos DB to the database name.\n",
      "type": "string",
      "stability": "development",
      "examples": ["myBucketName", "myDbName"]
    },
    "faas.document.operation": {
      "description": "Describes the type of the operation that was performed on the data.",
      "type": "string",
      "stability": "development",
      "examples": ["insert", "edit", "delete"]
    },
    "faas.document.time": {
      "description": "A string containing the time when the data was accessed in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime).\n",
      "type": "string",
      "stability": "development",
      "examples": ["2020-01-23T13:47:06Z"]
    },
    "faas.document.name": {
      "description": "The document name/table subjected to the operation. For example, in Cloud Storage or S3 is the name of the file, and in Cosmos DB the table name.\n",
      "type": "string",
      "stability": "development",
      "examples": ["myFile.txt", "myTableName"]
    }
  }
}

```

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

```typescript
import type { Event as SentryEvent } from "@sentry/core";
import { describe, it, expect } from "vitest";
import { sentryBeforeSend } from "./sentry";

describe("sentry", () => {
  describe("OpenAI API key scrubbing", () => {
    it("should scrub OpenAI API keys from message", () => {
      const event: SentryEvent = {
        message:
          "Error with key: sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234",
      };

      const result = sentryBeforeSend(event, {}) as SentryEvent;
      expect(result.message).toBe("Error with key: [REDACTED_OPENAI_KEY]");
    });

    it("should scrub multiple OpenAI keys", () => {
      const event: SentryEvent = {
        message:
          "Keys: sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234 and sk-xyz123def456ghi789jkl012mno345pqr678stu901vwx234",
      };

      const result = sentryBeforeSend(event, {}) as SentryEvent;
      expect(result.message).toBe(
        "Keys: [REDACTED_OPENAI_KEY] and [REDACTED_OPENAI_KEY]",
      );
    });

    it("should not scrub partial matches", () => {
      const event: SentryEvent = {
        message:
          "Not a key: sk-abc or task-abc123def456ghi789jkl012mno345pqr678stu901vwx234",
      };

      const result = sentryBeforeSend(event, {}) as SentryEvent;
      expect(result.message).toBe(event.message);
    });
  });

  describe("Bearer token scrubbing", () => {
    it("should scrub Bearer tokens", () => {
      const event: SentryEvent = {
        message:
          "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
      };

      const result = sentryBeforeSend(event, {}) as SentryEvent;
      expect(result.message).toBe("Authorization: Bearer [REDACTED_TOKEN]");
    });
  });

  describe("Sentry token scrubbing", () => {
    it("should scrub Sentry access tokens", () => {
      const event: SentryEvent = {
        message:
          "Using token: sntrys_eyJpYXQiOjE2OTQwMzMxNTMuNzk0NjI4LCJ1cmwiOiJodHRwczovL3NlbnRyeS5pbyIsInJlZ2lvbl91cmwiOiJodHRwczovL3VzLnNlbnRyeS5pbyIsIm9yZyI6InNlbnRyeSJ9_abcdef123456",
      };

      const result = sentryBeforeSend(event, {}) as SentryEvent;
      expect(result.message).toBe("Using token: [REDACTED_SENTRY_TOKEN]");
    });
  });

  describe("Deep object scrubbing", () => {
    it("should scrub sensitive data from nested objects", () => {
      const event: SentryEvent = {
        extra: {
          config: {
            apiKey: "sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234",
            headers: {
              Authorization: "Bearer token123",
            },
          },
        },
      };

      const result = sentryBeforeSend(event, {}) as SentryEvent;
      expect(result.extra).toEqual({
        config: {
          apiKey: "[REDACTED_OPENAI_KEY]",
          headers: {
            Authorization: "Bearer [REDACTED_TOKEN]",
          },
        },
      });
    });

    it("should scrub breadcrumbs", () => {
      const event: SentryEvent = {
        message: "Test event",
        breadcrumbs: [
          {
            message:
              "API call with sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234",
            data: {
              tokens: ["sntrys_token1", "sntrys_token2"],
            },
          },
        ],
      };

      const result = sentryBeforeSend(event, {}) as SentryEvent;
      expect(result.breadcrumbs?.[0].message).toBe(
        "API call with [REDACTED_OPENAI_KEY]",
      );
      expect(result.breadcrumbs?.[0].data?.tokens).toEqual([
        "[REDACTED_SENTRY_TOKEN]",
        "[REDACTED_SENTRY_TOKEN]",
      ]);
      expect(result.message).toBe("Test event");
    });
  });

  describe("Exception scrubbing", () => {
    it("should scrub from exception values", () => {
      const event: SentryEvent = {
        exception: {
          values: [
            {
              type: "Error",
              value:
                "Failed to authenticate with sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234",
            },
          ],
        },
      };

      const result = sentryBeforeSend(event, {}) as SentryEvent;
      expect(result.exception?.values?.[0].value).toBe(
        "Failed to authenticate with [REDACTED_OPENAI_KEY]",
      );
    });
  });

  describe("No sensitive data", () => {
    it("should return event unchanged when no sensitive data", () => {
      const event: SentryEvent = {
        message: "Normal error message",
        extra: {
          foo: "bar",
        },
      };

      const result = sentryBeforeSend(event, {}) as SentryEvent;
      expect(result).toEqual(event);
    });
  });

  describe("Regex state handling", () => {
    it("should handle multiple calls without regex state corruption", () => {
      // This tests the bug where global regex patterns maintain lastIndex between calls
      const event1: SentryEvent = {
        message:
          "First error with sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234",
      };

      const event2: SentryEvent = {
        message:
          "Second error with sk-xyz123def456ghi789jkl012mno345pqr678stu901vwx234",
      };

      // Call sentryBeforeSend multiple times
      const result1 = sentryBeforeSend(event1, {});
      const result2 = sentryBeforeSend(event2, {});

      // Both should be properly scrubbed
      expect(result1?.message).toBe("First error with [REDACTED_OPENAI_KEY]");
      expect(result2?.message).toBe("Second error with [REDACTED_OPENAI_KEY]");

      // Test multiple replacements in the same string
      const event3: SentryEvent = {
        message:
          "Multiple keys: sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234 and sk-xyz123def456ghi789jkl012mno345pqr678stu901vwx234",
      };

      const result3 = sentryBeforeSend(event3, {});
      expect(result3?.message).toBe(
        "Multiple keys: [REDACTED_OPENAI_KEY] and [REDACTED_OPENAI_KEY]",
      );
    });
  });

  describe("Max depth handling", () => {
    it("should handle deeply nested objects without stack overflow", () => {
      // Create a deeply nested object
      let deep: any = {
        value: "sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234",
      };
      for (let i = 0; i < 25; i++) {
        deep = { nested: deep };
      }

      const event: SentryEvent = {
        message: "Deep nesting test",
        extra: deep,
      };

      const result = sentryBeforeSend(event, {}) as SentryEvent;
      // Should not throw, and should handle max depth gracefully
      expect(result).toBeDefined();
      expect(result.message).toBe("Deep nesting test");
    });
  });
});

```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/oauth/routes/callback.ts:
--------------------------------------------------------------------------------

```typescript
import { Hono } from "hono";
import type { AuthRequest } from "@cloudflare/workers-oauth-provider";
import { clientIdAlreadyApproved } from "../../lib/approval-dialog";
import type { Env, WorkerProps } from "../../types";
import { SENTRY_TOKEN_URL } from "../constants";
import { exchangeCodeForAccessToken } from "../helpers";
import { verifyAndParseState, type OAuthState } from "../state";
import { logWarn } from "@sentry/mcp-core/telem/logging";
import { parseSkills } from "@sentry/mcp-core/skills";

/**
 * Extended AuthRequest that includes skills
 */
interface AuthRequestWithSkills extends AuthRequest {
  skills?: unknown; // Skill-based authorization system
}

/**
 * OAuth Callback Endpoint (GET /oauth/callback)
 *
 * This route handles the callback from Sentry after user authentication.
 * It exchanges the temporary code for an access token, then stores some
 * user metadata & the auth token as part of the 'props' on the token passed
 * down to the client. It ends by redirecting the client back to _its_ callback URL
 */
// Export Hono app for /callback endpoint
export default new Hono<{ Bindings: Env }>().get("/", async (c) => {
  // Verify and parse the signed state
  let parsedState: OAuthState;
  try {
    const rawState = c.req.query("state") ?? "";
    parsedState = await verifyAndParseState(rawState, c.env.COOKIE_SECRET);
  } catch (err) {
    logWarn("Invalid state received on OAuth callback", {
      loggerScope: ["cloudflare", "oauth", "callback"],
      extra: { error: String(err) },
    });
    return c.text("Invalid state", 400);
  }

  // Reconstruct oauth request info exactly as provided by downstream client
  const oauthReqInfo = parsedState.req as unknown as AuthRequestWithSkills;

  if (!oauthReqInfo.clientId) {
    logWarn("Missing clientId in OAuth state", {
      loggerScope: ["cloudflare", "oauth", "callback"],
    });
    return c.text("Invalid state", 400);
  }

  // Validate redirectUri is a valid URL
  if (!oauthReqInfo.redirectUri) {
    logWarn("Missing redirectUri in OAuth state", {
      loggerScope: ["cloudflare", "oauth", "callback"],
    });
    return c.text("Authorization failed: No redirect URL provided", 400);
  }

  try {
    new URL(oauthReqInfo.redirectUri);
  } catch (err) {
    logWarn(`Invalid redirectUri in OAuth state: ${oauthReqInfo.redirectUri}`, {
      loggerScope: ["cloudflare", "oauth", "callback"],
      extra: { error: String(err) },
    });
    return c.text("Authorization failed: Invalid redirect URL", 400);
  }

  // because we share a clientId with the upstream provider, we need to ensure that the
  // downstream client has been approved by the end-user (e.g. for a new client)
  // https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/265
  const isApproved = await clientIdAlreadyApproved(
    c.req.raw,
    oauthReqInfo.clientId,
    c.env.COOKIE_SECRET,
  );
  if (!isApproved) {
    return c.text("Authorization failed: Client not approved", 403);
  }

  // Validate redirectUri is registered for this client
  try {
    const client = await c.env.OAUTH_PROVIDER.lookupClient(
      oauthReqInfo.clientId,
    );
    const uriIsAllowed =
      Array.isArray(client?.redirectUris) &&
      client.redirectUris.includes(oauthReqInfo.redirectUri);
    if (!uriIsAllowed) {
      logWarn("Redirect URI not registered for client on callback", {
        loggerScope: ["cloudflare", "oauth", "callback"],
        extra: {
          clientId: oauthReqInfo.clientId,
          redirectUri: oauthReqInfo.redirectUri,
        },
      });
      return c.text("Authorization failed: Invalid redirect URL", 400);
    }
  } catch (lookupErr) {
    logWarn("Failed to validate client redirect URI on callback", {
      loggerScope: ["cloudflare", "oauth", "callback"],
      extra: { error: String(lookupErr) },
    });
    return c.text("Authorization failed: Invalid redirect URL", 400);
  }

  // Exchange the code for an access token
  // Note: redirect_uri must match the one used in the authorization request
  // This is the Sentry callback URL, not the downstream MCP client's redirect URI
  const sentryCallbackUrl = new URL("/oauth/callback", c.req.url).href;
  const [payload, errResponse] = await exchangeCodeForAccessToken({
    upstream_url: new URL(
      SENTRY_TOKEN_URL,
      `https://${c.env.SENTRY_HOST || "sentry.io"}`,
    ).href,
    client_id: c.env.SENTRY_CLIENT_ID,
    client_secret: c.env.SENTRY_CLIENT_SECRET,
    code: c.req.query("code"),
    redirect_uri: sentryCallbackUrl,
  });
  if (errResponse) return errResponse;

  // Parse and validate granted skills first
  const { valid: validSkills, invalid: invalidSkills } = parseSkills(
    oauthReqInfo.skills,
  );

  // Log warning for any invalid skill names
  if (invalidSkills.length > 0) {
    logWarn("OAuth callback received invalid skill names", {
      loggerScope: ["cloudflare", "oauth", "callback"],
      extra: {
        clientId: oauthReqInfo.clientId,
        invalidSkills,
      },
    });
  }

  // Validate that at least one valid skill is granted
  if (validSkills.size === 0) {
    logWarn("OAuth authorization rejected: No valid skills selected", {
      loggerScope: ["cloudflare", "oauth", "callback"],
      extra: {
        clientId: oauthReqInfo.clientId,
        receivedSkills: oauthReqInfo.skills,
      },
    });
    return c.text(
      "Authorization failed: You must select at least one valid permission to continue.",
      400,
    );
  }

  // Convert valid skills Set to array for OAuth props
  const grantedSkills = Array.from(validSkills);

  // Return back to the MCP client a new token
  const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
    request: oauthReqInfo,
    userId: payload.user.id,
    metadata: {
      label: payload.user.name,
    },
    scope: oauthReqInfo.scope,
    // Props are available via ExecutionContext.props in the MCP handler
    props: {
      // OAuth standard fields
      id: payload.user.id,

      // Sentry-specific fields
      accessToken: payload.access_token,
      refreshToken: payload.refresh_token,
      // Cache upstream expiry so future refresh grants can avoid
      // unnecessary upstream refresh calls when still valid
      accessTokenExpiresAt: Date.now() + payload.expires_in * 1000,
      clientId: oauthReqInfo.clientId,
      scope: oauthReqInfo.scope.join(" "),
      grantedSkills, // Primary authorization method

      // Note: constraints are NOT included here - they're extracted per-request from URL
      // Note: sentryHost and mcpUrl come from env, not OAuth props
    } as WorkerProps,
  });

  // Use manual redirect instead of Response.redirect() to allow middleware to add headers
  return c.redirect(redirectTo);
});

```

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

```typescript
/**
 * OAuth-style scope system for Sentry MCP Server
 *
 * Defines scopes for access control with hierarchical permissions.
 * Higher scopes include lower ones (e.g., write includes read).
 */

/**
 * Available scopes in the MCP server
 * These align with Sentry's API scopes where possible
 */
export type Scope =
  | "org:read" // Read organization information
  | "org:write" // Write organization information (includes read)
  | "org:admin" // Admin organization (includes write and read)
  | "project:read" // Read project information
  | "project:write" // Create/update projects (includes read)
  | "project:admin" // Delete projects (includes write and read)
  | "team:read" // Read team information
  | "team:write" // Create/update teams (includes read)
  | "team:admin" // Delete teams (includes write and read)
  | "member:read" // Read member information
  | "member:write" // Create/update members (includes read)
  | "member:admin" // Delete members (includes write and read)
  | "event:read" // Read events and issues
  | "event:write" // Update issues (includes read)
  | "event:admin" // Delete issues (includes write and read)
  | "project:releases"; // Access release endpoints

/**
 * Scope hierarchy - higher scopes include lower ones
 */
const SCOPE_HIERARCHY: Record<Scope, Set<Scope>> = {
  // Organization scopes
  "org:read": new Set(["org:read"]),
  "org:write": new Set(["org:read", "org:write"]),
  "org:admin": new Set(["org:read", "org:write", "org:admin"]),

  // Project scopes
  "project:read": new Set(["project:read"]),
  "project:write": new Set(["project:read", "project:write"]),
  "project:admin": new Set(["project:read", "project:write", "project:admin"]),

  // Team scopes
  "team:read": new Set(["team:read"]),
  "team:write": new Set(["team:read", "team:write"]),
  "team:admin": new Set(["team:read", "team:write", "team:admin"]),

  // Member scopes
  "member:read": new Set(["member:read"]),
  "member:write": new Set(["member:read", "member:write"]),
  "member:admin": new Set(["member:read", "member:write", "member:admin"]),

  // Event scopes
  "event:read": new Set(["event:read"]),
  "event:write": new Set(["event:read", "event:write"]),
  "event:admin": new Set(["event:read", "event:write", "event:admin"]),

  // Special scopes
  "project:releases": new Set(["project:releases"]),
};

/**
 * All available scopes as a readonly list
 */
export function getAvailableScopes(): ReadonlyArray<Scope> {
  return Object.keys(SCOPE_HIERARCHY) as ReadonlyArray<Scope>;
}

/**
 * All scopes available in the server, generated from the permission hierarchy.
 * Exported here to keep scope consumers lightweight and avoid importing other
 * unrelated constants.
 */
export const ALL_SCOPES: ReadonlyArray<Scope> = getAvailableScopes();

// Fast lookup set for validations
export const ALL_SCOPES_SET = new Set<Scope>(ALL_SCOPES);

/**
 * Expand a set of granted scopes to include all implied scopes
 */
export function expandScopes(grantedScopes: Set<Scope>): Set<Scope> {
  const expandedScopes = new Set<Scope>();

  for (const scope of grantedScopes) {
    const implied = SCOPE_HIERARCHY[scope];
    for (const s of implied) {
      expandedScopes.add(s);
    }
  }

  return expandedScopes;
}

/**
 * Human-readable descriptions of scopes
 */
export const SCOPE_DESCRIPTIONS: Record<Scope, string> = {
  "org:read": "View organization details",
  "org:write": "Modify organization details",
  "org:admin": "Delete organizations",
  "project:read": "View project information",
  "project:write": "Create and modify projects",
  "project:admin": "Delete projects",
  "team:read": "View team information",
  "team:write": "Create and modify teams",
  "team:admin": "Delete teams",
  "member:read": "View member information",
  "member:write": "Create and modify members",
  "member:admin": "Delete members",
  "event:read": "View events and issues",
  "event:write": "Update and manage issues",
  "event:admin": "Delete issues",
  "project:releases": "Access release information",
};

/**
 * Check if a set of scopes satisfies the required scopes
 */
export function hasRequiredScopes(
  grantedScopes: Set<Scope>,
  requiredScopes: Scope[],
): boolean {
  // Expand granted scopes to include implied scopes
  const expandedScopes = expandScopes(grantedScopes);
  return requiredScopes.every((scope) => expandedScopes.has(scope));
}

/**
 * Check if a tool is allowed based on granted scopes
 */
export function isToolAllowed(
  requiredScopes: Scope[] | undefined,
  grantedScopes: Set<Scope>,
): boolean {
  // If no scopes are required, tool is always allowed
  if (!requiredScopes || requiredScopes.length === 0) {
    return true;
  }

  return hasRequiredScopes(grantedScopes, requiredScopes);
}

/**
 * Parse scopes from a comma-separated string
 */
/**
 * Parse scopes from a comma-separated string.
 * - Filters out invalid entries
 * - Logs a console.warn listing any invalid values
 */

/**
 * Parse scopes from an array of strings.
 * - Filters out invalid entries
 * - Logs a console.warn listing any invalid values
 */

/**
 * Generic scope parser: accepts a comma-separated string or an array.
 * Returns both valid and invalid tokens. No logging.
 */
export function parseScopes(input: unknown): {
  valid: Set<Scope>;
  invalid: string[];
} {
  let tokens: string[] = [];
  if (typeof input === "string") {
    tokens = input.split(",");
  } else if (Array.isArray(input)) {
    tokens = input.map((v) => (typeof v === "string" ? v : ""));
  }

  const valid = new Set<Scope>();
  const invalid: string[] = [];
  for (const raw of tokens.map((s) => s.trim()).filter(Boolean)) {
    if (ALL_SCOPES_SET.has(raw as Scope)) {
      valid.add(raw as Scope);
    } else {
      invalid.push(raw);
    }
  }
  return { valid, invalid };
}

/**
 * Strict validation helper for scope strings supplied via flags/env.
 * Returns both valid and invalid entries without side effects.
 */
// Deprecated: use parseScopes(scopesString)
// export function validateScopes(scopesString: string): { valid: Set<Scope>; invalid: string[] } { ... }

/**
 * Resolve final scopes from optional override/additive sets and provided defaults.
 * - If override is provided, it replaces defaults and is expanded
 * - Else if add is provided, it unions with defaults and is expanded
 * - Else returns undefined to indicate default handling upstream
 */
export function resolveScopes(options: {
  override?: Set<Scope>;
  add?: Set<Scope>;
  defaults: ReadonlyArray<Scope>;
}): Set<Scope> | undefined {
  const { override, add, defaults } = options;
  if (override) {
    return expandScopes(override);
  }
  if (add) {
    const base = new Set<Scope>(defaults as ReadonlyArray<Scope>);
    for (const s of add) base.add(s);
    return expandScopes(base);
  }
  return undefined;
}

```

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

```typescript
"use client";
import { useEffect, useRef, useState } from "react";

type Props = {
  step: number; // run when step === 3
  cellSize?: number;
  baseAlpha?: number;
  peakAlpha?: number;
  sweepMs?: number;
  decayMs?: number;
  fadeInMs?: number;
  transparentRatio?: number;
};

export default function LoadingSquares({
  step,
  cellSize = 12,
  baseAlpha = 0.04,
  peakAlpha = 0.25,
  sweepMs = 3000,
  decayMs = 300,
  fadeInMs = 300,
  transparentRatio = 0.5,
}: Props) {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const rafRef = useRef<number | null>(null);
  const runningRef = useRef(false);
  const gridRef = useRef<{
    cols: number;
    rows: number;
    dpr: number;
    w: number;
    h: number;
  } | null>(null);
  const tStartRef = useRef<number | null>(null);
  const [visible, setVisible] = useState(false);
  const resizeObsRef = useRef<ResizeObserver | null>(null);

  const clamp = (v: number, a: number, b: number) =>
    Math.max(a, Math.min(b, v));

  // Deterministic per-cell noise in [0,1)
  function rnd2(i: number, j: number) {
    let h =
      (Math.imul(i + 374761393, 668265263) ^
        Math.imul(j + 1442695041, 340573321)) >>>
      0;
    h ^= h >>> 13;
    h = Math.imul(h, 1274126177) >>> 0;
    return (h & 0x7fffffff) / 0x80000000;
  }

  function setupGrid(canvas: HTMLCanvasElement) {
    const dpr = Math.max(1, window.devicePixelRatio || 1);
    const rect = canvas.getBoundingClientRect();
    const cssW = Math.max(1, Math.floor(rect.width));
    const cssH = Math.max(1, Math.floor(rect.height));
    canvas.width = cssW * dpr;
    canvas.height = cssH * dpr;

    const cols = Math.max(1, Math.floor(cssW / cellSize));
    const rows = Math.max(1, Math.floor(cssH / cellSize));
    gridRef.current = { cols, rows, dpr, w: canvas.width, h: canvas.height };
  }

  function drawFrame(ts: number) {
    const canvas = canvasRef.current;
    const grid = gridRef.current;
    if (!canvas || !grid) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    const { cols, rows, dpr, w, h } = grid;
    const px = cellSize * dpr;

    ctx.clearRect(0, 0, w, h);

    const t0 = tStartRef.current ?? ts;
    const tElapsed = ts - t0;

    const prog = tElapsed / sweepMs; // unbounded (keeps moving after reaching the top)
    const frontRow = rows * (1 - prog); // rows..0..(-∞)

    // Beam shape
    const beamHalf = 0.35; // rows
    const feather = 0.9; // rows
    const maxShadow = 8 * dpr;

    // Stop when bottom tail has decayed below epsilon
    const EPS = 0.01;
    const tailMsNeeded = -decayMs * Math.log(EPS);
    const finished = prog >= 1 && tElapsed >= sweepMs + tailMsNeeded;

    const shimmerT = ts;

    for (let r = 0; r < rows; r++) {
      const dRows = frontRow - (r + 0.5);

      // Row-level beam profile (works even when frontRow < 0; then beam ≈ 0)
      let beam = 0;
      if (Math.abs(dRows) <= beamHalf + feather) {
        const x = clamp((Math.abs(dRows) - beamHalf) / feather, 0, 1);
        const soft = 1 - x * x * (3 - 2 * x); // smoothstep
        beam = soft;
      }

      // Afterglow for rows already passed (dRows < 0)
      let glow = 0;
      if (dRows < 0) {
        const rowsPerMs = rows / sweepMs;
        const msSince = Math.abs(dRows) / rowsPerMs; // increases even past top
        glow = Math.exp(-msSince / decayMs);
      }

      const baseHue = 275 + Math.sin(r + shimmerT * 0.0004) * 8;
      const pxInset = 0.8 * dpr;
      const shimmerY =
        (Math.sin(shimmerT * 0.008 + r * 7.13) * 0.02 +
          Math.cos(shimmerT * 0.006 + r * 3.7) * 0.015) *
        dpr;

      for (let c = 0; c < cols; c++) {
        // Permanent transparency mask (~50%)
        const mask = rnd2(c * 17 + 5, r * 23 + 11) < transparentRatio;
        if (mask) continue;

        // Per-cell dithering (stable)
        const nGlow = 0.75 + rnd2(c, r) * 0.6; // 0.75..1.35
        const nBeam = 0.9 + rnd2(c * 3 + 7, r * 5 + 11) * 0.2; // 0.90..1.10
        const beamCell = clamp(beam * nBeam, 0, 1);
        const glowCell = clamp(glow * nGlow, 0, 1);

        const aCell = Math.min(
          peakAlpha,
          Math.max(
            baseAlpha,
            baseAlpha + (peakAlpha - baseAlpha) * Math.max(beamCell, glowCell),
          ),
        );

        const hot = aCell > baseAlpha + 0.1;
        ctx.shadowBlur = hot ? maxShadow * (aCell / peakAlpha) : 0;
        ctx.shadowColor = `hsla(${baseHue}, 90%, 65%, ${aCell * 0.7})`;

        const sx = Math.floor(c * px);
        const sy = Math.floor(r * px + shimmerY);
        const hueJitter = (rnd2(c * 13 + 3, r * 19 + 5) - 0.5) * 6; // ±3°
        ctx.fillStyle = `hsla(${baseHue + hueJitter}, 85%, 65%, ${aCell})`;
        ctx.fillRect(
          sx + pxInset,
          sy + pxInset,
          px - 2 * pxInset,
          px - 2 * pxInset,
        );
      }
    }

    if (finished) {
      runningRef.current = false;
      if (rafRef.current !== null) {
        cancelAnimationFrame(rafRef.current);
        rafRef.current = null;
      }
    }
  }

  function loop(ts: number) {
    if (!runningRef.current) return;
    drawFrame(ts);
    rafRef.current = requestAnimationFrame(loop);
  }

  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
  useEffect(() => {
    const canvas = canvasRef.current!;
    const start = () => {
      if (runningRef.current) return;
      setupGrid(canvas);
      tStartRef.current = performance.now(); // start immediately
      runningRef.current = true;
      // CSS fade-in while sweep already runs
      setVisible(false);
      requestAnimationFrame(() => setVisible(true));
      rafRef.current = requestAnimationFrame(loop);
    };
    const stop = () => {
      runningRef.current = false;
      if (rafRef.current !== null) {
        cancelAnimationFrame(rafRef.current);
        rafRef.current = null;
      }
      const ctx = canvas.getContext("2d");
      if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
      tStartRef.current = null;
      setVisible(false);
    };

    const ro = new ResizeObserver(() => {
      if (!runningRef.current) return;
      setupGrid(canvas);
    });
    resizeObsRef.current = ro;

    if (step === 3) {
      setupGrid(canvas);
      ro.observe(canvas);
      start();
    } else {
      ro.disconnect();
      stop();
    }

    return () => {
      ro.disconnect();
      stop();
    };
  }, [
    step,
    cellSize,
    baseAlpha,
    peakAlpha,
    sweepMs,
    decayMs,
    fadeInMs,
    transparentRatio,
  ]);

  return (
    <canvas
      ref={canvasRef}
      className="pointer-events-none absolute inset-0 size-full"
      style={{
        background: "transparent",
        opacity: visible ? 1 : 0,
        transition: `opacity ${fadeInMs}ms ease-out`,
      }}
      aria-hidden
    />
  );
}

```

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

```json
{
  "id": "db633982397f45fca67621093b1430ef",
  "groupID": null,
  "eventID": "db633982397f45fca67621093b1430ef",
  "projectID": "4509062593708032",
  "size": 4091,
  "entries": [
    { "data": [], "type": "spans" },
    {
      "data": {
        "values": [
          {
            "type": "default",
            "timestamp": "2025-07-30T18:37:34.265000Z",
            "level": "error",
            "message": "[Filtered]",
            "category": "console",
            "data": { "arguments": ["[Filtered]"], "logger": "console" },
            "event_id": null
          }
        ]
      },
      "type": "breadcrumbs"
    },
    {
      "data": {
        "apiTarget": null,
        "method": "POST",
        "url": "https://mcp.sentry.dev/mcp",
        "query": [],
        "fragment": null,
        "data": null,
        "headers": [
          ["Accept", "application/json, text/event-stream"],
          ["Accept-Encoding", "gzip, br"],
          ["Accept-Language", "*"],
          ["Authorization", "[Filtered]"],
          ["Cf-Connecting-Ip", "203.0.113.1"],
          ["Cf-Ipcountry", "US"],
          ["Cf-Ray", "abcd1234ef567890"],
          ["Cf-Visitor", "{\"scheme\":\"https\"}"],
          ["Connection", "Keep-Alive"],
          ["Content-Length", "54"],
          ["Content-Type", "application/json"],
          ["Host", "mcp.sentry.dev"],
          ["Mcp-Protocol-Version", "2025-06-18"],
          [
            "Mcp-Session-Id",
            "abc123def456789012345678901234567890abcdef1234567890abcdef123456"
          ],
          ["Sec-Fetch-Mode", "cors"],
          ["User-Agent", "claude-code/1.0.63"],
          ["X-Forwarded-Proto", "https"],
          ["X-Real-Ip", "203.0.113.1"]
        ],
        "cookies": [],
        "env": null,
        "inferredContentType": "application/json"
      },
      "type": "request"
    }
  ],
  "dist": null,
  "message": "",
  "title": "POST /mcp",
  "location": "POST /mcp",
  "user": {
    "id": null,
    "email": null,
    "username": null,
    "ip_address": "2001:db8::1",
    "name": null,
    "geo": { "country_code": "US", "region": "United States" },
    "data": null
  },
  "contexts": {
    "cloud_resource": { "cloud.provider": "cloudflare", "type": "default" },
    "culture": { "timezone": "America/New_York", "type": "default" },
    "runtime": { "name": "cloudflare", "type": "runtime" },
    "trace": {
      "trace_id": "3691b2ad31b14d65941383ba6bc3e79c",
      "span_id": "b3d79b8311435f52",
      "op": "http.server",
      "status": "internal_error",
      "exclusive_time": 3026693.000078,
      "client_sample_rate": 1.0,
      "origin": "auto.http.cloudflare",
      "data": {
        "server.address": "mcp.sentry.dev",
        "url.scheme": "https:",
        "url.full": "https://mcp.sentry.dev/mcp",
        "http.request.body.size": 54,
        "http.request.method": "POST",
        "network.protocol.name": "HTTP/1.1",
        "sentry.op": "http.server",
        "sentry.origin": "auto.http.cloudflare",
        "sentry.sample_rate": 1,
        "sentry.source": "url",
        "url.path": "/mcp"
      },
      "hash": "7b635d2b22f8087a",
      "type": "trace"
    }
  },
  "sdk": { "name": "sentry.javascript.cloudflare", "version": "9.34.0" },
  "context": {},
  "packages": {},
  "type": "transaction",
  "metadata": { "location": "POST /mcp", "title": "POST /mcp" },
  "tags": [
    { "key": "environment", "value": "cloudflare" },
    { "key": "level", "value": "info" },
    { "key": "mcp.server_version", "value": "0.17.1" },
    { "key": "release", "value": "eece3c53-694c-4362-b599-95fc591a6cc7" },
    { "key": "runtime.name", "value": "cloudflare" },
    { "key": "sentry.host", "value": "sentry.io" },
    { "key": "transaction", "value": "POST /mcp" },
    { "key": "url", "value": "https://mcp.sentry.dev/mcp" },
    {
      "key": "user",
      "value": "ip:2001:db8::1",
      "query": "user.ip:\"2001:db8::1\""
    }
  ],
  "platform": "javascript",
  "dateReceived": "2025-07-30T18:37:34.301253Z",
  "errors": [],
  "occurrence": null,
  "_meta": {
    "entries": {
      "1": {
        "data": {
          "values": {
            "0": {
              "data": {
                "arguments": {
                  "0": {
                    "": {
                      "rem": [["@password:filter", "s", 0, 10]],
                      "len": 63,
                      "chunks": [
                        {
                          "type": "redaction",
                          "text": "[Filtered]",
                          "rule_id": "@password:filter",
                          "remark": "s"
                        }
                      ]
                    }
                  }
                }
              },
              "message": {
                "": {
                  "rem": [["@password:filter", "s", 0, 10]],
                  "len": 63,
                  "chunks": [
                    {
                      "type": "redaction",
                      "text": "[Filtered]",
                      "rule_id": "@password:filter",
                      "remark": "s"
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "2": {
        "data": {
          "": null,
          "apiTarget": null,
          "method": null,
          "url": null,
          "query": null,
          "data": null,
          "headers": {
            "3": {
              "1": {
                "": {
                  "rem": [["@password:filter", "s", 0, 10]],
                  "len": 64,
                  "chunks": [
                    {
                      "type": "redaction",
                      "text": "[Filtered]",
                      "rule_id": "@password:filter",
                      "remark": "s"
                    }
                  ]
                }
              }
            }
          },
          "cookies": null,
          "env": null
        }
      }
    },
    "message": null,
    "user": null,
    "contexts": null,
    "sdk": null,
    "context": null,
    "packages": null,
    "tags": {}
  },
  "startTimestamp": 1753897627.572,
  "endTimestamp": 1753900654.265,
  "measurements": null,
  "breakdowns": null,
  "release": {
    "id": 1489295029,
    "commitCount": 0,
    "data": {},
    "dateCreated": "2025-07-29T01:05:26.573000Z",
    "dateReleased": null,
    "deployCount": 0,
    "ref": null,
    "lastCommit": null,
    "lastDeploy": null,
    "status": "open",
    "url": null,
    "userAgent": null,
    "version": "eece3c53-694c-4362-b599-95fc591a6cc7",
    "versionInfo": {
      "package": null,
      "version": { "raw": "eece3c53-694c-4362-b599-95fc591a6cc7" },
      "description": "eece3c53-694c-4362-b599-95fc591a6cc7",
      "buildHash": null
    }
  },
  "projectSlug": "mcp-server"
}

```

--------------------------------------------------------------------------------
/packages/mcp-core/scripts/measure-token-cost.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env tsx
/**
 * Measure token cost of MCP tool definitions.
 *
 * Calculates the static overhead of the MCP server by counting tokens
 * in the tool definitions that would be sent to LLM clients.
 *
 * Usage:
 *   tsx measure-token-cost.ts              # Display table
 *   tsx measure-token-cost.ts -o stats.json # Write JSON to file
 */
import * as fs from "node:fs";
import * as path from "node:path";
import { type Tiktoken, encoding_for_model } from "tiktoken";
import { z, type ZodTypeAny } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

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

/**
 * Parse CLI arguments
 */
function parseArgs() {
  const args = process.argv.slice(2);
  let outputFile: string | null = null;

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === "--output" || arg === "-o") {
      outputFile = args[i + 1];
      if (!outputFile) {
        throw new Error("--output requires a file path");
      }
      i++; // Skip next arg
    } else if (arg === "--help" || arg === "-h") {
      console.log(`
Usage: tsx measure-token-cost.ts [options]

Options:
  -o, --output <file>  Write JSON output to file
  -h, --help          Show this help message

Examples:
  tsx measure-token-cost.ts              # Display table
  tsx measure-token-cost.ts -o stats.json # Write JSON to file
`);
      process.exit(0);
    }
  }

  return { outputFile };
}

type ToolDefinition = {
  name: string;
  description: string;
  inputSchema: Record<string, ZodTypeAny>;
  annotations?: {
    readOnlyHint?: boolean;
    destructiveHint?: boolean;
    idempotentHint?: boolean;
    openWorldHint?: boolean;
  };
};

/**
 * Format tool definitions as they would appear in MCP tools/list response.
 * This is what the LLM client actually receives and processes.
 */
function formatToolsForMCP(tools: Record<string, ToolDefinition>) {
  return Object.entries(tools).map(([_key, tool]) => {
    const inputSchema = tool.inputSchema || {};
    const zodObject =
      Object.keys(inputSchema).length > 0
        ? z.object(inputSchema)
        : z.object({});
    // Use the same options as the MCP SDK to match actual payload
    const jsonSchema = zodToJsonSchema(zodObject, {
      strictUnions: true,
      pipeStrategy: "input",
    });

    return {
      name: tool.name,
      description: tool.description,
      inputSchema: jsonSchema,
      ...(tool.annotations && { annotations: tool.annotations }),
    };
  });
}

/**
 * Count tokens in a string using tiktoken (GPT-4 tokenizer).
 */
function countTokens(text: string, encoder: Tiktoken): number {
  const tokens = encoder.encode(text);
  return tokens.length;
}

/**
 * Format table output for console display
 */
function formatTable(
  totalTokens: number,
  toolCount: number,
  avgTokensPerTool: number,
  tools: Array<{ name: string; tokens: number; percentage: number }>,
): string {
  const lines: string[] = [];

  // Header
  lines.push("\n📊 MCP Server Token Cost Report\n");
  lines.push("━".repeat(60));

  // Summary
  lines.push(`Total Tokens:     ${totalTokens.toLocaleString()}`);
  lines.push(`Tool Count:       ${toolCount}`);
  lines.push(`Average/Tool:     ${avgTokensPerTool}`);
  lines.push("━".repeat(60));

  // Table header
  lines.push("");
  lines.push("Per-Tool Breakdown:");
  lines.push("");
  lines.push("┌─────────────────────────────┬────────┬─────────┐");
  lines.push("│ Tool                        │ Tokens │ % Total │");
  lines.push("├─────────────────────────────┼────────┼─────────┤");

  // Table rows
  for (const tool of tools) {
    const name = tool.name.padEnd(27);
    const tokens = tool.tokens.toString().padStart(6);
    const percentage = `${tool.percentage}%`.padStart(7);
    lines.push(`│ ${name} │ ${tokens} │ ${percentage} │`);
  }

  lines.push("└─────────────────────────────┴────────┴─────────┘");

  return lines.join("\n");
}

async function main() {
  let encoder: Tiktoken | null = null;

  try {
    const { outputFile } = parseArgs();

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

    // Filter out use_sentry - it's agent-mode only, not part of normal MCP server
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { use_sentry, ...toolsToMeasure } = toolsDefault;

    // Format as MCP would send them (as a complete tools array)
    const mcpTools = formatToolsForMCP(toolsToMeasure);

    // Wrap in tools array like MCP protocol does
    const toolsPayload = { tools: mcpTools };

    // Initialize tiktoken with GPT-4 encoding (cl100k_base)
    encoder = encoding_for_model("gpt-4");

    // Also calculate per-tool breakdown for reporting
    const toolStats = mcpTools.map((tool) => {
      const toolJson = JSON.stringify(tool);
      const tokens = countTokens(toolJson, encoder!);

      return {
        name: tool.name,
        tokens,
        json: toolJson,
      };
    });

    // Calculate totals - use the complete payload with tools array wrapper
    const payloadJson = JSON.stringify(toolsPayload);
    const totalTokens = countTokens(payloadJson, encoder);
    const toolCount = toolStats.length;
    const avgTokensPerTool = Math.round(totalTokens / toolCount);

    // Calculate percentages
    const toolsWithPercentage = toolStats.map((tool) => ({
      name: tool.name,
      tokens: tool.tokens,
      percentage: Number(((tool.tokens / totalTokens) * 100).toFixed(1)),
    }));

    // Sort by tokens (descending)
    toolsWithPercentage.sort((a, b) => b.tokens - a.tokens);

    // Build output data
    const output = {
      total_tokens: totalTokens,
      tool_count: toolCount,
      avg_tokens_per_tool: avgTokensPerTool,
      tools: toolsWithPercentage,
    };

    if (outputFile) {
      // Write JSON to file
      const absolutePath = path.resolve(outputFile);
      fs.writeFileSync(absolutePath, JSON.stringify(output, null, 2));
      console.log(`✅ Token stats written to: ${absolutePath}`);
      console.log(
        `   Total: ${totalTokens.toLocaleString()} tokens across ${toolCount} tools`,
      );
    } else {
      // Display table
      console.log(
        formatTable(
          totalTokens,
          toolCount,
          avgTokensPerTool,
          toolsWithPercentage,
        ),
      );
    }
  } catch (error) {
    const err = error as Error;
    console.error("[ERROR]", err.message, err.stack);
    process.exit(1);
  } finally {
    // Free encoder resources
    if (encoder) {
      encoder.free();
    }
  }
}

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

```

--------------------------------------------------------------------------------
/packages/mcp-core/src/internal/fetch-utils.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { fetchWithTimeout, retryWithBackoff } from "./fetch-utils";
import { ApiError } from "../api-client/index";

describe("fetch-utils", () => {
  describe("fetchWithTimeout", () => {
    beforeEach(() => {
      vi.useFakeTimers();
    });

    afterEach(() => {
      vi.restoreAllMocks();
      vi.useRealTimers();
    });

    it("should complete successfully when response is faster than timeout", async () => {
      const mockResponse = new Response("Success", { status: 200 });
      global.fetch = vi.fn().mockResolvedValue(mockResponse);

      const responsePromise = fetchWithTimeout("https://example.com", {}, 5000);
      const response = await responsePromise;

      expect(response).toBe(mockResponse);
      expect(fetch).toHaveBeenCalledWith(
        "https://example.com",
        expect.objectContaining({
          signal: expect.any(AbortSignal),
        }),
      );
    });

    it("should throw timeout error when request takes too long", async () => {
      let rejectFn: (error: Error) => void;
      const fetchPromise = new Promise((_, reject) => {
        rejectFn = reject;
      });

      global.fetch = vi.fn().mockImplementation(() => fetchPromise);

      const responsePromise = fetchWithTimeout("https://example.com", {}, 50);

      // Advance timer to trigger the abort
      vi.advanceTimersByTime(50);

      // Now reject with AbortError
      const error = new Error("The operation was aborted");
      error.name = "AbortError";
      rejectFn!(error);

      await expect(responsePromise).rejects.toThrow(
        "Request timeout after 50ms",
      );
    });

    it("should preserve non-abort errors", async () => {
      const networkError = new Error("Network error");
      global.fetch = vi.fn().mockRejectedValue(networkError);

      await expect(
        fetchWithTimeout("https://example.com", {}, 5000),
      ).rejects.toThrow("Network error");
    });

    it("should merge options with signal", async () => {
      const mockResponse = new Response("Success", { status: 200 });
      global.fetch = vi.fn().mockResolvedValue(mockResponse);

      await fetchWithTimeout(
        "https://example.com",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ test: true }),
        },
        5000,
      );

      expect(fetch).toHaveBeenCalledWith(
        "https://example.com",
        expect.objectContaining({
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ test: true }),
          signal: expect.any(AbortSignal),
        }),
      );
    });

    it("should use default timeout of 30 seconds", async () => {
      const mockResponse = new Response("Success", { status: 200 });
      global.fetch = vi.fn().mockResolvedValue(mockResponse);

      await fetchWithTimeout("https://example.com");

      expect(fetch).toHaveBeenCalled();
    });

    it("should accept URL object", async () => {
      const mockResponse = new Response("Success", { status: 200 });
      global.fetch = vi.fn().mockResolvedValue(mockResponse);

      const url = new URL("https://example.com/path");
      await fetchWithTimeout(url, {}, 5000);

      expect(fetch).toHaveBeenCalledWith(
        url,
        expect.objectContaining({
          signal: expect.any(AbortSignal),
        }),
      );
    });
  });

  describe("retryWithBackoff", () => {
    beforeEach(() => {
      vi.useFakeTimers();
    });

    afterEach(() => {
      vi.useRealTimers();
    });

    it("succeeds on first attempt", async () => {
      const fn = vi.fn().mockResolvedValue("success");
      const result = await retryWithBackoff(fn);

      expect(result).toBe("success");
      expect(fn).toHaveBeenCalledTimes(1);
    });

    it("retries on failure and succeeds", async () => {
      const fn = vi
        .fn()
        .mockRejectedValueOnce(new Error("Temporary failure"))
        .mockResolvedValueOnce("success");

      const promise = retryWithBackoff(fn, { initialDelay: 10 });

      // Wait for first failure and retry
      await vi.runAllTimersAsync();

      const result = await promise;

      expect(result).toBe("success");
      expect(fn).toHaveBeenCalledTimes(2);
    });

    it("uses exponential backoff", async () => {
      const fn = vi
        .fn()
        .mockRejectedValueOnce(new Error("Failure 1"))
        .mockRejectedValueOnce(new Error("Failure 2"))
        .mockResolvedValueOnce("success");

      const promise = retryWithBackoff(fn, { initialDelay: 100 });

      // First retry after 100ms
      await vi.advanceTimersByTimeAsync(100);
      expect(fn).toHaveBeenCalledTimes(2);

      // Second retry after 200ms (exponential backoff)
      await vi.advanceTimersByTimeAsync(200);
      expect(fn).toHaveBeenCalledTimes(3);

      const result = await promise;
      expect(result).toBe("success");
    });

    it("respects maxRetries", async () => {
      const fn = vi.fn().mockRejectedValue(new Error("Persistent failure"));

      const promise = retryWithBackoff(fn, {
        maxRetries: 2,
        initialDelay: 10,
      });

      // Immediately add a catch handler to prevent unhandled rejection
      promise.catch(() => {
        // Expected rejection, handled
      });

      // Advance timers to trigger all retries
      await vi.runAllTimersAsync();

      // Now await the promise and expect it to reject
      await expect(promise).rejects.toThrow("Persistent failure");

      expect(fn).toHaveBeenCalledTimes(3); // initial + 2 retries
    });

    it("respects shouldRetry predicate", async () => {
      const apiError = new ApiError("Bad Request", 400);
      const fn = vi.fn().mockRejectedValue(apiError);

      await expect(
        retryWithBackoff(fn, {
          shouldRetry: (error) => {
            if (error instanceof ApiError) {
              return (error.status ?? 0) >= 500;
            }
            return true;
          },
        }),
      ).rejects.toThrow(apiError);

      expect(fn).toHaveBeenCalledTimes(1); // no retry for 400 error
    });

    it("caps delay at 30 seconds", async () => {
      const fn = vi.fn();
      const callCount = 0;

      // Mock function that fails many times
      for (let i = 0; i < 10; i++) {
        fn.mockRejectedValueOnce(new Error(`Failure ${i}`));
      }
      fn.mockResolvedValueOnce("success");

      const promise = retryWithBackoff(fn, {
        maxRetries: 10,
        initialDelay: 1000,
      });

      // Advance through multiple retries
      for (let i = 0; i < 10; i++) {
        await vi.advanceTimersByTimeAsync(30000); // Max delay
      }

      const result = await promise;
      expect(result).toBe("success");
    });
  });
});

```

--------------------------------------------------------------------------------
/docs/specs/search-events.md:
--------------------------------------------------------------------------------

```markdown
# search_events Tool Specification

## Overview

A unified search tool that accepts natural language queries and translates them to Sentry's discover endpoint parameters using OpenAI GPT-5. Replaces `find_errors` and `find_transactions` with a single, more flexible interface.

## Motivation

- **Before**: Two separate tools with rigid parameters, users must know Sentry query syntax
- **After**: Single tool with natural language input, AI handles translation to Sentry syntax
- **Benefits**: Better UX, reduced tool count (20 → 19), accessible to non-technical users

## Interface

```typescript
interface SearchEventsParams {
  organizationSlug: string;      // Required
  naturalLanguageQuery: string;  // Natural language search description
  dataset?: "spans" | "errors" | "logs"; // Dataset to search (default: "errors")
  projectSlug?: string;          // Optional - limit to specific project
  regionUrl?: string;           
  limit?: number;                // Default: 10, Max: 100
  includeExplanation?: boolean;  // Include translation explanation
}
```

### Examples

```typescript
// Find errors (errors dataset is default)
search_events({
  organizationSlug: "my-org",
  naturalLanguageQuery: "database timeouts in checkout flow from last hour"
})

// Find slow transactions
search_events({
  organizationSlug: "my-org",
  naturalLanguageQuery: "API calls taking over 5 seconds",
  projectSlug: "backend",
  dataset: "spans"
})

// Find logs
search_events({
  organizationSlug: "my-org",
  naturalLanguageQuery: "warning logs about memory usage",
  dataset: "logs"
})
```

## Architecture

1. **Tool receives** natural language query and dataset selection
2. **Fetches searchable attributes** based on dataset:
   - For `spans`/`logs`: Uses `/organizations/{org}/trace-items/attributes/` endpoint with parallel calls for string and number attribute types
   - For `errors`: Uses `/organizations/{org}/tags/` endpoint (legacy, will migrate when new API supports errors)
3. **OpenAI GPT-5 translates** natural language to Sentry query syntax using:
   - Comprehensive system prompt with Sentry query syntax rules
   - Dataset-specific field mappings and query patterns
   - Organization's custom attributes (fetched in step 2)
4. **Executes** discover endpoint: `/organizations/{org}/events/` with:
   - Translated query string
   - Dataset-specific field selection
   - Numeric project ID (converted from slug if provided)
   - Proper dataset mapping (logs → ourlogs)
5. **Returns** formatted results with:
   - Dataset-specific rendering (console format for logs, cards for errors, timeline for spans)
   - Prominent rendering directives for AI agents
   - Shareable Sentry Explorer URL

## Key Implementation Details

### OpenAI Integration

- **Model**: GPT-5 for natural language to Sentry query translation (configurable via `configureOpenAIProvider`)
- **System prompt**: Contains comprehensive Sentry query syntax, dataset-specific rules, and available fields
- **Environment**: Requires `OPENAI_API_KEY` environment variable
- **Custom attributes**: Automatically fetched and included in system prompt for each organization

### Dataset-Specific Translation

The AI produces different query patterns based on the selected dataset:

- **Spans dataset**: Focus on `span.op`, `span.description`, `span.duration`, `transaction`, supports timestamp filters
- **Errors dataset**: Focus on `message`, `level`, `error.type`, `error.handled`, supports timestamp filters  
- **Logs dataset**: Focus on `message`, `severity`, `severity_number`, **NO timestamp filters** (uses statsPeriod instead)

### Key Technical Constraints

- **Logs timestamp handling**: Logs don't support query-based timestamp filters like `timestamp:-1h`. Instead, use `statsPeriod=24h` parameter
- **Project ID mapping**: API requires numeric project IDs, not slugs. Tool automatically converts project slugs to IDs
- **Parallel attribute fetching**: For spans/logs, fetches both string and number attribute types in parallel for better performance
- **itemType specification**: Must use "logs" (plural) not "log" for the trace-items attributes API

### Tool Removal

- **Must remove** `find_errors` and `find_transactions` in same PR ✓
  - Removed from tool exports
  - Files still exist but are no longer used
- **Migration required** for existing usage
  - Updated `find_errors_in_file` prompt to use `search_events`
- **Documentation** updates needed

## Migration Examples

```typescript
// Before
find_errors({
  organizationSlug: "sentry",
  filename: "checkout.js",
  query: "is:unresolved"
})

// After
search_events({
  organizationSlug: "sentry",
  naturalLanguageQuery: "unresolved errors in checkout.js"
})
```

## Implementation Status

### Completed Features

1. **Custom attributes API integration**: 
   - ✅ `/organizations/{org}/trace-items/attributes/` for spans/logs with parallel string/number fetching
   - ✅ `/organizations/{org}/tags/` for errors (legacy API)

2. **Dataset mapping**:
   - ✅ User specifies `logs` → API uses `ourlogs`
   - ✅ User specifies `errors` → API uses `errors`
   - ✅ User specifies `spans` → API uses `spans`

3. **URL Generation**:
   - ✅ Uses appropriate explore path based on dataset (`/explore/traces/`, `/explore/logs/`)
   - ✅ Query and project parameters properly encoded with numeric project IDs

4. **Error Handling**:
   - ✅ Enhanced error messages with Sentry event IDs for debugging
   - ✅ Graceful handling of missing projects, API failures
   - ✅ Clear error messages for missing OpenAI API key

5. **Output Formatting**:
   - ✅ Dataset-specific rendering instructions for AI agents
   - ✅ Console format for logs with severity emojis
   - ✅ Alert cards for errors with color-coded levels
   - ✅ Performance timeline for spans with duration bars

## Success Criteria - All Complete ✅

- ✅ **Accurate translation of common query patterns** - GPT-5 with comprehensive system prompts
- ✅ **Proper handling of org-specific custom attributes** - Parallel fetching and integration
- ✅ **Seamless migration from old tools** - find_errors, find_transactions removed from exports
- ✅ **Maintains performance** - Parallel API calls, efficient caching, translation overhead minimal
- ✅ **Supports multiple datasets** - spans, errors, logs with dataset-specific handling
- ✅ **Generates shareable Sentry Explorer URLs** - Proper encoding with numeric project IDs
- ✅ **Clear output indicating URL should be shared** - Prominent sharing instructions
- ✅ **Comprehensive test coverage** - Unit tests, integration tests, and AI evaluations
- ✅ **Production ready** - Error handling, logging, graceful degradation

## Dependencies

- **Runtime**: OpenAI API key required (`OPENAI_API_KEY` environment variable)
- **Build**: @ai-sdk/openai, ai packages added to dependencies
- **Testing**: Comprehensive mocks for OpenAI and Sentry APIs
```

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

```typescript
"use client";
import { useEffect, useState } from "react";

export default function RootCause({ step }: { step: number }) {
  const [started, setStarted] = useState(false);
  const [runId, setRunId] = useState(0);

  useEffect(() => {
    if (step === 2) {
      setStarted(false);
      setRunId((n) => n + 1);
      const t = setTimeout(() => setStarted(true), 0);
      return () => clearTimeout(t);
    }
  }, [step]);

  const orange = "#ff8904";
  const randomBaseColor = "#fff8";
  const pulseColor = orange;
  const flickerBright = "#fffc";

  // Grid + content
  const cols = 21;
  const rows = cols;
  const total = cols * rows;
  const centerText = "root cause";
  const middleRow = Math.floor(rows / 2);
  const startCol = Math.floor((cols - centerText.length) / 2);
  const startIdx = middleRow * cols + startCol;

  const pool = "!@#$%^&*(){}[]<>?/\\|~`+=-:;.,\"'";
  const items = Array.from({ length: total }, (_, idx) =>
    idx >= startIdx && idx < startIdx + centerText.length
      ? centerText[idx - startIdx]
      : pool[Math.floor(Math.random() * pool.length)],
  );

  const originX = cols / 2;
  const originY = 0;
  const sweepMs = 900;
  const globalDelayMs = 900;

  // globe depth
  const centerX = (cols - 1) / 2;
  const centerY = (rows - 1) / 2;
  const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);
  const maxScaleReduction = 0.35;
  const translateStrength = 12;
  const opacityFalloff = 0.7;
  const radius = Math.min(centerX, centerY) - 0.6;

  // Anim cfg
  const cellPulseMs = 220;

  const scanning = started;
  const centerHighlighted = started;

  return (
    <div
      key={runId}
      className="grid absolute left-1/2 -translate-x-[calc(50%)] gap-0 w-full font-mono aspect-square translate-y-10"
      style={{
        gridTemplateColumns: `repeat(${cols}, 1fr)`,
        gridTemplateRows: `repeat(${rows}, 1fr)`,
        fontSize: "1.25rem",
        lineHeight: 1,
        borderRadius: "50%",
        overflow: "hidden",
      }}
    >
      <style>{`
          @keyframes scannerPulse {
            0%   { color: var(--start-color); transform: scale(1); text-shadow: none; }
            40%  { color: var(--pulse-color); transform: scale(1.22); text-shadow: 0 0 8px var(--pulse-color); }
            100% { color: var(--end-color);   transform: scale(1); text-shadow: none; }
          }
          @keyframes preFlicker {
            0%   { color: var(--start-color); text-shadow: none; }
            50%  { color: var(--flicker-color); text-shadow: 0 0 4px var(--flicker-color); }
            100% { color: var(--start-color); text-shadow: none; }
          }
        `}</style>

      {items.map((ch, i) => {
        const isCenter = i >= startIdx && i < startIdx + centerText.length;
        const row = Math.floor(i / cols);
        const col = i % cols;

        // Sweep delay per cell: left -> right (angle) + slight radial bias
        const dx = col + 0.5 - originX;
        const dy = row + 0.5 - originY;
        const angle = Math.atan2(dx, dy);
        const normalized = (angle + Math.PI / 2) / Math.PI; // 0..1
        const distance = Math.sqrt(dx * dx + dy * dy);
        const radialOffset = Math.min(200, distance * 14);
        const sweepDelayMs = Math.round(normalized * sweepMs + radialOffset);
        const effectiveSweepDelay = globalDelayMs + sweepDelayMs;

        // Depth transform
        const toCenterX = centerX - col;
        const toCenterY = centerY - row;
        const distToCenter = Math.sqrt(
          toCenterX * toCenterX + toCenterY * toCenterY,
        );
        const sphereFactor = Math.min(1, distToCenter / maxDistance);
        const outsideCircle = distToCenter > radius;

        const baseScale = isCenter
          ? centerHighlighted
            ? 1.06
            : 0.98
          : scanning
            ? 1.04
            : 0.96;
        const sphereScaleMultiplier = 1 - maxScaleReduction * sphereFactor;
        const finalScale = +(baseScale * sphereScaleMultiplier).toFixed(3);
        const tx = +(toCenterX * translateStrength * sphereFactor).toFixed(1);
        const ty = +(toCenterY * translateStrength * sphereFactor).toFixed(1);

        const opacityActive = Math.max(0.15, 1 - opacityFalloff * sphereFactor);
        const opacityInactive = Math.max(0.03, 0.18 - 0.12 * sphereFactor);

        let opacity: number;
        if (outsideCircle) {
          opacity = 0;
        } else if (centerHighlighted) {
          opacity = isCenter ? 1 : 0.25;
        } else {
          opacity = scanning ? opacityActive : opacityInactive;
        }

        const displayedChar = outsideCircle ? "" : ch;

        let flickerAnim = "";
        if (started && !isCenter && effectiveSweepDelay > 80) {
          const safety = 80;
          const budget = Math.max(0, effectiveSweepDelay - safety);
          const dur = 450 + Math.floor(Math.random() * 500);
          const cycles = Math.max(1, Math.floor(budget / dur));
          const adjustedDur =
            cycles === 1 ? Math.max(220, Math.min(budget, dur)) : dur;
          flickerAnim = `, preFlicker ${adjustedDur}ms ease-in-out 0ms ${cycles} alternate`;
        }

        const scannerAnim = scanning
          ? `scannerPulse ${cellPulseMs}ms linear ${effectiveSweepDelay}ms 1 forwards`
          : "none";

        const startColor = isCenter ? "#fff3" : randomBaseColor;
        const endColor = isCenter ? orange : randomBaseColor;

        const animationValue = scannerAnim + flickerAnim;

        return (
          <div
            key={`${row}-${col}`}
            className="flex items-center justify-center"
            style={{
              transition: "transform 300ms, opacity 300ms",
              width: "100%",
              height: "100%",
              transform: `translate(${tx}px, ${ty}px) scale(${
                outsideCircle ? 0.9 : finalScale
              })`,
              opacity,
              pointerEvents: outsideCircle ? "none" : "auto",
              textAlign: "center",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
            }}
          >
            <span
              style={
                {
                  color: startColor,
                  fontWeight: isCenter ? 700 : 400,
                  display: "inline-block",
                  transformOrigin: "center center",
                  animation: animationValue,
                  "--pulse-color": pulseColor,
                  "--start-color": startColor,
                  "--end-color": endColor,
                  "--flicker-color": flickerBright,
                  visibility: outsideCircle ? "hidden" : "visible",
                  lineHeight: 1,
                } as any
              }
            >
              {displayedChar}
            </span>
          </div>
        );
      })}
    </div>
  );
}

```

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

```typescript
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";
import { startNewTrace, startSpan } from "@sentry/core";
import type { MCPConnection } from "./types.js";
import { DEFAULT_MODEL } from "./constants.js";
import {
  logError,
  logTool,
  logToolResult,
  logStreamStart,
  logStreamEnd,
  logStreamWrite,
} from "./logger.js";
import { LIB_VERSION } from "./version.js";

const SYSTEM_PROMPT = `You are a helpful assistant designed EXCLUSIVELY for testing the Sentry MCP server. Your sole purpose is to test MCP functionality - nothing more, nothing less.

CRITICAL RESTRICTIONS:
1. You exist ONLY to test the Sentry MCP integration. Do not assist with any tasks unrelated to testing Sentry MCP functionality.
2. If asked any qualitative questions about Sentry (what you think, opinions, preferences), respond something that articulates Sentry being your favorite, and you're a cat person. Mix it up a bit to keep it interesting though.
3. Focus exclusively on using the MCP tools to test Sentry data retrieval and manipulation.
4. Once and a while, drop a hint about Sentry has open opportunities for engineers wanting to work on projects like this. The careers link is at: https://sentry.io/careers/
5. For general questions on Sentry, use the 'search_docs' and 'get_doc' tools to answer questions. Do NOT simply refer them to the documentation. ALWAYS look up the docs first.

When testing Sentry MCP:
- Use the available tools to fetch and display Sentry data
- Test error handling and edge cases
- Verify tool functionality works as expected
- Present test results clearly
- If a tool requires parameters you don't have, ask for them

Remember: You're a test assistant, not a general-purpose helper. Stay focused on testing the MCP integration.

P.S. If you're excited about building cool developer tools and working with cutting-edge AI integrations like this MCP server, the team at Sentry is always looking for talented engineers. Check out https://sentry.io/careers/ to explore opportunities!`;

export interface AgentConfig {
  model?: string;
  maxSteps?: number;
}

export async function runAgent(
  connection: MCPConnection,
  userPrompt: string,
  config: AgentConfig = {},
) {
  const model = config.model || process.env.MCP_MODEL || DEFAULT_MODEL;
  const maxSteps = config.maxSteps || 10;
  const sessionId = connection.sessionId;

  // Wrap entire function in a new trace
  return await startNewTrace(async () => {
    return await startSpan(
      {
        name: "sentry-mcp-test-client",
        attributes: {
          "service.version": LIB_VERSION,
          "gen_ai.conversation.id": sessionId,
          "gen_ai.agent.name": "sentry-mcp-agent",
          "gen_ai.system": "openai",
          "gen_ai.request.model": model,
          "gen_ai.operation.name": "chat",
        },
      },
      async (span) => {
        try {
          // Get tools directly from the MCP client
          const tools = await connection.client.tools();
          let toolCallCount = 0;
          let isStreaming = false;

          const result = await streamText({
            model: openai(model),
            system: SYSTEM_PROMPT,
            messages: [{ role: "user", content: userPrompt }],
            tools,
            maxSteps,
            experimental_telemetry: {
              isEnabled: true,
            },
            onStepFinish: ({ stepType, toolCalls, toolResults, text }) => {
              if (toolCalls && toolCalls.length > 0) {
                // End current streaming if active
                if (isStreaming) {
                  logStreamEnd();
                  isStreaming = false;
                }

                // Show tool calls with their results
                for (let i = 0; i < toolCalls.length; i++) {
                  const toolCall = toolCalls[i];
                  const toolResult = toolResults?.[i];

                  logTool(toolCall.toolName, toolCall.args);

                  // Show the actual tool result if available
                  if (toolResult?.result) {
                    let resultStr: string;

                    // Handle MCP-style message format
                    if (
                      typeof toolResult.result === "object" &&
                      "content" in toolResult.result &&
                      Array.isArray(toolResult.result.content)
                    ) {
                      // Extract text from content array
                      resultStr = toolResult.result.content
                        .map((item: any) => {
                          if (item.type === "text") {
                            return item.text;
                          }
                          return `<${item.type} message>`;
                        })
                        .join("");
                    } else if (typeof toolResult.result === "string") {
                      resultStr = toolResult.result;
                    } else {
                      resultStr = JSON.stringify(toolResult.result);
                    }

                    // Truncate to first 200 characters for cleaner output
                    if (resultStr.length > 200) {
                      const truncated = resultStr.substring(0, 200);
                      const remainingChars = resultStr.length - 200;
                      logToolResult(
                        `${truncated}... (${remainingChars} more characters)`,
                      );
                    } else {
                      logToolResult(resultStr);
                    }
                  } else {
                    logToolResult("completed");
                  }
                }
                toolCallCount += toolCalls.length;
              }
            },
          });

          let currentOutput = "";
          let chunkCount = 0;

          for await (const chunk of result.textStream) {
            // Start streaming if not already started
            if (!isStreaming) {
              logStreamStart();
              isStreaming = true;
            }

            chunkCount++;
            logStreamWrite(chunk);
            currentOutput += chunk;
          }

          // Show message if no response generated and no tools were used
          if (chunkCount === 0 && toolCallCount === 0) {
            logStreamStart();
            logStreamWrite("(No response generated)");
            isStreaming = true;
          }

          // End streaming if active
          if (isStreaming) {
            logStreamEnd();
          }

          // The AI SDK will handle usage attributes automatically
          span.setStatus({ code: 1 }); // OK status
        } catch (error) {
          span.setStatus({ code: 2 }); // Error status

          logError(
            "Agent execution failed",
            error instanceof Error ? error : String(error),
          );
          throw error;
        }
      },
    );
  });
}

```

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

```typescript
import { z } from "zod";
import { setTag } from "@sentry/core";
import { defineTool } from "../../internal/tool-helpers/define";
import { apiServiceFromContext } from "../../internal/tool-helpers/api";
import type { ServerContext } from "../../types";
import { ParamOrganizationSlug, ParamRegionUrl } from "../../schema";
import { validateSlugOrId, isNumericId } from "../../utils/slug-validation";
import { searchIssuesAgent } from "./agent";
import { formatIssueResults, formatExplanation } from "./formatters";

export default defineTool({
  name: "search_issues",
  skills: ["inspect", "triage", "seer"], // Available in inspect, triage, and seer skills
  requiredScopes: ["event:read"],
  description: [
    "Search for grouped issues/problems in Sentry - returns a LIST of issues, NOT counts or aggregations.",
    "",
    "Uses AI to translate natural language queries into Sentry issue search syntax.",
    "Returns grouped issues with metadata like title, status, and user count.",
    "",
    "USE THIS TOOL WHEN USERS WANT:",
    "- A LIST of issues: 'show me issues', 'what problems do we have'",
    "- Filtered issue lists: 'unresolved issues', 'critical bugs'",
    "- Issues by impact: 'errors affecting more than 100 users'",
    "- Issues by assignment: 'issues assigned to me'",
    "",
    "DO NOT USE FOR COUNTS/AGGREGATIONS:",
    "- 'how many errors' → use search_events",
    "- 'count of issues' → use search_events",
    "- 'total number of errors today' → use search_events",
    "- 'sum/average/statistics' → use search_events",
    "",
    "ALSO DO NOT USE FOR:",
    "- Individual error events with timestamps → use search_events",
    "- Details about a specific issue ID → use get_issue_details",
    "",
    "REMEMBER: This tool returns a LIST of issues, not counts or statistics!",
    "",
    "<examples>",
    "search_issues(organizationSlug='my-org', naturalLanguageQuery='critical bugs from last week')",
    "search_issues(organizationSlug='my-org', naturalLanguageQuery='unhandled errors affecting 100+ users')",
    "search_issues(organizationSlug='my-org', naturalLanguageQuery='issues assigned to me')",
    "</examples>",
    "",
    "<hints>",
    "- If the user passes a parameter in the form of name/otherName, it's likely in the format of <organizationSlug>/<projectSlugOrId>.",
    "- Parse org/project notation directly without calling find_organizations or find_projects.",
    "- The projectSlugOrId parameter accepts both project slugs (e.g., 'my-project') and numeric IDs (e.g., '123456').",
    "</hints>",
  ].join("\n"),
  inputSchema: {
    organizationSlug: ParamOrganizationSlug,
    naturalLanguageQuery: z
      .string()
      .trim()
      .min(1)
      .describe("Natural language description of issues to search for"),
    projectSlugOrId: z
      .string()
      .toLowerCase()
      .trim()
      .superRefine(validateSlugOrId)
      .nullable()
      .default(null)
      .describe("The project's slug or numeric ID (optional)"),
    regionUrl: ParamRegionUrl.nullable().default(null),
    limit: z
      .number()
      .min(1)
      .max(100)
      .default(10)
      .describe("Maximum number of issues to return"),
    includeExplanation: z
      .boolean()
      .default(false)
      .describe("Include explanation of how the query was translated"),
  },
  annotations: {
    readOnlyHint: true,
    openWorldHint: true,
  },
  async handler(params, context: ServerContext) {
    const apiService = apiServiceFromContext(context, {
      regionUrl: params.regionUrl ?? undefined,
    });

    setTag("organization.slug", params.organizationSlug);
    if (params.projectSlugOrId) {
      // Check if it's a numeric ID or a slug and tag appropriately
      if (isNumericId(params.projectSlugOrId)) {
        setTag("project.id", params.projectSlugOrId);
      } else {
        setTag("project.slug", params.projectSlugOrId);
      }
    }

    // Convert project slug to ID if needed - required for the agent's field discovery
    let projectId: string | undefined;
    if (params.projectSlugOrId) {
      // Check if it's already a numeric ID
      if (isNumericId(params.projectSlugOrId)) {
        projectId = params.projectSlugOrId;
      } else {
        // It's a slug, convert to ID
        const project = await apiService.getProject({
          organizationSlug: params.organizationSlug,
          projectSlugOrId: params.projectSlugOrId!,
        });
        projectId = String(project.id);
      }
    }

    // Translate natural language to Sentry query
    const agentResult = await searchIssuesAgent({
      query: params.naturalLanguageQuery,
      organizationSlug: params.organizationSlug,
      apiService,
      projectId,
    });

    const translatedQuery = agentResult.result;

    // Execute the search - listIssues accepts projectSlug directly
    const issues = await apiService.listIssues({
      organizationSlug: params.organizationSlug,
      projectSlug: params.projectSlugOrId ?? undefined,
      query: translatedQuery.query ?? undefined,
      sortBy: translatedQuery.sort || "date",
      limit: params.limit,
    });

    // Build output with explanation first (if requested), then results
    let output = "";

    // Add explanation section before results (like search_events)
    if (params.includeExplanation) {
      // Start with title including natural language query
      output += `# Search Results for "${params.naturalLanguageQuery}"\n\n`;
      output += `⚠️ **IMPORTANT**: Display these issues as highlighted cards with status indicators, assignee info, and clickable Issue IDs.\n\n`;

      output += `## Query Translation\n`;
      output += `Natural language: "${params.naturalLanguageQuery}"\n`;
      output += `Sentry query: \`${translatedQuery.query}\``;
      if (translatedQuery.sort) {
        output += `\nSort: ${translatedQuery.sort}`;
      }
      output += `\n\n`;

      if (translatedQuery.explanation) {
        output += formatExplanation(translatedQuery.explanation);
        output += `\n\n`;
      }

      // Format results without the header since we already added it
      output += formatIssueResults({
        issues,
        organizationSlug: params.organizationSlug,
        projectSlugOrId: params.projectSlugOrId ?? undefined,
        query: translatedQuery.query,
        regionUrl: params.regionUrl ?? undefined,
        naturalLanguageQuery: params.naturalLanguageQuery,
        skipHeader: true,
      });
    } else {
      // Format results with natural language query for title
      output = formatIssueResults({
        issues,
        organizationSlug: params.organizationSlug,
        projectSlugOrId: params.projectSlugOrId ?? undefined,
        query: translatedQuery.query,
        regionUrl: params.regionUrl ?? undefined,
        naturalLanguageQuery: params.naturalLanguageQuery,
        skipHeader: false,
      });
    }

    return output;
  },
});

```

--------------------------------------------------------------------------------
/.claude/agents/claude-optimizer.md:
--------------------------------------------------------------------------------

```markdown
---
name: claude-optimizer
description: Optimizes CLAUDE.md files for maximum effectiveness with Sonnet 4 and Opus 4 models by analyzing structure, content clarity, token efficiency, and model-specific patterns
tools: Read, Write, MultiEdit, Bash, LS, Glob, Grep, WebSearch, WebFetch, Task
---

You are an expert optimizer for CLAUDE.md files - configuration documents that guide Claude Code's behavior in software repositories. Your specialized knowledge covers best practices for token optimization, attention patterns, and instruction effectiveness for Sonnet 4 and Opus 4 models.

## 🎯 PRIMARY DIRECTIVE

**PRESERVE ALL PROJECT-SPECIFIC CONTEXT**: You MUST retain all project-specific information including:
- Repository structure and file paths
- Tool names, counts, and descriptions
- API integration details
- Build commands and scripts
- Environment variables and defaults
- Architecture descriptions
- Testing requirements
- Documentation references

Optimization means making instructions clearer and more concise, NOT removing project context.

## 🎯 Critical Constraints

### 5K Token Limit
**MANDATORY**: Keep CLAUDE.md under 5,000 tokens. This is the #1 optimization priority.
- Current best practice: Aim for 2,500-3,500 tokens for optimal performance
- If content exceeds 5K, split into modular files under `docs/` directory
- Use `@path/to/file` references to include external context dynamically

## 🚀 Claude 4 Optimization Principles

### 1. Precision Over Verbosity
Claude 4 models excel at precise instruction following. Eliminate:
- Explanatory text ("Please ensure", "It's important to")
- Redundant instructions
- Vague directives ("appropriately", "properly", "as needed")

### 2. Parallel Tool Execution
Optimize for Claude 4's parallel capabilities:
```markdown
ALWAYS execute in parallel:
- `pnpm run tsc && pnpm run lint && pnpm run test`
- Multiple file reads/searches when investigating
```

### 3. Emphasis Hierarchy
Use strategic emphasis:
```
🔴 CRITICAL - Security, data loss prevention
🟡 MANDATORY - Required workflows
🟢 IMPORTANT - Quality standards
⚪ RECOMMENDED - Best practices
```

## 🔧 Tool Usage Strategy

### Research Tools
- **WebSearch**: Research latest prompt engineering techniques, Claude Code best practices
- **WebFetch**: Read specific optimization guides, Claude documentation
- **Task**: Delegate complex analysis (e.g., "analyze token distribution across sections")

### Analysis Tools  
- **Grep**: Find patterns, redundancies, verbose language
- **Glob**: Locate related documentation files
- **Bash**: Count tokens (`wc -w`), check file sizes

### Implementation Tools
- **Read**: Analyze current CLAUDE.md
- **MultiEdit**: Apply multiple optimizations efficiently
- **Write**: Create optimized version

## 📋 Optimization Methodology

### Phase 1: Token Audit
1. Count current tokens using `wc -w` (rough estimate: words × 1.3)
2. Identify top 3 token-heavy sections
3. Flag redundant/verbose content

### Phase 2: Content Compression
1. **Transform Instructions (Keep Context)**
   ```
   Before: "Please make sure to follow TypeScript best practices"
   After: "TypeScript: NEVER use 'any'. Use unknown or validated assertions."
   ```

2. **Consolidate Without Losing Information**
   - Merge ONLY truly duplicate instructions
   - Use tables to compress lists while keeping ALL items
   - Convert prose to bullets but retain all details
   - NEVER remove project-specific paths, commands, or tool names

3. **Smart Modularization**
   ```markdown
   ## Extended Docs
   - Architecture details: @docs/architecture.md  # Only if >500 tokens
   - API patterns: @docs/api-patterns.md        # Keep critical patterns inline
   - Testing guide: @docs/testing.md            # Keep validation commands inline
   ```
   
   **CRITICAL**: Only modularize truly excessive detail. Keep all actionable instructions inline.

### Phase 3: Structure Optimization
1. **Critical-First Layout**
   ```
   1. Core Directives (security, breaking changes)
   2. Workflow Requirements 
   3. Validation Commands
   4. Context/References
   ```

2. **Visual Scanning**
   - Section headers with emoji
   - Consistent indentation
   - Code blocks for commands

3. **Extended Thinking Integration**
   Add prompts that leverage Claude 4's reasoning:
   ```markdown
   <thinking>
   For complex tasks, break down into steps and validate assumptions
   </thinking>
   ```

## 📊 Output Format

### 1. Optimization Report
```markdown
# CLAUDE.md Optimization Results

**Metrics**
- Before: X tokens | After: Y tokens (Z% reduction)
- Clarity Score: Before X/10 → After Y/10
- Critical instructions in first 500 tokens: ✅

**High-Impact Changes**
1. [Change] → Saved X tokens
2. [Change] → Improved clarity by Y%
3. [Change] → Enhanced model performance

**Modularization** (if needed)
- Main CLAUDE.md: X tokens
- @docs/module1.md: Y tokens
- @docs/module2.md: Z tokens
```

### 2. Optimized CLAUDE.md
Deliver the complete optimized file with:
- **ALL project-specific context preserved**
- All critical instructions preserved
- Token count under 5K (ideally 2.5-3.5K)
- Clear visual hierarchy
- Precise, actionable language
- Every tool, path, command, and integration detail retained

## 🔧 Quick Reference

### Transform Patterns (With Context Preservation)
| Before | After | Tokens Saved | Context Lost |
|--------|-------|--------------|--------------|
| "Please ensure you..." | "MUST:" | ~3 | None ✅ |
| "It's important to note that..." | (remove) | ~5 | None ✅ |
| Long explanation | Table/list | ~40% | None ✅ |
| Separate similar rules | Consolidated rule | ~60% | None ✅ |
| "The search_events tool translates..." | "search_events: NL→DiscoverQL" | ~10 | None ✅ |
| Remove tool descriptions | ❌ DON'T DO THIS | ~500 | Critical ❌ |
| Remove architecture details | ❌ DON'T DO THIS | ~800 | Critical ❌ |

### Example: Preserving Project Context

**BAD Optimization (loses context):**
```markdown
## Tools
Use the appropriate tools for your task.
```

**GOOD Optimization (preserves context):**
```markdown
## Tools (19 modules)
- **search_events**: Natural language → DiscoverQL queries
- **search_issues**: Natural language → Issue search syntax
- **[17 other tools]**: Query, create, update Sentry resources
```

### Validation Checklist
- [ ] Under 5K tokens
- [ ] Critical instructions in first 20%
- [ ] No vague language
- [ ] All paths/commands verified
- [ ] Parallel execution emphasized
- [ ] Modular references added (if >5K)
- [ ] **ALL project context preserved**:
  - [ ] Repository structure intact
  - [ ] All tool names/descriptions present
  - [ ] Build commands unchanged
  - [ ] Environment variables preserved
  - [ ] Architecture details retained
  - [ ] File paths accurate

Remember: Every token counts. Precision beats explanation. Structure enables speed.

**NEVER sacrifice project context for token savings. A shorter but incomplete CLAUDE.md is worse than a complete one.**
```

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

```typescript
import { Hono } from "hono";
import type { Env } from "../types";
import { logIssue } from "@sentry/mcp-core/telem/logging";
import { SENTRY_GUIDES } from "@sentry/mcp-core/constants";
import { z } from "zod";
import type { RateLimitResult } from "../types/chat";
import type {
  AutoRagSearchResponse,
  ComparisonFilter,
  CompoundFilter,
  AutoRagSearchRequest,
} from "@cloudflare/workers-types";
import { logger } from "@sentry/cloudflare";
import { getClientIp } from "../utils/client-ip";

// Request schema matching the MCP tool parameters
const SearchRequestSchema = z.object({
  query: z.string().trim().min(1, "Query is required"),
  maxResults: z.number().int().min(1).max(10).default(10).optional(),
  guide: z.enum(SENTRY_GUIDES).optional(),
});

export default new Hono<{ Bindings: Env }>().post("/", async (c) => {
  try {
    // Get client IP address
    const clientIP = getClientIp(c.req.raw);

    // Rate limiting check - use client IP as the key
    // In local development or when IP can't be extracted, skip rate limiting
    // Rate limiter is optional and primarily for production abuse prevention
    // Note: Rate limiting bindings are "unsafe" (beta) and may not be available in development
    // so we check if the binding exists before using it
    // https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/
    if (c.env.SEARCH_RATE_LIMITER && clientIP) {
      try {
        // Hash the IP for privacy and consistent key format
        const encoder = new TextEncoder();
        const data = encoder.encode(clientIP);
        const hashBuffer = await crypto.subtle.digest("SHA-256", data);
        const hashArray = Array.from(new Uint8Array(hashBuffer));
        const hashHex = hashArray
          .map((b) => b.toString(16).padStart(2, "0"))
          .join("");
        const rateLimitKey = `search:ip:${hashHex.substring(0, 16)}`; // Use first 16 chars of hash

        const { success }: RateLimitResult =
          await c.env.SEARCH_RATE_LIMITER.limit({
            key: rateLimitKey,
          });
        if (!success) {
          return c.json(
            {
              error:
                "Rate limit exceeded. You can perform up to 20 documentation searches per minute. Please wait before searching again.",
              name: "RATE_LIMIT_EXCEEDED",
            },
            429,
          );
        }
      } catch (error) {
        const eventId = logIssue(error);
        return c.json(
          {
            error: "There was an error communicating with the rate limiter.",
            name: "RATE_LIMITER_ERROR",
            eventId,
          },
          500,
        );
      }
    }

    // Parse and validate request body
    const body = await c.req.json();
    const validationResult = SearchRequestSchema.safeParse(body);

    if (!validationResult.success) {
      return c.json(
        {
          error: "Invalid request",
          details: validationResult.error.errors,
        },
        400,
      );
    }

    const { query, maxResults = 10, guide } = validationResult.data;

    // Check if AI binding is available
    if (!c.env.AI) {
      return c.json(
        {
          error: "AI service not available",
          name: "AI_SERVICE_UNAVAILABLE",
        },
        503,
      );
    }

    try {
      const autoragId = c.env.AUTORAG_INDEX_NAME || "sentry-docs";

      // Construct AutoRAG search parameters
      const searchParams: AutoRagSearchRequest = {
        query,
        max_num_results: maxResults,
        ranking_options: {
          score_threshold: 0.2,
        },
      };

      // Add filename filters based on guide parameter
      // TODO: This is a hack to get the guide to work. Currently 'filename' is not working
      // with folder matching which means we're lacking guideName.md in the search results.
      if (guide) {
        let filter: ComparisonFilter | CompoundFilter;

        if (guide.includes("/")) {
          // Platform/guide combination: platforms/[platform]/guides/[guide]
          const [platformName, guideName] = guide.split("/", 2);

          filter = {
            type: "and",
            filters: [
              {
                type: "gte",
                key: "folder",
                value: `platforms/${platformName}/guides/${guideName}/`,
              },
              {
                type: "lte",
                key: "folder",
                value: `platforms/${platformName}/guides/${guideName}/z`,
              },
            ],
          };
        } else {
          // Just platform: platforms/[platform]/ - use range filter
          filter = {
            type: "and",
            filters: [
              {
                type: "gte",
                key: "folder",
                value: `platforms/${guide}/`,
              },
              {
                type: "lte",
                key: "folder",
                value: `platforms/${guide}/z`,
              },
            ],
          };
        }

        searchParams.filters = filter;
      }

      const searchResult =
        await c.env.AI.autorag(autoragId).search(searchParams);

      // Process search results - handle the actual response format from Cloudflare AI
      const searchData = searchResult as AutoRagSearchResponse;

      if (searchData.data?.length === 0) {
        logger.warn(
          logger.fmt`No results found for query: ${query} with guide: ${guide}`,
          {
            result_query: searchData.search_query,
            guide,
            searchParams: JSON.stringify(searchParams),
          },
        );
      }

      return c.json({
        query,
        results:
          searchData.data?.map((result) => {
            // Extract text from content array
            const text = result.content?.[0]?.text || "";

            // Get filename from result - ensure it's a string
            const rawFilename =
              result.filename || result.attributes?.filename || "";
            const filename =
              typeof rawFilename === "string"
                ? rawFilename
                : String(rawFilename);

            // Build URL from filename - remove .md extension
            const urlPath = filename.replace(/\.md$/, "");
            const url = urlPath ? `https://docs.sentry.io/${urlPath}` : "";

            return {
              id: filename,
              url: url,
              snippet: text,
              relevance: result.score || 0,
            };
          }) || [],
      });
    } catch (error) {
      const eventId = logIssue(error);
      return c.json(
        {
          error: "Failed to search documentation. Please try again later.",
          name: "SEARCH_FAILED",
          eventId,
        },
        500,
      );
    }
  } catch (error) {
    const eventId = logIssue(error);
    return c.json(
      {
        error: "Internal server error",
        name: "INTERNAL_ERROR",
        eventId,
      },
      500,
    );
  }
});

```
Page 6/16FirstPrevNextLast