#
tokens: 49807/50000 26/422 files (page 5/12)
lines: off (toggle) GitHub
raw markdown copy
This is page 5 of 12. Use http://codebase.md/getsentry/sentry-mcp?lines=false&page={x} to view the full context.

# Directory Structure

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

# Files

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

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

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

describeEval("search-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/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-server/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",
  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.optional(),
    regionUrl: ParamRegionUrl.optional(),
  },
  annotations: {
    readOnlyHint: true,
    openWorldHint: true,
  },
  async handler(params, context: ServerContext) {
    const apiService = apiServiceFromContext(context, {
      regionUrl: params.regionUrl,
    });

    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 = {
            id: params.attachmentId,
            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-server/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-server/src/telem/sentry.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from "vitest";
import { sentryBeforeSend } from "./sentry";
import type * as Sentry from "@sentry/node";

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

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

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

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

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

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

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

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

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

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

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

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

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

      const result = sentryBeforeSend(event, {}) as Sentry.Event;
      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: Sentry.Event = {
        exception: {
          values: [
            {
              type: "Error",
              value:
                "Failed to authenticate with sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234",
            },
          ],
        },
      };

      const result = sentryBeforeSend(event, {}) as Sentry.Event;
      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: Sentry.Event = {
        message: "Normal error message",
        extra: {
          foo: "bar",
        },
      };

      const result = sentryBeforeSend(event, {}) as Sentry.Event;
      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: Sentry.Event = {
        message:
          "First error with sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234",
      };

      const event2: Sentry.Event = {
        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: Sentry.Event = {
        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: Sentry.Event = {
        message: "Deep nesting test",
        extra: deep,
      };

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

```

--------------------------------------------------------------------------------
/packages/mcp-server/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",
  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)
      .optional()
      .describe("The project's slug or numeric ID (optional)"),
    regionUrl: ParamRegionUrl.optional(),
    limit: z
      .number()
      .min(1)
      .max(100)
      .optional()
      .default(10)
      .describe("Maximum number of issues to return"),
    includeExplanation: z
      .boolean()
      .optional()
      .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,
    });

    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,
      query: translatedQuery.query,
      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,
        query: translatedQuery.query,
        regionUrl: params.regionUrl,
        naturalLanguageQuery: params.naturalLanguageQuery,
        skipHeader: true,
      });
    } else {
      // Format results with natural language query for title
      output = formatIssueResults({
        issues,
        organizationSlug: params.organizationSlug,
        projectSlugOrId: params.projectSlugOrId,
        query: translatedQuery.query,
        regionUrl: params.regionUrl,
        naturalLanguageQuery: params.naturalLanguageQuery,
        skipHeader: false,
      });
    }

    return output;
  },
});

```

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

```typescript
import { Hono } from "hono";
import type { Env } from "../types";
import { logIssue } from "@sentry/mcp-server/telem/logging";
import { SENTRY_GUIDES } from "@sentry/mcp-server/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";

// 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 from Cloudflare header
    const clientIP = c.req.header("CF-Connecting-IP") || "unknown";

    // Rate limiting check - use client IP as the key
    // 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) {
      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,
    );
  }
});

```

--------------------------------------------------------------------------------
/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-server/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-server/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-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;
        }
      },
    );
  });
}

```

--------------------------------------------------------------------------------
/.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/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 type { Scope } from "@sentry/mcp-server/permissions";
import { DEFAULT_SCOPES } from "@sentry/mcp-server/constants";
import { SENTRY_TOKEN_URL } from "../constants";
import { exchangeCodeForAccessToken } from "../helpers";
import { verifyAndParseState, type OAuthState } from "../state";
import { logWarn } from "@sentry/mcp-server/telem/logging";

/**
 * Extended AuthRequest that includes permissions
 */
interface AuthRequestWithPermissions extends AuthRequest {
  permissions?: unknown;
}

/**
 * Convert selected permissions to granted scopes
 * Permissions are additive:
 * - Base (always included): org:read, project:read, team:read, event:read
 * - Seer adds: seer (virtual scope)
 * - Docs adds: docs (virtual scope)
 * - Issue Triage adds: event:write
 * - Project Management adds: project:write, team:write
 * @param permissions Array of permission strings
 */
function getScopesFromPermissions(permissions?: unknown): Set<Scope> {
  // Start with base read-only scopes (always granted via DEFAULT_SCOPES)
  const scopes = new Set<Scope>(DEFAULT_SCOPES);

  // Validate permissions is an array of strings
  if (!Array.isArray(permissions) || permissions.length === 0) {
    return scopes;
  }
  const perms = (permissions as unknown[]).filter(
    (p): p is string => typeof p === "string",
  );

  // Add scopes based on selected permissions
  if (perms.includes("seer")) {
    scopes.add("seer");
  }

  if (perms.includes("docs")) {
    scopes.add("docs");
  }

  if (perms.includes("issue_triage")) {
    scopes.add("event:write");
  }

  if (perms.includes("project_management")) {
    scopes.add("project:write");
    scopes.add("team:write");
  }

  return scopes;
}

/**
 * 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 AuthRequestWithPermissions;

  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;

  // Get scopes based on selected permissions
  const grantedScopes = getScopesFromPermissions(oauthReqInfo.permissions);

  // 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(" "),
      grantedScopes: Array.from(grantedScopes),

      // 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-server/src/tools/search-events/utils.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { http, HttpResponse } from "msw";
import { mswServer } from "@sentry/mcp-server-mocks";
import { fetchCustomAttributes } from "./utils";
import { SentryApiService } from "../../api-client";
import * as logging from "../../telem/logging";

describe("fetchCustomAttributes", () => {
  let apiService: SentryApiService;

  beforeEach(() => {
    vi.clearAllMocks();
    vi.spyOn(logging, "logWarn").mockImplementation(() => {});

    // Create a real SentryApiService instance
    apiService = new SentryApiService({
      accessToken: "test-token",
    });
  });

  afterEach(() => {
    vi.restoreAllMocks();
    mswServer.resetHandlers();
  });

  describe("403 permission errors", () => {
    it("should throw 403 'no multi-project access' error as UserInputError", async () => {
      // Mock the API to return a 403 error like in the Sentry issue
      mswServer.use(
        http.get(
          "https://sentry.io/api/0/organizations/test-org/trace-items/attributes/",
          () => {
            return HttpResponse.json(
              {
                detail:
                  "You do not have access to query across multiple projects. Please select a project for your query.",
              },
              { status: 403 },
            );
          },
        ),
      );

      // Should throw ApiPermissionError with the improved error message
      await expect(
        fetchCustomAttributes(apiService, "test-org", "spans"),
      ).rejects.toThrow(
        "You do not have access to query across multiple projects. Please select a project for your query.",
      );

      // Should NOT log - the caller handles logging
    });

    it("should throw 403 errors for logs dataset", async () => {
      mswServer.use(
        http.get(
          "https://sentry.io/api/0/organizations/test-org/trace-items/attributes/",
          () => {
            return HttpResponse.json(
              { detail: "Permission denied" },
              { status: 403 },
            );
          },
        ),
      );

      // Should throw ApiPermissionError with the raw error message
      await expect(
        fetchCustomAttributes(apiService, "test-org", "logs", "project-123"),
      ).rejects.toThrow("Permission denied");
    });

    it("should throw 404 errors for errors dataset", async () => {
      mswServer.use(
        http.get("https://sentry.io/api/0/organizations/test-org/tags/", () => {
          return HttpResponse.json(
            { detail: "Project not found" },
            { status: 404 },
          );
        }),
      );

      // Should throw ApiNotFoundError with the raw error message
      await expect(
        fetchCustomAttributes(apiService, "test-org", "errors", "non-existent"),
      ).rejects.toThrow("Project not found");
    });
  });

  describe("5xx server errors", () => {
    it("should re-throw 500 errors to be captured by Sentry", async () => {
      mswServer.use(
        http.get(
          "https://sentry.io/api/0/organizations/test-org/trace-items/attributes/",
          () => {
            return HttpResponse.json(
              { detail: "Internal server error" },
              { status: 500 },
            );
          },
        ),
      );

      // Should re-throw the error with the exact message (not wrapped as UserInputError)
      const error = await fetchCustomAttributes(
        apiService,
        "test-org",
        "spans",
      ).catch((e) => e);

      expect(error).toBeInstanceOf(Error);
      expect(error.message).toBe("Internal server error");
    });

    it("should re-throw 502 errors", async () => {
      mswServer.use(
        http.get("https://sentry.io/api/0/organizations/test-org/tags/", () => {
          return HttpResponse.json({ detail: "Bad gateway" }, { status: 502 });
        }),
      );

      const error = await fetchCustomAttributes(
        apiService,
        "test-org",
        "errors",
      ).catch((e) => e);

      expect(error).toBeInstanceOf(Error);
      expect(error.message).toBe("Bad gateway");
    });
  });

  describe("network errors", () => {
    it("should re-throw network errors to be captured by Sentry", async () => {
      mswServer.use(
        http.get(
          "https://sentry.io/api/0/organizations/test-org/trace-items/attributes/",
          () => {
            // Simulate network error by throwing
            throw new Error("Network error: ETIMEDOUT");
          },
        ),
      );

      await expect(
        fetchCustomAttributes(apiService, "test-org", "spans"),
      ).rejects.toThrow("Network error: ETIMEDOUT");
    });
  });

  describe("successful responses", () => {
    it("should return attributes for spans dataset", async () => {
      // Mock with separate string and number queries as the real API does
      // The API client makes two separate calls with attributeType parameter
      mswServer.use(
        http.get(
          "https://sentry.io/api/0/organizations/test-org/trace-items/attributes/",
          ({ request }) => {
            const url = new URL(request.url);
            const attributeType = url.searchParams.get("attributeType");
            const itemType = url.searchParams.get("itemType");

            // Validate the request has expected parameters
            if (!attributeType || !itemType) {
              return HttpResponse.json(
                { detail: "Missing required parameters" },
                { status: 400 },
              );
            }

            if (attributeType === "string") {
              return HttpResponse.json([
                { key: "span.op", name: "Operation" },
                { key: "sentry:internal", name: "Internal" }, // Should be filtered
              ]);
            }
            if (attributeType === "number") {
              return HttpResponse.json([
                { key: "span.duration", name: "Duration" },
              ]);
            }
            return HttpResponse.json([]);
          },
        ),
      );

      const result = await fetchCustomAttributes(
        apiService,
        "test-org",
        "spans",
      );

      expect(result).toEqual({
        attributes: {
          "span.op": "Operation",
          "span.duration": "Duration",
        },
        fieldTypes: {
          "span.op": "string",
          "span.duration": "number",
        },
      });
    });

    it("should return attributes for errors dataset", async () => {
      mswServer.use(
        http.get("https://sentry.io/api/0/organizations/test-org/tags/", () => {
          return HttpResponse.json([
            { key: "browser", name: "Browser", totalValues: 10 },
            { key: "sentry:user", name: "User", totalValues: 5 }, // Should be filtered
            { key: "environment", name: "Environment", totalValues: 3 },
          ]);
        }),
      );

      const result = await fetchCustomAttributes(
        apiService,
        "test-org",
        "errors",
      );

      expect(result).toEqual({
        attributes: {
          browser: "Browser",
          environment: "Environment",
        },
        fieldTypes: {},
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/packages/mcp-server/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
  | "seer" // Virtual scope: Use Seer analysis (not respected by upstream Sentry API)
  | "docs"; // Virtual scope: Access Sentry documentation (not respected by upstream Sentry API)

/**
 * 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"]),

  // Virtual scopes (not respected by upstream Sentry API)
  seer: new Set(["seer"]),
  docs: new Set(["docs"]),
};

/**
 * 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",
  seer: "Use Seer for issue analysis (may incur costs)",
  docs: "Access Sentry documentation",
};

/**
 * 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/chat/chat-messages.tsx:
--------------------------------------------------------------------------------

```typescript
import { useMemo } from "react";
import { Loader2, AlertCircle } from "lucide-react";
import { Button } from "../ui/button";
import { MessagePart } from ".";
import { ToolActions } from "../ui/tool-actions";
import type { Message, ProcessedMessagePart, ChatMessagesProps } from "./types";
import { isAuthError, getErrorMessage } from "../../utils/chat-error-handler";
import { useAuth } from "../../contexts/auth-context";

// Cache for stable part objects to avoid recreating them
const partCache = new WeakMap<Message, { type: "text"; text: string }>();

function processMessages(
  messages: Message[],
  isChatLoading: boolean,
  isLocalStreaming?: boolean,
  isMessageStreaming?: (messageId: string) => boolean,
): ProcessedMessagePart[] {
  const allParts: ProcessedMessagePart[] = [];

  // Only the very last text part of the very last message should be streaming
  const lastMessageIndex = messages.length - 1;

  messages.forEach((message, messageIndex) => {
    const isLastMessage = messageIndex === lastMessageIndex;

    // Handle messages with parts array
    if (message.parts && message.parts.length > 0) {
      const lastPartIndex = message.parts.length - 1;

      message.parts.forEach((part, partIndex) => {
        const isLastPartOfLastMessage =
          isLastMessage && partIndex === lastPartIndex;

        allParts.push({
          part,
          messageId: message.id,
          messageRole: message.role,
          partIndex,
          // Stream if it's AI response OR local streaming simulation
          isStreaming:
            (isLastPartOfLastMessage &&
              isChatLoading &&
              part.type === "text") ||
            (part.type === "text" && !!isMessageStreaming?.(message.id)),
        });
      });
    } else if (message.content) {
      // Use cached part object to maintain stable references
      let part = partCache.get(message);
      if (!part) {
        part = { type: "text", text: message.content };
        partCache.set(message, part);
      }

      allParts.push({
        part,
        messageId: message.id,
        messageRole: message.role,
        partIndex: 0,
        // Stream if it's AI response OR local streaming simulation
        isStreaming:
          (isLastMessage && isChatLoading) ||
          isMessageStreaming?.(message.id) ||
          false,
      });
    }
  });

  return allParts;
}

export function ChatMessages({
  messages,
  isChatLoading,
  isLocalStreaming,
  isMessageStreaming,
  error,
  onRetry,
  onSlashCommand,
}: ChatMessagesProps) {
  const { handleOAuthLogin } = useAuth();

  const processedParts = useMemo(
    () =>
      processMessages(
        messages,
        isChatLoading,
        isLocalStreaming,
        isMessageStreaming,
      ),
    [messages, isChatLoading, isLocalStreaming, isMessageStreaming],
  );

  // Simple error handling - just check if it's auth or not
  const errorIsAuth = error ? isAuthError(error) : false;
  const errorMessage = error ? getErrorMessage(error) : null;
  return (
    <div className="mx-6 mt-6 space-y-4 flex-1">
      {/* Empty State when no messages */}
      {messages.length === 0 && (
        <div className="flex flex-col items-center justify-center h-full">
          <div className="max-w-md w-full space-y-6">
            <div className="text-slate-400 hidden [@media(min-height:500px)]:block">
              <img
                src="/flow-transparent.png"
                alt="Flow"
                width={1536}
                height={1024}
                className="w-full mb-6 bg-violet-300 rounded"
              />
            </div>

            <div className="text-center text-slate-400">
              <h2 id="chat-panel-title" className="text-lg mb-2">
                Chat with your stack traces. Argue with confidence. Lose
                gracefully.
              </h2>
            </div>
          </div>
        </div>
      )}

      {/* Show messages when we have any */}
      {messages.length > 0 && (
        <>
          <h2 id="chat-panel-title" className="sr-only">
            Chat Messages
          </h2>
          {processedParts.map((item) => {
            // Find the original message to check for metadata
            const originalMessage = messages.find(
              (m) => m.id === item.messageId,
            );
            const messageData = originalMessage?.data as any;
            const hasToolActions =
              messageData?.type === "tools-list" &&
              messageData?.toolsDetailed &&
              Array.isArray(messageData.toolsDetailed);

            return (
              <div key={`${item.messageId}-part-${item.partIndex}`}>
                <MessagePart
                  part={item.part}
                  messageId={item.messageId}
                  messageRole={item.messageRole}
                  partIndex={item.partIndex}
                  isStreaming={item.isStreaming}
                  messageData={originalMessage?.data}
                  onSlashCommand={onSlashCommand}
                />
                {/* Show tool actions list for tools-list messages */}
                {hasToolActions &&
                  item.partIndex ===
                    (originalMessage?.parts?.length ?? 1) - 1 && (
                    <div className="mr-8 mt-4">
                      <ToolActions tools={messageData.toolsDetailed} />
                    </div>
                  )}
              </div>
            );
          })}

          {/* Show error or loading state */}
          {error && errorMessage ? (
            <div className="mr-8 p-4 bg-red-900/10 border border-red-500/30 rounded">
              <div className="flex items-start gap-3">
                <AlertCircle className="h-5 w-5 text-red-400 mt-0.5" />
                <div className="flex-1">
                  <p className="text-red-300">{errorMessage}</p>
                  {/* Simple action buttons */}
                  <div className="mt-3 flex gap-2">
                    {errorIsAuth ? (
                      <Button
                        onClick={() => {
                          handleOAuthLogin();
                        }}
                        size="sm"
                        variant="secondary"
                        className="bg-red-900/20 hover:bg-red-900/30 text-red-300 border-red-500/30 cursor-pointer"
                      >
                        Reauthenticate
                      </Button>
                    ) : (
                      onRetry && (
                        <Button
                          onClick={onRetry}
                          size="sm"
                          variant="secondary"
                          className="bg-red-900/20 hover:bg-red-900/30 text-red-300 border-red-500/30 cursor-pointer"
                        >
                          Try again
                        </Button>
                      )
                    )}
                  </div>
                </div>
              </div>
            </div>
          ) : isChatLoading ? (
            <div className="flex items-center space-x-2 text-slate-400 mr-8">
              <Loader2 className="h-4 w-4 animate-spin" />
              <span>Assistant is thinking...</span>
            </div>
          ) : null}
        </>
      )}
    </div>
  );
}

```

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

```typescript
import { describe, it, expect, vi } from "vitest";
import getDoc from "./get-doc.js";

describe("get_doc", () => {
  it("returns document content", async () => {
    const result = await getDoc.handler(
      {
        path: "/product/rate-limiting.md",
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
        mcpUrl: "https://mcp.sentry.dev",
      },
    );
    expect(result).toMatchInlineSnapshot(`
      "# Documentation Content

      **Path**: /product/rate-limiting.md

      ---

      # Project Rate Limits and Quotas

      Rate limiting allows you to control the volume of events that Sentry accepts from your applications. This helps you manage costs and ensures that a sudden spike in errors doesn't consume your entire quota.

      ## Why Use Rate Limiting?

      - **Cost Control**: Prevent unexpected charges from error spikes
      - **Noise Reduction**: Filter out repetitive or low-value events
      - **Resource Management**: Ensure critical projects have quota available
      - **Performance**: Reduce load on your Sentry organization

      ## Types of Rate Limits

      ### 1. Organization Rate Limits

      Set a maximum number of events per hour across your entire organization:

      \`\`\`python
      # In your organization settings
      rate_limit = 1000  # events per hour
      \`\`\`

      ### 2. Project Rate Limits

      Configure limits for specific projects:

      \`\`\`javascript
      // Project settings
      {
        "rateLimit": {
          "window": 3600,  // 1 hour in seconds
          "limit": 500     // max events
        }
      }
      \`\`\`

      ### 3. Key-Based Rate Limiting

      Rate limit by specific attributes:

      - **By Release**: Limit events from specific releases
      - **By User**: Prevent single users from consuming quota
      - **By Transaction**: Control high-volume transactions

      ## Configuration Examples

      ### SDK Configuration

      Configure client-side sampling to reduce events before they're sent:

      \`\`\`javascript
      Sentry.init({
        dsn: "your-dsn",
        tracesSampleRate: 0.1,  // Sample 10% of transactions
        beforeSend(event) {
          // Custom filtering logic
          if (event.exception?.values?.[0]?.value?.includes("NetworkError")) {
            return null;  // Drop network errors
          }
          return event;
        }
      });
      \`\`\`

      ### Inbound Filters

      Use Sentry's inbound filters to drop events server-side:

      1. Go to **Project Settings** → **Inbound Filters**
      2. Enable filters for:
         - Legacy browsers
         - Web crawlers
         - Specific error messages
         - IP addresses

      ### Spike Protection

      Enable spike protection to automatically limit events during traffic spikes:

      \`\`\`python
      # Project settings
      spike_protection = {
        "enabled": True,
        "max_events_per_hour": 10000,
        "detection_window": 300  # 5 minutes
      }
      \`\`\`

      ## Best Practices

      1. **Start Conservative**: Begin with lower limits and increase as needed
      2. **Monitor Usage**: Regularly review your quota consumption
      3. **Use Sampling**: Implement transaction sampling for high-volume apps
      4. **Filter Noise**: Drop known low-value events at the SDK level
      5. **Set Alerts**: Configure notifications for quota thresholds

      ## Rate Limit Headers

      Sentry returns rate limit information in response headers:

      \`\`\`
      X-Sentry-Rate-Limit: 60
      X-Sentry-Rate-Limit-Remaining: 42
      X-Sentry-Rate-Limit-Reset: 1634567890
      \`\`\`

      ## Quota Management

      ### Viewing Quota Usage

      1. Navigate to **Settings** → **Subscription**
      2. View usage by:
         - Project
         - Event type
         - Time period

      ### On-Demand Budgets

      Purchase additional events when approaching limits:

      \`\`\`bash
      # Via API
      curl -X POST https://sentry.io/api/0/organizations/{org}/quotas/ \\
        -H 'Authorization: Bearer <token>' \\
        -d '{"events": 100000}'
      \`\`\`

      ## Troubleshooting

      ### Events Being Dropped?

      Check:
      1. Organization and project rate limits
      2. Spike protection status
      3. SDK sampling configuration
      4. Inbound filter settings

      ### Rate Limit Errors

      If you see 429 errors:
      - Review your rate limit configuration
      - Implement exponential backoff
      - Consider event buffering

      ## Related Documentation

      - [SDK Configuration Guide](/platforms/javascript/configuration)
      - [Quotas and Billing](/product/quotas)
      - [Filtering Events](/product/data-management/filtering)

      ---

      ## Using this documentation

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

  it("handles invalid path format", async () => {
    await expect(
      getDoc.handler(
        {
          path: "/product/rate-limiting", // Missing .md extension
        },
        {
          constraints: {
            organizationSlug: null,
          },
          accessToken: "access-token",
          userId: "1",
        },
      ),
    ).rejects.toThrow(
      "Invalid documentation path. Path must end with .md extension.",
    );
  });

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

    await expect(
      getDoc.handler(
        {
          path: "/product/test.md",
        },
        {
          constraints: {
            organizationSlug: null,
          },
          accessToken: "access-token",
          userId: "1",
        },
      ),
    ).rejects.toThrow();
  });

  it("validates domain whitelist", async () => {
    // Test with absolute URL that would resolve to a different domain
    await expect(
      getDoc.handler(
        {
          path: "https://malicious.com/test.md",
        },
        {
          constraints: {
            organizationSlug: null,
          },
          accessToken: "access-token",
          userId: "1",
        },
      ),
    ).rejects.toThrow(
      "Invalid domain. Documentation can only be fetched from allowed domains: docs.sentry.io, develop.sentry.io",
    );
  });

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

    await expect(
      getDoc.handler(
        {
          path: "/product/test.md",
        },
        {
          constraints: {
            organizationSlug: null,
          },
          accessToken: "access-token",
          userId: "1",
        },
      ),
    ).rejects.toThrow("Request timeout after 15000ms");
  });
});

```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/oauth/authorize.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from "vitest";
import { Hono } from "hono";
import oauthRoute from "./index";
import type { Env } from "../types";
import { verifyAndParseState } from "./state";

// Mock the OAuth provider
const mockOAuthProvider = {
  parseAuthRequest: vi.fn(),
  lookupClient: vi.fn(),
  completeAuthorization: vi.fn(),
};

// Create test app with mocked environment
function createTestApp(env: Partial<Env> = {}) {
  const app = new Hono<{ Bindings: Env }>();
  app.route("/oauth", oauthRoute);
  return app;
}

describe("oauth authorize routes", () => {
  let app: ReturnType<typeof createTestApp>;
  let testEnv: Partial<Env>;

  beforeEach(() => {
    vi.clearAllMocks();
    testEnv = {
      OAUTH_PROVIDER: mockOAuthProvider as unknown as Env["OAUTH_PROVIDER"],
      COOKIE_SECRET: "test-secret-key",
      SENTRY_CLIENT_ID: "test-client-id",
      SENTRY_CLIENT_SECRET: "test-client-secret",
      SENTRY_HOST: "sentry.io",
    };
    app = createTestApp(testEnv);
  });

  describe("GET /oauth/authorize", () => {
    it("renders approval dialog HTML with state field", async () => {
      mockOAuthProvider.parseAuthRequest.mockResolvedValueOnce({
        clientId: "test-client",
        redirectUri: "https://example.com/callback",
        scope: ["read"],
        state: "orig",
      });
      mockOAuthProvider.lookupClient.mockResolvedValueOnce({
        clientId: "test-client",
        clientName: "Test Client",
        redirectUris: ["https://example.com/callback"],
        tokenEndpointAuthMethod: "client_secret_basic",
      });

      const request = new Request("http://localhost/oauth/authorize", {
        method: "GET",
      });
      const response = await app.fetch(request, testEnv as Env);
      expect(response.status).toBe(200);
      const html = await response.text();
      expect(html).toContain("<form");
      expect(html).toContain('name="state"');
    });
  });

  describe("POST /oauth/authorize", () => {
    beforeEach(() => {
      mockOAuthProvider.lookupClient.mockResolvedValue({
        clientId: "test-client",
        clientName: "Test Client",
        redirectUris: ["https://example.com/callback"],
        tokenEndpointAuthMethod: "client_secret_basic",
      });
    });
    it("should encode permissions in the redirect state", async () => {
      const oauthReqInfo = {
        clientId: "test-client",
        redirectUri: "https://example.com/callback",
        scope: ["read", "write"],
        state: "original-state",
      };
      const formData = new FormData();
      formData.append("state", btoa(JSON.stringify({ oauthReqInfo })));
      formData.append("permission", "issue_triage");
      formData.append("permission", "project_management");
      const request = new Request("http://localhost/oauth/authorize", {
        method: "POST",
        body: formData,
      });
      const response = await app.fetch(request, testEnv as Env);
      expect(response.status).toBe(302);
      const location = response.headers.get("location");
      expect(location).toBeTruthy();
      const redirectUrl = new URL(location!);
      expect(redirectUrl.hostname).toBe("sentry.io");
      expect(redirectUrl.pathname).toBe("/oauth/authorize/");
      const stateParam = redirectUrl.searchParams.get("state");
      expect(stateParam).toBeTruthy();
      const decodedState = await verifyAndParseState(
        stateParam!,
        testEnv.COOKIE_SECRET as string,
      );
      expect((decodedState.req as any).permissions).toEqual([
        "issue_triage",
        "project_management",
      ]);
      expect((decodedState.req as any).clientId).toBe("test-client");
      expect((decodedState.req as any).redirectUri).toBe(
        "https://example.com/callback",
      );
      expect((decodedState.req as any).scope).toEqual(["read", "write"]);
    });

    it("should handle no permissions selected (read-only default)", async () => {
      const oauthReqInfo = {
        clientId: "test-client",
        redirectUri: "https://example.com/callback",
        scope: ["read"],
        state: "original-state",
      };
      const formData = new FormData();
      formData.append("state", btoa(JSON.stringify({ oauthReqInfo })));
      const request = new Request("http://localhost/oauth/authorize", {
        method: "POST",
        body: formData,
      });
      const response = await app.fetch(request, testEnv as Env);
      expect(response.status).toBe(302);
      const location = response.headers.get("location");
      expect(location).toBeTruthy();
      const redirectUrl = new URL(location!);
      const stateParam = redirectUrl.searchParams.get("state");
      const decodedState = await verifyAndParseState(
        stateParam!,
        testEnv.COOKIE_SECRET as string,
      );
      expect((decodedState.req as any).permissions).toEqual([]);
    });

    it("should handle only issue triage permission", async () => {
      const oauthReqInfo = {
        clientId: "test-client",
        redirectUri: "https://example.com/callback",
        scope: ["read", "write"],
        state: "original-state",
      };
      const formData = new FormData();
      formData.append("state", btoa(JSON.stringify({ oauthReqInfo })));
      formData.append("permission", "issue_triage");
      const request = new Request("http://localhost/oauth/authorize", {
        method: "POST",
        body: formData,
      });
      const response = await app.fetch(request, testEnv as Env);
      expect(response.status).toBe(302);
      const location = response.headers.get("location");
      const redirectUrl = new URL(location!);
      const stateParam = redirectUrl.searchParams.get("state");
      const decodedState = await verifyAndParseState(
        stateParam!,
        testEnv.COOKIE_SECRET as string,
      );
      expect((decodedState.req as any).permissions).toEqual(["issue_triage"]);
    });

    it("should include Set-Cookie header for approval", async () => {
      const oauthReqInfo = {
        clientId: "test-client",
        redirectUri: "https://example.com/callback",
        scope: ["read"],
      };
      const formData = new FormData();
      formData.append("state", btoa(JSON.stringify({ oauthReqInfo })));
      const request = new Request("http://localhost/oauth/authorize", {
        method: "POST",
        body: formData,
      });
      const response = await app.fetch(request, testEnv as Env);
      const setCookie = response.headers.get("Set-Cookie");
      expect(setCookie).toBeTruthy();
      expect(setCookie).toContain("mcp-approved-clients=");
      expect(setCookie).toContain("HttpOnly");
      expect(setCookie).toContain("Secure");
      expect(setCookie).toContain("SameSite=Lax");
    });
  });

  describe("POST /oauth/authorize (CSRF/validation)", () => {
    it("should reject invalid encoded state (bad base64/json)", async () => {
      const formData = new FormData();
      formData.append("state", "%%%INVALID-BASE64%%%");
      const request = new Request("http://localhost/oauth/authorize", {
        method: "POST",
        body: formData,
      });
      const response = await app.fetch(request, testEnv as Env);
      expect(response.status).toBe(400);
      const text = await response.text();
      expect(text).toBe("Invalid request");
    });
  });
});

```

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

```typescript
import { describe, it, expect, vi, beforeEach } from "vitest";
import { Hono } from "hono";
import oauthRoute from "./index";
import { signState, type OAuthState } from "./state";
import type { Env } from "../types";

// Mock the OAuth provider
const mockOAuthProvider = {
  parseAuthRequest: vi.fn(),
  lookupClient: vi.fn(),
  completeAuthorization: vi.fn(),
};

function createTestApp(env: Partial<Env> = {}) {
  const app = new Hono<{ Bindings: Env }>();
  app.route("/oauth", oauthRoute);
  return app;
}

describe("oauth callback routes", () => {
  let app: ReturnType<typeof createTestApp>;
  let testEnv: Partial<Env>;

  beforeEach(() => {
    vi.clearAllMocks();
    testEnv = {
      OAUTH_PROVIDER: mockOAuthProvider as unknown as Env["OAUTH_PROVIDER"],
      COOKIE_SECRET: "test-secret-key",
      SENTRY_CLIENT_ID: "test-client-id",
      SENTRY_CLIENT_SECRET: "test-client-secret",
      SENTRY_HOST: "sentry.io",
    };
    app = createTestApp(testEnv);
  });

  describe("GET /oauth/callback", () => {
    it("should reject callback with invalid state param", async () => {
      const request = new Request(
        `http://localhost/oauth/callback?code=test-code&state=%%%INVALID%%%`,
        { method: "GET" },
      );
      const response = await app.fetch(request, testEnv as Env);
      expect(response.status).toBe(400);
      const text = await response.text();
      expect(text).toBe("Invalid state");
    });

    it("should reject callback without approved client cookie", async () => {
      // Build signed state matching what /oauth/authorize issues
      const now = Date.now();
      const payload: OAuthState = {
        req: {
          clientId: "test-client",
          redirectUri: "https://example.com/callback",
          scope: ["read"],
        },
        iat: now,
        exp: now + 10 * 60 * 1000,
      } as unknown as OAuthState;
      const signedState = await signState(payload, testEnv.COOKIE_SECRET!);

      const request = new Request(
        `http://localhost/oauth/callback?code=test-code&state=${signedState}`,
        {
          method: "GET",
          headers: {},
        },
      );
      const response = await app.fetch(request, testEnv as Env);
      expect(response.status).toBe(403);
      const text = await response.text();
      expect(text).toBe("Authorization failed: Client not approved");
    });

    it("should reject callback with invalid client approval cookie", async () => {
      const now = Date.now();
      const payload: OAuthState = {
        req: {
          clientId: "test-client",
          redirectUri: "https://example.com/callback",
          scope: ["read"],
        },
        iat: now,
        exp: now + 10 * 60 * 1000,
      } as unknown as OAuthState;
      const signedState = await signState(payload, testEnv.COOKIE_SECRET!);

      const request = new Request(
        `http://localhost/oauth/callback?code=test-code&state=${signedState}`,
        {
          method: "GET",
          headers: {
            Cookie: "mcp-approved-clients=invalid-cookie-value",
          },
        },
      );
      const response = await app.fetch(request, testEnv as Env);
      expect(response.status).toBe(403);
      const text = await response.text();
      expect(text).toBe("Authorization failed: Client not approved");
    });

    it("should reject callback with cookie for different client", async () => {
      // Ensure authorize POST accepts the redirectUri
      mockOAuthProvider.lookupClient.mockResolvedValueOnce({
        clientId: "different-client",
        clientName: "Other Client",
        redirectUris: ["https://example.com/callback"],
        tokenEndpointAuthMethod: "client_secret_basic",
      });

      const approvalFormData = new FormData();
      approvalFormData.append(
        "state",
        btoa(
          JSON.stringify({
            oauthReqInfo: {
              clientId: "different-client",
              redirectUri: "https://example.com/callback",
              scope: ["read"],
            },
          }),
        ),
      );
      const approvalRequest = new Request("http://localhost/oauth/authorize", {
        method: "POST",
        body: approvalFormData,
      });
      const approvalResponse = await app.fetch(approvalRequest, testEnv as Env);
      expect(approvalResponse.status).toBe(302);
      const setCookie = approvalResponse.headers.get("Set-Cookie");
      expect(setCookie).toBeTruthy();

      // Build a signed state for a different client than the approved one
      const now = Date.now();
      const payload: OAuthState = {
        req: {
          clientId: "test-client",
          redirectUri: "https://example.com/callback",
          scope: ["read"],
        },
        iat: now,
        exp: now + 10 * 60 * 1000,
      } as unknown as OAuthState;
      const signedState = await signState(payload, testEnv.COOKIE_SECRET!);

      const request = new Request(
        `http://localhost/oauth/callback?code=test-code&state=${signedState}`,
        {
          method: "GET",
          headers: {
            Cookie: setCookie!.split(";")[0],
          },
        },
      );
      const response = await app.fetch(request, testEnv as Env);
      expect(response.status).toBe(403);
      const text = await response.text();
      expect(text).toBe("Authorization failed: Client not approved");
    });

    it("should reject callback when state signature is tampered", async () => {
      // Ensure client redirectUri is registered
      mockOAuthProvider.lookupClient.mockResolvedValueOnce({
        clientId: "test-client",
        clientName: "Test Client",
        redirectUris: ["https://example.com/callback"],
        tokenEndpointAuthMethod: "client_secret_basic",
      });

      // Prepare approval POST to generate a signed state
      const oauthReqInfo = {
        clientId: "test-client",
        redirectUri: "https://example.com/callback",
        scope: ["read"],
      };
      const approvalFormData = new FormData();
      approvalFormData.append(
        "state",
        btoa(
          JSON.stringify({
            oauthReqInfo,
          }),
        ),
      );
      const approvalRequest = new Request("http://localhost/oauth/authorize", {
        method: "POST",
        body: approvalFormData,
      });
      const approvalResponse = await app.fetch(approvalRequest, testEnv as Env);
      expect(approvalResponse.status).toBe(302);
      const setCookie = approvalResponse.headers.get("Set-Cookie");
      const location = approvalResponse.headers.get("location");
      expect(location).toBeTruthy();
      const redirectUrl = new URL(location!);
      const signedState = redirectUrl.searchParams.get("state")!;

      // Tamper with the signature portion (hex) without breaking payload parsing
      const [sig, b64] = signedState.split(".");
      const badSig = (sig[0] === "a" ? "b" : "a") + sig.slice(1);
      const tamperedState = `${badSig}.${b64}`;

      // Call callback with tampered state and valid approval cookie
      const callbackRequest = new Request(
        `http://localhost/oauth/callback?code=test-code&state=${tamperedState}`,
        {
          method: "GET",
          headers: {
            Cookie: setCookie!.split(";")[0],
          },
        },
      );
      const response = await app.fetch(callbackRequest, testEnv as Env);
      expect(response.status).toBe(400);
      const text = await response.text();
      expect(text).toBe("Invalid state");
    });
  });
});

```

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

```typescript
import { describe, it, expect } from "vitest";
import { z } from "zod";
import {
  getConstraintKeysToFilter,
  getConstraintParametersToInject,
} from "./constraint-helpers";

/**
 * Test suite for constraint helper functions.
 *
 * These tests verify the logic for filtering schemas and injecting parameters
 * when handling MCP constraints with parameter aliases (projectSlug → projectSlugOrId).
 */

describe("Constraint Helpers", () => {
  // Mock tool schemas for testing
  const schemaWithProjectSlugOrId = {
    organizationSlug: z.string(),
    projectSlugOrId: z.string().optional(),
    query: z.string().optional(),
  };

  const schemaWithProjectSlug = {
    organizationSlug: z.string(),
    projectSlug: z.string().optional(),
    query: z.string().optional(),
  };

  describe("getConstraintKeysToFilter", () => {
    it("filters direct constraint matches", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: null,
        regionUrl: null,
      };

      const keys = getConstraintKeysToFilter(
        constraints,
        schemaWithProjectSlug,
      );

      expect(keys).toEqual(["organizationSlug"]);
    });

    it("applies projectSlug → projectSlugOrId alias when projectSlug is constrained", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: "my-project",
        regionUrl: null,
      };

      const keys = getConstraintKeysToFilter(
        constraints,
        schemaWithProjectSlugOrId,
      );

      // Should filter both organizationSlug (direct match) and projectSlugOrId (alias)
      expect(keys).toEqual(["organizationSlug", "projectSlugOrId"]);
    });

    it("does NOT apply alias when projectSlugOrId is explicitly constrained", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: "project-a",
        projectSlugOrId: "project-b", // Explicit constraint takes precedence
        regionUrl: null,
      };

      const keys = getConstraintKeysToFilter(
        constraints,
        schemaWithProjectSlugOrId,
      );

      // Should filter organizationSlug and projectSlugOrId (explicit), but NOT the alias
      expect(keys).toEqual(["organizationSlug", "projectSlugOrId"]);
    });

    it("handles null/falsy constraint values", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: null, // Falsy - should not trigger alias
        regionUrl: null,
      };

      const keys = getConstraintKeysToFilter(
        constraints,
        schemaWithProjectSlugOrId,
      );

      expect(keys).toEqual(["organizationSlug"]);
    });

    it("handles empty string as falsy", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: "", // Empty string is falsy
        regionUrl: null,
      };

      const keys = getConstraintKeysToFilter(
        constraints,
        schemaWithProjectSlugOrId,
      );

      // Empty string is falsy, so no alias should be applied
      expect(keys).toEqual(["organizationSlug"]);
    });

    it("only filters parameters that exist in the tool schema", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: "my-project",
        regionUrl: "https://us.sentry.io",
      };

      const schemaWithoutRegion = {
        organizationSlug: z.string(),
        query: z.string(),
      };

      const keys = getConstraintKeysToFilter(constraints, schemaWithoutRegion);

      // regionUrl not in schema, so it shouldn't be filtered
      expect(keys).toEqual(["organizationSlug"]);
    });
  });

  describe("getConstraintParametersToInject", () => {
    it("injects direct constraint matches", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: null,
        regionUrl: null,
      };

      const params = getConstraintParametersToInject(
        constraints,
        schemaWithProjectSlug,
      );

      expect(params).toEqual({
        organizationSlug: "my-org",
      });
    });

    it("injects projectSlug as projectSlugOrId when alias applies", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: "my-project",
        regionUrl: null,
      };

      const params = getConstraintParametersToInject(
        constraints,
        schemaWithProjectSlugOrId,
      );

      expect(params).toEqual({
        organizationSlug: "my-org",
        projectSlugOrId: "my-project", // Injected via alias
      });
    });

    it("respects explicit projectSlugOrId constraint over alias", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: "project-a",
        projectSlugOrId: "project-b", // Explicit constraint
        regionUrl: null,
      };

      const params = getConstraintParametersToInject(
        constraints,
        schemaWithProjectSlugOrId,
      );

      expect(params).toEqual({
        organizationSlug: "my-org",
        projectSlugOrId: "project-b", // Explicit wins, not alias
      });
    });

    it("handles null/falsy constraint values", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: null,
        regionUrl: null,
      };

      const params = getConstraintParametersToInject(
        constraints,
        schemaWithProjectSlugOrId,
      );

      expect(params).toEqual({
        organizationSlug: "my-org",
      });
    });

    it("only injects parameters that exist in the tool schema", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: "my-project",
        regionUrl: "https://us.sentry.io",
      };

      const schemaWithoutRegion = {
        organizationSlug: z.string(),
        query: z.string(),
      };

      const params = getConstraintParametersToInject(
        constraints,
        schemaWithoutRegion,
      );

      // regionUrl not in schema, so it shouldn't be injected
      expect(params).toEqual({
        organizationSlug: "my-org",
      });
    });
  });

  describe("Consistency between filtering and injection", () => {
    it("ensures filtered keys match injected keys", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: "my-project",
        regionUrl: null,
      };

      const keysToFilter = getConstraintKeysToFilter(
        constraints,
        schemaWithProjectSlugOrId,
      );
      const paramsToInject = getConstraintParametersToInject(
        constraints,
        schemaWithProjectSlugOrId,
      );

      // Every key that's filtered should have a corresponding injected parameter
      const injectedKeys = Object.keys(paramsToInject);
      expect(keysToFilter.sort()).toEqual(injectedKeys.sort());
    });

    it("handles explicit constraint precedence consistently", () => {
      const constraints = {
        organizationSlug: "my-org",
        projectSlug: "project-a",
        projectSlugOrId: "project-b",
        regionUrl: null,
      };

      const keysToFilter = getConstraintKeysToFilter(
        constraints,
        schemaWithProjectSlugOrId,
      );
      const paramsToInject = getConstraintParametersToInject(
        constraints,
        schemaWithProjectSlugOrId,
      );

      // Both should handle the explicit constraint the same way
      expect(keysToFilter).toContain("projectSlugOrId");
      expect(paramsToInject.projectSlugOrId).toBe("project-b");
      expect(paramsToInject.projectSlug).toBeUndefined();
    });
  });
});

```

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

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

// Import all JSON files directly
import android from "./data/android.json";
import app from "./data/app.json";
import artifact from "./data/artifact.json";
import aspnetcore from "./data/aspnetcore.json";
import aws from "./data/aws.json";
import azure from "./data/azure.json";
import browser from "./data/browser.json";
import cassandra from "./data/cassandra.json";
import cicd from "./data/cicd.json";
import client from "./data/client.json";
import cloud from "./data/cloud.json";
import cloudevents from "./data/cloudevents.json";
import cloudfoundry from "./data/cloudfoundry.json";
import code from "./data/code.json";
import container from "./data/container.json";
import cpu from "./data/cpu.json";
import cpython from "./data/cpython.json";
import database from "./data/database.json";
import db from "./data/db.json";
import deployment from "./data/deployment.json";
import destination from "./data/destination.json";
import device from "./data/device.json";
import disk from "./data/disk.json";
import dns from "./data/dns.json";
import dotnet from "./data/dotnet.json";
import elasticsearch from "./data/elasticsearch.json";
import enduser from "./data/enduser.json";
import error from "./data/error.json";
import faas from "./data/faas.json";
import feature_flags from "./data/feature_flags.json";
import file from "./data/file.json";
import gcp from "./data/gcp.json";
import gen_ai from "./data/gen_ai.json";
import geo from "./data/geo.json";
import go from "./data/go.json";
import graphql from "./data/graphql.json";
import hardware from "./data/hardware.json";
import heroku from "./data/heroku.json";
import host from "./data/host.json";
import http from "./data/http.json";
import ios from "./data/ios.json";
import jvm from "./data/jvm.json";
import k8s from "./data/k8s.json";
import linux from "./data/linux.json";
import log from "./data/log.json";
import mcp from "./data/mcp.json";
import messaging from "./data/messaging.json";
import network from "./data/network.json";
import nodejs from "./data/nodejs.json";
import oci from "./data/oci.json";
import opentracing from "./data/opentracing.json";
import os from "./data/os.json";
import otel from "./data/otel.json";
import peer from "./data/peer.json";
import process from "./data/process.json";
import profile from "./data/profile.json";
import rpc from "./data/rpc.json";
import server from "./data/server.json";
import service from "./data/service.json";
import session from "./data/session.json";
import signalr from "./data/signalr.json";
import source from "./data/source.json";
import system from "./data/system.json";
import telemetry from "./data/telemetry.json";
import test from "./data/test.json";
import thread from "./data/thread.json";
import tls from "./data/tls.json";
import url from "./data/url.json";
import user from "./data/user.json";
import v8js from "./data/v8js.json";
import vcs from "./data/vcs.json";
import webengine from "./data/webengine.json";
import zos from "./data/zos.json";

// Create the namespaceData object
const namespaceData: Record<string, NamespaceData> = {
  android,
  app,
  artifact,
  aspnetcore,
  aws,
  azure,
  browser,
  cassandra,
  cicd,
  client,
  cloud,
  cloudevents,
  cloudfoundry,
  code,
  container,
  cpu,
  cpython,
  database,
  db,
  deployment,
  destination,
  device,
  disk,
  dns,
  dotnet,
  elasticsearch,
  enduser,
  error,
  faas,
  feature_flags,
  file,
  gcp,
  gen_ai,
  geo,
  go,
  graphql,
  hardware,
  heroku,
  host,
  http,
  ios,
  jvm,
  k8s,
  linux,
  log,
  mcp,
  messaging,
  network,
  nodejs,
  oci,
  opentracing,
  os,
  otel,
  peer,
  process,
  profile,
  rpc,
  server,
  service,
  session,
  signalr,
  source,
  system,
  telemetry,
  test,
  thread,
  tls,
  url,
  user,
  v8js,
  vcs,
  webengine,
  zos,
};

// TypeScript types
interface NamespaceData {
  namespace: string;
  description: string;
  attributes: Record<
    string,
    {
      description: string;
      type: string;
      examples?: Array<any>;
      note?: string;
      stability?: string;
    }
  >;
  custom?: boolean;
}

/**
 * Lookup OpenTelemetry semantic convention attributes for a given namespace
 */
export async function lookupOtelSemantics(
  namespace: string,
  dataset: "errors" | "logs" | "spans",
  apiService: SentryApiService,
  organizationSlug: string,
  projectId?: string,
): Promise<string> {
  // Normalize namespace (replace - with _)
  const normalizedNamespace = namespace.replace(/-/g, "_");

  // Check if namespace exists
  const data = namespaceData[normalizedNamespace];
  if (!data) {
    // Try to find similar namespaces
    const allNamespaces = Object.keys(namespaceData);
    const suggestions = allNamespaces
      .filter((ns) => ns.includes(namespace) || namespace.includes(ns))
      .slice(0, 3);

    return suggestions.length > 0
      ? `Namespace '${namespace}' not found. Did you mean: ${suggestions.join(", ")}?`
      : `Namespace '${namespace}' not found. Use 'list' to see all available namespaces.`;
  }

  // Format the response
  let response = `# OpenTelemetry Semantic Conventions: ${data.namespace}\n\n`;
  response += `${data.description}\n\n`;

  if (data.custom) {
    response +=
      "**Note:** This is a custom namespace, not part of standard OpenTelemetry conventions.\n\n";
  }

  // Get all attributes
  const attributes = Object.entries(data.attributes);

  response += `## Attributes (${attributes.length} total)\n\n`;

  // Sort attributes by key
  const sortedAttributes = attributes.sort(([a], [b]) => a.localeCompare(b));

  for (const [key, attr] of sortedAttributes) {
    response += `### \`${key}\`\n`;
    response += `- **Type:** ${attr.type}\n`;
    response += `- **Description:** ${attr.description}\n`;

    if (attr.stability) {
      response += `- **Stability:** ${attr.stability}\n`;
    }

    if (attr.examples && attr.examples.length > 0) {
      response += `- **Examples:** ${attr.examples
        .map(
          (ex) =>
            `\`${typeof ex === "object" ? JSON.stringify(ex) : String(ex)}\``,
        )
        .join(", ")}\n`;
    }

    if (attr.note) {
      response += `- **Note:** ${attr.note}\n`;
    }

    response += "\n";
  }

  return response;
}

/**
 * Create the otel-semantics-lookup tool for AI agents
 */
/**
 * Create a tool for looking up OpenTelemetry semantic convention attributes
 * The tool is pre-bound with the API service and organization configured for the appropriate region
 */
export function createOtelLookupTool(options: {
  apiService: SentryApiService;
  organizationSlug: string;
  projectId?: string;
}) {
  const { apiService, organizationSlug, projectId } = options;
  return agentTool({
    description:
      "Look up OpenTelemetry semantic convention attributes for a specific namespace. OpenTelemetry attributes are universal standards that work across all datasets.",
    parameters: z.object({
      namespace: z
        .string()
        .describe(
          "The OpenTelemetry namespace to look up (e.g., 'gen_ai', 'db', 'http', 'mcp')",
        ),
      dataset: z
        .enum(["spans", "errors", "logs"])
        .describe(
          "REQUIRED: Dataset to check attribute availability in. The agent MUST specify this based on their chosen dataset.",
        ),
    }),
    execute: async ({ namespace, dataset }) => {
      return await lookupOtelSemantics(
        namespace,
        dataset,
        apiService,
        organizationSlug,
        projectId,
      );
    },
  });
}

```

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

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

import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { Command } from "commander";
import { config } from "dotenv";
import path from "node:path";
import { fileURLToPath } from "node:url";
import chalk from "chalk";
import * as Sentry from "@sentry/node";
import { connectToMCPServer } from "./mcp-test-client.js";
import { connectToRemoteMCPServer } from "./mcp-test-client-remote.js";
import { runAgent } from "./agent.js";
import { logError, logInfo } from "./logger.js";
import { sentryBeforeSend } from "@sentry/mcp-server/telem/sentry";
import type { MCPConnection } from "./types.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.resolve(__dirname, "../../../");

// Load environment variables from multiple possible locations
// IMPORTANT: Do NOT use override:true as it would overwrite shell/CI environment variables
config(); // Try current directory first (.env in mcp-test-client)
config({ path: path.join(rootDir, ".env") }); // Also try root directory (fallback for shared values)

const program = new Command();

// OAuth support for MCP server (not Sentry directly)

program
  .name("mcp-test-client")
  .description("CLI tool to test Sentry MCP server")
  .version("0.0.1")
  .argument("[prompt]", "Prompt to send to the AI agent")
  .option("-m, --model <model>", "Override the AI model to use")
  .option("--access-token <token>", "Sentry access token")
  .option(
    "--mcp-host <host>",
    "MCP server host",
    process.env.MCP_URL || "http://localhost:5173",
  )
  .option("--sentry-dsn <dsn>", "Sentry DSN for error reporting")
  .option(
    "--agent",
    "Use agent mode (/mcp?agent=1) instead of standard mode (for use_sentry tool)",
  )
  .action(async (prompt, options) => {
    try {
      // Initialize Sentry with CLI-provided DSN if available
      const sentryDsn =
        options.sentryDsn ||
        process.env.SENTRY_DSN ||
        process.env.DEFAULT_SENTRY_DSN;

      Sentry.init({
        dsn: sentryDsn,
        sendDefaultPii: true,
        tracesSampleRate: 1,
        beforeSend: sentryBeforeSend,
        initialScope: {
          tags: {
            "gen_ai.agent.name": "sentry-mcp-agent",
            "gen_ai.system": "openai",
          },
        },
        release: process.env.SENTRY_RELEASE,
        integrations: [
          Sentry.consoleIntegration(),
          Sentry.zodErrorsIntegration(),
          Sentry.vercelAIIntegration({
            recordInputs: true,
            recordOutputs: true,
          }),
        ],
        environment:
          process.env.SENTRY_ENVIRONMENT ??
          (process.env.NODE_ENV !== "production"
            ? "development"
            : "production"),
      });

      // Check for access token in priority order
      const accessToken =
        options.accessToken || process.env.SENTRY_ACCESS_TOKEN;
      const sentryHost = process.env.SENTRY_HOST;

      const openaiKey = process.env.OPENAI_API_KEY;

      // Determine mode based on access token availability
      const useLocalMode = !!accessToken;

      // Only require Sentry access token for local mode
      if (useLocalMode && !accessToken) {
        logError("No Sentry access token found");
        console.log(chalk.yellow("To authenticate with Sentry:\n"));
        console.log(chalk.gray("1. Use an access token:"));
        console.log(
          chalk.gray("   - Set SENTRY_ACCESS_TOKEN environment variable"),
        );
        console.log(chalk.gray("   - Add it to your .env file"));
        console.log(chalk.gray("   - Or use --access-token flag\n"));
        console.log(chalk.gray("2. Get an access token from:"));
        console.log(
          chalk.gray(
            "   https://sentry.io/settings/account/api/auth-tokens/\n",
          ),
        );
        console.log(
          chalk.gray(
            "Required scopes: org:read, project:read, project:write, team:read, team:write, event:write\n",
          ),
        );
        process.exit(1);
      }

      if (!openaiKey) {
        logError("OPENAI_API_KEY environment variable is required");
        console.log(
          chalk.yellow("\nPlease set it in your .env file or environment:"),
        );
        console.log(chalk.gray("OPENAI_API_KEY=your_openai_api_key"));
        process.exit(1);
      }

      // Connect to MCP server
      let connection: MCPConnection;
      if (useLocalMode) {
        // Use local stdio transport when access token is provided
        connection = await connectToMCPServer({
          accessToken,
          host: sentryHost || process.env.SENTRY_HOST,
          sentryDsn: sentryDsn,
          useAgentEndpoint: options.agent,
        });
      } else {
        // Use remote SSE transport when no access token
        connection = await connectToRemoteMCPServer({
          mcpHost: options.mcpHost,
          accessToken: accessToken,
          useAgentEndpoint: options.agent,
        });
      }

      // Set conversation ID as a global tag for all traces
      Sentry.setTag("gen_ai.conversation.id", connection.sessionId);

      const agentConfig = {
        model: options.model,
      };

      try {
        if (prompt) {
          // Single prompt mode
          await runAgent(connection, prompt, agentConfig);
        } else {
          // Interactive mode (default when no prompt provided)
          logInfo("Interactive mode", "type 'exit', 'quit', or Ctrl+D to end");
          console.log(); // Add extra newline after startup message

          const rl = readline.createInterface({ input, output });

          while (true) {
            try {
              const userInput = await rl.question(chalk.gray("> "));

              // Handle null input (Ctrl+D / EOF)
              if (userInput === null) {
                logInfo("Goodbye!");
                break;
              }

              if (
                userInput.toLowerCase() === "exit" ||
                userInput.toLowerCase() === "quit"
              ) {
                logInfo("Goodbye!");
                break;
              }

              if (userInput.trim()) {
                await runAgent(connection, userInput, agentConfig);
                // Add newline after response before next prompt
                console.log();
              }
            } catch (error) {
              // Handle EOF (Ctrl+D) which may throw an error
              if (
                (error as any).code === "ERR_USE_AFTER_CLOSE" ||
                (error instanceof Error && error.message?.includes("EOF"))
              ) {
                logInfo("Goodbye!");
                break;
              }
              throw error;
            }
          }

          rl.close();
        }
      } finally {
        // Always disconnect
        await connection.disconnect();
        // Ensure Sentry events are flushed
        await Sentry.flush(5000);
      }
    } catch (error) {
      const eventId = Sentry.captureException(error);
      logError(
        "Fatal error",
        `${error instanceof Error ? error.message : String(error)}. Event ID: ${eventId}`,
      );
      // Ensure Sentry events are flushed before exit
      await Sentry.flush(5000);
      process.exit(1);
    }
  });

// Handle uncaught errors
process.on("unhandledRejection", async (error) => {
  const eventId = Sentry.captureException(error);
  logError(
    "Unhandled error",
    `${error instanceof Error ? error.message : String(error)}. Event ID: ${eventId}`,
  );
  // Ensure Sentry events are flushed before exit
  await Sentry.flush(5000);
  process.exit(1);
});

program.parse(process.argv);

```

--------------------------------------------------------------------------------
/packages/mcp-server-evals/src/evals/utils/toolPredictionScorer.ts:
--------------------------------------------------------------------------------

```typescript
import { openai } from "@ai-sdk/openai";
import { generateObject, type LanguageModel } from "ai";
import { z } from "zod";
import { experimental_createMCPClient } from "ai";
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio";

// Cache for available tools to avoid reconnecting for each test
let cachedTools: string[] | null = null;

/**
 * Get available tools from the MCP server by connecting to it directly.
 * This ensures the tool list stays in sync with what's actually registered.
 */
async function getAvailableTools(): Promise<string[]> {
  if (cachedTools) {
    return cachedTools;
  }

  // Use pnpm exec to run the binary from the workspace
  const transport = new Experimental_StdioMCPTransport({
    command: "pnpm",
    args: [
      "exec",
      "sentry-mcp",
      "--access-token=mocked-access-token",
      "--all-scopes",
    ],
    env: {
      ...process.env,
      SENTRY_ACCESS_TOKEN: "mocked-access-token",
      SENTRY_HOST: "sentry.io",
    },
  });

  const client = await experimental_createMCPClient({
    transport,
  });

  // Discover available tools
  const toolsMap = await client.tools();

  // Convert tools to the format expected by the scorer
  cachedTools = Object.entries(toolsMap).map(([name, tool]) => {
    // Extract the first line of description for a concise summary
    const shortDescription = (tool as any).description?.split("\n")[0] || "";
    return `${name} - ${shortDescription}`;
  });

  // Clean up
  await client.close();

  return cachedTools;
}

export interface ExpectedToolCall {
  name: string;
  arguments: Record<string, any>;
}

interface ToolPredictionScorerOptions {
  input: string;
  output: string;
  expectedTools?: ExpectedToolCall[];
  result?: any;
}

const defaultModel = openai("gpt-4o");

const predictionSchema = z.object({
  score: z.number().min(0).max(1).describe("Score from 0 to 1"),
  rationale: z.string().describe("Explanation of the score"),
  predictedTools: z
    .array(
      z.object({
        name: z.string(),
        arguments: z.record(z.any()).optional().default({}),
      }),
    )
    .describe("What tools the AI would likely call"),
});

function generateSystemPrompt(
  availableTools: string[],
  task: string,
  expectedDescription: string,
): string {
  return `You are evaluating whether an AI assistant with access to Sentry MCP tools would make the correct tool calls for a given task.

[AVAILABLE TOOLS]
${availableTools.join("\n")}

[TASK]
${task}

[EXPECTED TOOL CALLS]
${expectedDescription}

Based on the task and available tools, predict what tools the AI would call to complete this task.

IMPORTANT: Look at what information is already provided in the task:
- When only an organization name is given (e.g., "in sentry-mcp-evals"), discovery calls ARE typically needed
- When organization/project are given in "org/project" format, the AI may skip discovery if confident
- The expected tool calls show what is ACTUALLY expected for this specific case - follow them exactly
- Discovery calls (find_organizations, find_projects) are commonly used to get regionUrl and verify access
- Match the expected tool sequence exactly - if expected includes discovery, predict discovery

Consider:
1. Match the expected tool sequence exactly - the expected tools show realistic AI behavior
2. When a value like "sentry-mcp-evals" appears alone, it's typically an organizationSlug, not a projectSlug
3. Arguments should match expected values (organizationSlug, projectSlug, name, etc.)
4. For natural language queries in search_events, exact phrasing doesn't need to match
5. Extra parameters like regionUrl are acceptable
6. The AI commonly does discovery calls even when slugs appear to be provided, to get region info

Score as follows:
- 1.0: All expected tools would be called with correct arguments in the right order
- 0.8: All expected tools would be called, minor differences (extra params, slight variations)
- 0.6: Most expected tools would be called but missing some or wrong order
- 0.3: Some expected tools would be called but significant issues
- 0.0: Wrong tools or critical tools missing

CRITICAL: The expected tools represent the actual realistic behavior for this specific case. Follow the expected sequence exactly:
- If expected tools include discovery calls, predict discovery calls
- If expected tools do NOT include discovery calls, do NOT predict them
- The test author has determined what's appropriate for each specific scenario`;
}

/**
 * A scorer that uses AI to predict what tools would be called without executing them.
 * This is much faster than actually running the tools and checking what was called.
 *
 * @param model - Optional language model to use for predictions (defaults to gpt-4o)
 * @returns A scorer function that compares predicted vs expected tool calls
 *
 * @example
 * ```typescript
 * import { ToolPredictionScorer } from './utils/toolPredictionScorer';
 * import { NoOpTaskRunner } from './utils/runner';
 * import { describeEval } from 'vitest-evals';
 *
 * describeEval("Sentry issue search", {
 *   data: async () => [
 *     {
 *       input: "Find the newest issues in my-org",
 *       expectedTools: [
 *         { name: "find_organizations", arguments: {} },
 *         { name: "find_issues", arguments: { organizationSlug: "my-org", sortBy: "first_seen" } }
 *       ]
 *     }
 *   ],
 *   task: NoOpTaskRunner(), // Don't execute tools, just predict them
 *   scorers: [ToolPredictionScorer()],
 *   threshold: 0.8
 * });
 * ```
 *
 * The scorer works by:
 * 1. Connecting to the MCP server to get available tools and their descriptions
 * 2. Using AI to predict what tools would be called for the given task
 * 3. Comparing predictions against the expectedTools array
 * 4. Returning a score from 0.0 to 1.0 based on accuracy
 *
 * Scoring criteria:
 * - 1.0: All expected tools predicted with correct arguments in right order
 * - 0.8: All expected tools predicted, minor differences (extra params, slight variations)
 * - 0.6: Most expected tools predicted but missing some or wrong order
 * - 0.3: Some expected tools predicted but significant issues
 * - 0.0: Wrong tools or critical tools missing
 *
 * If `expectedTools` is not provided in test data, the scorer is automatically skipped
 * and returns `{ score: null }` to allow other scorers to run without interference.
 */
export function ToolPredictionScorer(model: LanguageModel = defaultModel) {
  return async function ToolPredictionScorer(
    opts: ToolPredictionScorerOptions,
  ) {
    // If expectedTools is not defined, skip this scorer
    if (!opts.expectedTools) {
      return {
        score: null,
        metadata: {
          rationale: "Skipped: No expectedTools defined for this test case",
        },
      };
    }

    const expectedTools = opts.expectedTools;

    // Get available tools from the MCP server
    const AVAILABLE_TOOLS = await getAvailableTools();

    // Generate a description of the expected tools for the prompt
    const expectedDescription = expectedTools
      .map(
        (tool) =>
          `- ${tool.name} with arguments: ${JSON.stringify(tool.arguments)}`,
      )
      .join("\n");

    const { object } = await generateObject({
      model,
      prompt: generateSystemPrompt(
        AVAILABLE_TOOLS,
        opts.input,
        expectedDescription,
      ),
      schema: predictionSchema,
      experimental_telemetry: {
        isEnabled: true,
        functionId: "tool_prediction_scorer",
      },
    });

    return {
      score: object.score,
      metadata: {
        rationale: object.rationale,
        predictedTools: object.predictedTools,
        expectedTools: expectedTools,
      },
    };
  };
}

```

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

```json
{
  "namespace": "vcs",
  "description": "This group defines the attributes for [Version Control Systems (VCS)](https://wikipedia.org/wiki/Version_control).\n",
  "attributes": {
    "vcs.repository.url.full": {
      "description": "The [canonical URL](https://support.google.com/webmasters/answer/10347851?hl=en#:~:text=A%20canonical%20URL%20is%20the,Google%20chooses%20one%20as%20canonical.) of the repository providing the complete HTTP(S) address in order to locate and identify the repository through a browser.\n",
      "type": "string",
      "note": "In Git Version Control Systems, the canonical URL SHOULD NOT include\nthe `.git` extension.\n",
      "stability": "development",
      "examples": [
        "https://github.com/opentelemetry/open-telemetry-collector-contrib",
        "https://gitlab.com/my-org/my-project/my-projects-project/repo"
      ]
    },
    "vcs.repository.name": {
      "description": "The human readable name of the repository. It SHOULD NOT include any additional identifier like Group/SubGroup in GitLab or organization in GitHub.\n",
      "type": "string",
      "note": "Due to it only being the name, it can clash with forks of the same\nrepository if collecting telemetry across multiple orgs or groups in\nthe same backends.\n",
      "stability": "development",
      "examples": ["semantic-conventions", "my-cool-repo"]
    },
    "vcs.ref.base.name": {
      "description": "The name of the [reference](https://git-scm.com/docs/gitglossary#def_ref) such as **branch** or **tag** in the repository.\n",
      "type": "string",
      "note": "`base` refers to the starting point of a change. For example, `main`\nwould be the base reference of type branch if you've created a new\nreference of type branch from it and created new commits.\n",
      "stability": "development",
      "examples": ["my-feature-branch", "tag-1-test"]
    },
    "vcs.ref.base.type": {
      "description": "The type of the [reference](https://git-scm.com/docs/gitglossary#def_ref) in the repository.\n",
      "type": "string",
      "note": "`base` refers to the starting point of a change. For example, `main`\nwould be the base reference of type branch if you've created a new\nreference of type branch from it and created new commits.\n",
      "stability": "development",
      "examples": ["branch", "tag"]
    },
    "vcs.ref.base.revision": {
      "description": "The revision, literally [revised version](https://www.merriam-webster.com/dictionary/revision), The revision most often refers to a commit object in Git, or a revision number in SVN.\n",
      "type": "string",
      "note": "`base` refers to the starting point of a change. For example, `main`\nwould be the base reference of type branch if you've created a new\nreference of type branch from it and created new commits. The\nrevision can be a full [hash value (see\nglossary)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf),\nof the recorded change to a ref within a repository pointing to a\ncommit [commit](https://git-scm.com/docs/git-commit) object. It does\nnot necessarily have to be a hash; it can simply define a [revision\nnumber](https://svnbook.red-bean.com/en/1.7/svn.tour.revs.specifiers.html)\nwhich is an integer that is monotonically increasing. In cases where\nit is identical to the `ref.base.name`, it SHOULD still be included.\nIt is up to the implementer to decide which value to set as the\nrevision based on the VCS system and situational context.\n",
      "stability": "development",
      "examples": [
        "9d59409acf479dfa0df1aa568182e43e43df8bbe28d60fcf2bc52e30068802cc",
        "main",
        "123",
        "HEAD"
      ]
    },
    "vcs.ref.head.name": {
      "description": "The name of the [reference](https://git-scm.com/docs/gitglossary#def_ref) such as **branch** or **tag** in the repository.\n",
      "type": "string",
      "note": "`head` refers to where you are right now; the current reference at a\ngiven time.\n",
      "stability": "development",
      "examples": ["my-feature-branch", "tag-1-test"]
    },
    "vcs.ref.head.type": {
      "description": "The type of the [reference](https://git-scm.com/docs/gitglossary#def_ref) in the repository.\n",
      "type": "string",
      "note": "`head` refers to where you are right now; the current reference at a\ngiven time.\n",
      "stability": "development",
      "examples": ["branch", "tag"]
    },
    "vcs.ref.head.revision": {
      "description": "The revision, literally [revised version](https://www.merriam-webster.com/dictionary/revision), The revision most often refers to a commit object in Git, or a revision number in SVN.\n",
      "type": "string",
      "note": "`head` refers to where you are right now; the current reference at a\ngiven time.The revision can be a full [hash value (see\nglossary)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf),\nof the recorded change to a ref within a repository pointing to a\ncommit [commit](https://git-scm.com/docs/git-commit) object. It does\nnot necessarily have to be a hash; it can simply define a [revision\nnumber](https://svnbook.red-bean.com/en/1.7/svn.tour.revs.specifiers.html)\nwhich is an integer that is monotonically increasing. In cases where\nit is identical to the `ref.head.name`, it SHOULD still be included.\nIt is up to the implementer to decide which value to set as the\nrevision based on the VCS system and situational context.\n",
      "stability": "development",
      "examples": [
        "9d59409acf479dfa0df1aa568182e43e43df8bbe28d60fcf2bc52e30068802cc",
        "main",
        "123",
        "HEAD"
      ]
    },
    "vcs.ref.type": {
      "description": "The type of the [reference](https://git-scm.com/docs/gitglossary#def_ref) in the repository.\n",
      "type": "string",
      "stability": "development",
      "examples": ["branch", "tag"]
    },
    "vcs.revision_delta.direction": {
      "description": "The type of revision comparison.\n",
      "type": "string",
      "stability": "development",
      "examples": ["behind", "ahead"]
    },
    "vcs.line_change.type": {
      "description": "The type of line change being measured on a branch or change.\n",
      "type": "string",
      "stability": "development",
      "examples": ["added", "removed"]
    },
    "vcs.change.title": {
      "description": "The human readable title of the change (pull request/merge request/changelist). This title is often a brief summary of the change and may get merged in to a ref as the commit summary.\n",
      "type": "string",
      "stability": "development",
      "examples": [
        "Fixes broken thing",
        "feat: add my new feature",
        "[chore] update dependency"
      ]
    },
    "vcs.change.id": {
      "description": "The ID of the change (pull request/merge request/changelist) if applicable. This is usually a unique (within repository) identifier generated by the VCS system.\n",
      "type": "string",
      "stability": "development",
      "examples": ["123"]
    },
    "vcs.change.state": {
      "description": "The state of the change (pull request/merge request/changelist).\n",
      "type": "string",
      "stability": "development",
      "examples": ["open", "wip", "closed", "merged"]
    },
    "vcs.owner.name": {
      "description": "The group owner within the version control system.\n",
      "type": "string",
      "stability": "development",
      "examples": ["my-org", "myteam", "business-unit"]
    },
    "vcs.provider.name": {
      "description": "The name of the version control system provider.\n",
      "type": "string",
      "stability": "development",
      "examples": ["github", "gitlab", "gittea", "gitea", "bitbucket"]
    }
  }
}

```

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

```json
{
  "namespace": "http",
  "description": "This document defines semantic convention attributes in the HTTP namespace.",
  "attributes": {
    "http.request.body.size": {
      "description": "The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size.\n",
      "type": "number",
      "stability": "development",
      "examples": ["3495"]
    },
    "http.request.header": {
      "description": "HTTP request headers, `<key>` being the normalized HTTP Header name (lowercase), the value being the header values.\n",
      "type": "string",
      "note": "Instrumentations SHOULD require an explicit configuration of which headers are to be captured.\nIncluding all request headers can be a security risk - explicit configuration helps avoid leaking sensitive information.\n\nThe `User-Agent` header is already captured in the `user_agent.original` attribute.\nUsers MAY explicitly configure instrumentations to capture them even though it is not recommended.\n\nThe attribute value MUST consist of either multiple header values as an array of strings\nor a single-item array containing a possibly comma-concatenated string, depending on the way\nthe HTTP library provides access to headers.\n\nExamples:\n\n- A header `Content-Type: application/json` SHOULD be recorded as the `http.request.header.content-type`\n  attribute with value `[\"application/json\"]`.\n- A header `X-Forwarded-For: 1.2.3.4, 1.2.3.5` SHOULD be recorded as the `http.request.header.x-forwarded-for`\n  attribute with value `[\"1.2.3.4\", \"1.2.3.5\"]` or `[\"1.2.3.4, 1.2.3.5\"]` depending on the HTTP library.\n",
      "stability": "stable",
      "examples": ["[\"application/json\"]", "[\"1.2.3.4\",\"1.2.3.5\"]"]
    },
    "http.request.method": {
      "description": "HTTP request method.",
      "type": "string",
      "note": "HTTP request method value SHOULD be \"known\" to the instrumentation.\nBy default, this convention defines \"known\" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods)\nand the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html).\n\nIf the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER`.\n\nIf the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override\nthe list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named\nOTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods\n(this list MUST be a full override of the default known method, it is not a list of known methods in addition to the defaults).\n\nHTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly.\nInstrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent.\nTracing instrumentations that do so, MUST also set `http.request.method_original` to the original value.\n",
      "stability": "stable",
      "examples": [
        "CONNECT",
        "DELETE",
        "GET",
        "HEAD",
        "OPTIONS",
        "PATCH",
        "POST",
        "PUT",
        "TRACE",
        "_OTHER"
      ]
    },
    "http.request.method_original": {
      "description": "Original HTTP method sent by the client in the request line.",
      "type": "string",
      "stability": "stable",
      "examples": ["GeT", "ACL", "foo"]
    },
    "http.request.resend_count": {
      "description": "The ordinal number of request resending attempt (for any reason, including redirects).\n",
      "type": "number",
      "note": "The resend count SHOULD be updated each time an HTTP request gets resent by the client, regardless of what was the cause of the resending (e.g. redirection, authorization failure, 503 Server Unavailable, network issues, or any other).\n",
      "stability": "stable",
      "examples": ["3"]
    },
    "http.request.size": {
      "description": "The total size of the request in bytes. This should be the total number of bytes sent over the wire, including the request line (HTTP/1.1), framing (HTTP/2 and HTTP/3), headers, and request body if any.\n",
      "type": "number",
      "stability": "development",
      "examples": ["1437"]
    },
    "http.response.body.size": {
      "description": "The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size.\n",
      "type": "number",
      "stability": "development",
      "examples": ["3495"]
    },
    "http.response.header": {
      "description": "HTTP response headers, `<key>` being the normalized HTTP Header name (lowercase), the value being the header values.\n",
      "type": "string",
      "note": "Instrumentations SHOULD require an explicit configuration of which headers are to be captured.\nIncluding all response headers can be a security risk - explicit configuration helps avoid leaking sensitive information.\n\nUsers MAY explicitly configure instrumentations to capture them even though it is not recommended.\n\nThe attribute value MUST consist of either multiple header values as an array of strings\nor a single-item array containing a possibly comma-concatenated string, depending on the way\nthe HTTP library provides access to headers.\n\nExamples:\n\n- A header `Content-Type: application/json` header SHOULD be recorded as the `http.request.response.content-type`\n  attribute with value `[\"application/json\"]`.\n- A header `My-custom-header: abc, def` header SHOULD be recorded as the `http.response.header.my-custom-header`\n  attribute with value `[\"abc\", \"def\"]` or `[\"abc, def\"]` depending on the HTTP library.\n",
      "stability": "stable",
      "examples": ["[\"application/json\"]", "[\"abc\",\"def\"]"]
    },
    "http.response.size": {
      "description": "The total size of the response in bytes. This should be the total number of bytes sent over the wire, including the status line (HTTP/1.1), framing (HTTP/2 and HTTP/3), headers, and response body and trailers if any.\n",
      "type": "number",
      "stability": "development",
      "examples": ["1437"]
    },
    "http.response.status_code": {
      "description": "[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).",
      "type": "number",
      "stability": "stable",
      "examples": ["200"]
    },
    "http.route": {
      "description": "The matched route, that is, the path template in the format used by the respective server framework.\n",
      "type": "string",
      "note": "MUST NOT be populated when this is not supported by the HTTP server framework as the route attribute should have low-cardinality and the URI path can NOT substitute it.\nSHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one.\n",
      "stability": "stable",
      "examples": ["/users/:userID?", "{controller}/{action}/{id?}"]
    },
    "http.connection.state": {
      "description": "State of the HTTP connection in the HTTP connection pool.",
      "type": "string",
      "stability": "development",
      "examples": ["active", "idle"]
    }
  }
}

```
Page 5/12FirstPrevNextLast