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

# Directory Structure

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

# Files

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

```typescript
/**
 * Reusable Zod parameter schemas for MCP tools.
 *
 * Shared validation schemas used across tool definitions to ensure consistent
 * parameter handling and validation. Each schema includes transformation
 * (e.g., toLowerCase, trim) and LLM-friendly descriptions.
 */
import { z } from "zod";
import { SENTRY_GUIDES } from "./constants";
import { validateSlug } from "./utils/slug-validation";

export const ParamOrganizationSlug = z
  .string()
  .toLowerCase()
  .trim()
  .superRefine(validateSlug)
  .describe(
    "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool.",
  );

export const ParamTeamSlug = z
  .string()
  .toLowerCase()
  .trim()
  .superRefine(validateSlug)
  .describe(
    "The team's slug. You can find a list of existing teams in an organization using the `find_teams()` tool.",
  );

export const ParamProjectSlug = z
  .string()
  .toLowerCase()
  .trim()
  .superRefine(validateSlug)
  .describe(
    "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool.",
  );

export const ParamProjectSlugOrAll = z
  .string()
  .toLowerCase()
  .trim()
  .superRefine(validateSlug)
  .describe(
    "The project's slug. This will default to all projects you have access to. It is encouraged to specify this when possible.",
  );

export const ParamSearchQuery = z
  .string()
  .trim()
  .describe(
    "Search query to filter results by name or slug. Use this to narrow down results when there are many items.",
  );

export const ParamIssueShortId = z
  .string()
  .toUpperCase()
  .trim()
  .describe("The Issue ID. e.g. `PROJECT-1Z43`");

export const ParamIssueUrl = z
  .string()
  .url()
  .trim()
  .describe(
    "The URL of the issue. e.g. https://my-organization.sentry.io/issues/PROJECT-1Z43",
  );

export const ParamTraceId = z
  .string()
  .trim()
  .regex(
    /^[0-9a-fA-F]{32}$/,
    "Trace ID must be a 32-character hexadecimal string",
  )
  .describe("The trace ID. e.g. `a4d1aae7216b47ff8117cf4e09ce9d0a`");

export const ParamPlatform = z
  .string()
  .toLowerCase()
  .trim()
  .describe(
    "The platform for the project. e.g., python, javascript, react, etc.",
  );

export const ParamTransaction = z
  .string()
  .trim()
  .describe("The transaction name. Also known as the endpoint, or route name.");

export const ParamQuery = z
  .string()
  .trim()
  .describe(
    `The search query to apply. Use the \`help(subject="query_syntax")\` tool to get more information about the query syntax rather than guessing.`,
  );

/**
 * Region URL parameter for Sentry API requests.
 *
 * Handles region-specific URLs for Sentry's Cloud Service while gracefully
 * supporting self-hosted Sentry installations that may return empty regionUrl values.
 * This schema accepts both valid URLs and empty strings to ensure compatibility
 * across different Sentry deployment types.
 */
export const ParamRegionUrl = z
  .string()
  .trim()
  .refine((value) => !value || z.string().url().safeParse(value).success, {
    message: "Must be a valid URL or empty string (for self-hosted Sentry)",
  })
  .describe(
    "The region URL for the organization you're querying, if known. " +
      "For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. " +
      "For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. " +
      "You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
  );

export const ParamIssueStatus = z
  .enum(["resolved", "resolvedInNextRelease", "unresolved", "ignored"])
  .describe(
    "The new status for the issue. Valid values are 'resolved', 'resolvedInNextRelease', 'unresolved', and 'ignored'.",
  );

export const ParamAssignedTo = z
  .string()
  .trim()
  .describe(
    "The assignee in format 'user:ID' or 'team:ID' where ID is numeric. Example: 'user:123456' or 'team:789'. Use the whoami tool to find your user ID.",
  );

export const ParamSentryGuide = z
  .enum(SENTRY_GUIDES)
  .describe(
    "Optional guide filter to limit search results to specific documentation sections. " +
      "Use either a platform (e.g., 'javascript', 'python') or platform/guide combination (e.g., 'javascript/nextjs', 'python/django').",
  );

export const ParamEventId = z.string().trim().describe("The ID of the event.");

export const ParamAttachmentId = z
  .string()
  .trim()
  .describe("The ID of the attachment to download.");

```

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

```typescript
/**
 * Issue parameter parsing and validation utilities.
 *
 * Handles flexible input formats for Sentry issues (URLs vs explicit parameters),
 * extracts organization and issue identifiers, and validates issue ID formats.
 * Provides robust parsing for LLM-generated parameters that may contain formatting
 * inconsistencies.
 */

import { UserInputError } from "../errors";

/**
 * Extracts the Sentry issue ID and organization slug from a full URL
 *
 * @param url - A full Sentry issue URL
 * @returns Object containing the numeric issue ID and organization slug (if found)
 * @throws Error if the input is invalid
 */
export function extractIssueId(url: string): {
  issueId: string;
  organizationSlug: string;
} {
  if (!url || typeof url !== "string") {
    throw new UserInputError(
      "Invalid Sentry issue URL. URL must be a non-empty string.",
    );
  }

  if (!url.startsWith("http://") && !url.startsWith("https://")) {
    throw new UserInputError(
      "Invalid Sentry issue URL. Must start with http:// or https://",
    );
  }

  let parsedUrl: URL;
  try {
    parsedUrl = new URL(url);
  } catch (error) {
    throw new UserInputError(
      `Invalid Sentry issue URL. Unable to parse URL: ${url}`,
    );
  }

  const pathParts = parsedUrl.pathname.split("/").filter(Boolean);
  if (pathParts.length < 2 || !pathParts.includes("issues")) {
    throw new UserInputError(
      "Invalid Sentry issue URL. Path must contain '/issues/{issue_id}'",
    );
  }

  const issueId = pathParts[pathParts.indexOf("issues") + 1];
  if (!issueId) {
    throw new UserInputError("Unable to determine issue ID from URL.");
  }

  // Extract organization slug from either the path or subdomain
  let organizationSlug: string | undefined;
  if (pathParts.includes("organizations")) {
    organizationSlug = pathParts[pathParts.indexOf("organizations") + 1];
  } else if (pathParts.length > 1 && pathParts[0] !== "issues") {
    // If URL is like sentry.io/sentry/issues/123
    organizationSlug = pathParts[0];
  } else {
    // Check for subdomain
    const hostParts = parsedUrl.hostname.split(".");
    if (hostParts.length > 2 && hostParts[0] !== "www") {
      organizationSlug = hostParts[0];
    }
  }

  if (!organizationSlug) {
    throw new UserInputError(
      "Invalid Sentry issue URL. Could not determine organization.",
    );
  }

  return { issueId, organizationSlug };
}

/**
 * Sometimes the LLM will pass in a funky issue shortId. For example it might pass
 * in "CLOUDFLARE-MCP-41." instead of "CLOUDFLARE-MCP-41". This function attempts to
 * fix common issues.
 *
 * @param issueId - The issue ID to parse
 * @returns The parsed issue ID
 */
export function parseIssueId(issueId: string) {
  if (!issueId.trim()) {
    throw new UserInputError("Issue ID cannot be empty");
  }

  let finalIssueId = issueId;
  // remove trailing punctuation
  finalIssueId = finalIssueId.replace(/[^\w-]/g, "");

  if (!finalIssueId) {
    throw new UserInputError(
      "Issue ID cannot be empty after removing special characters",
    );
  }

  // Validate against common Sentry issue ID patterns
  // Either numeric IDs or PROJECT-ABC123 format
  // Allow project codes to start with alphanumeric characters (including numbers)
  const validFormatRegex = /^(\d+|[A-Za-z0-9][\w-]*-[A-Za-z0-9]+)$/;

  if (!validFormatRegex.test(finalIssueId)) {
    throw new UserInputError(
      `Invalid issue ID format: "${finalIssueId}". Expected either a numeric ID or a project code followed by an alphanumeric identifier (e.g., "PROJECT-ABC123").`,
    );
  }

  return finalIssueId;
}

/**
 * Parses issue parameters from a variety of formats.
 *
 * @param params - Object containing issue URL, issue ID, and organization slug
 * @returns Object containing the parsed organization slug and issue ID
 * @throws Error if the input is invalid
 */
export function parseIssueParams({
  issueUrl,
  issueId,
  organizationSlug,
}: {
  issueUrl?: string | null;
  issueId?: string | null;
  organizationSlug?: string | null;
}): {
  organizationSlug: string;
  issueId: string;
} {
  if (issueUrl) {
    const resolved = extractIssueId(issueUrl);
    if (!resolved) {
      throw new Error(
        "Invalid Sentry issue URL. Path should contain '/issues/{issue_id}'",
      );
    }
    return {
      ...resolved,
      issueId: parseIssueId(resolved.issueId),
    };
  }

  if (!organizationSlug) {
    throw new UserInputError("Organization slug is required");
  }

  if (issueId) {
    return {
      organizationSlug,
      issueId: parseIssueId(issueId),
    };
  }

  throw new UserInputError("Either issueId or issueUrl must be provided");
}

```

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

```json
{
  "namespace": "host",
  "description": "A host is defined as a computing instance. For example, physical servers, virtual machines, switches or disk array.\n",
  "attributes": {
    "host.id": {
      "description": "Unique host ID. For Cloud, this must be the instance_id assigned by the cloud provider. For non-containerized systems, this should be the `machine-id`. See the table below for the sources to use to determine the `machine-id` based on operating system.\n",
      "type": "string",
      "stability": "development",
      "examples": ["fdbf79e8af94cb7f9e8df36789187052"]
    },
    "host.name": {
      "description": "Name of the host. On Unix systems, it may contain what the hostname command returns, or the fully qualified hostname, or another name specified by the user.\n",
      "type": "string",
      "stability": "development",
      "examples": ["opentelemetry-test"]
    },
    "host.type": {
      "description": "Type of host. For Cloud, this must be the machine type.\n",
      "type": "string",
      "stability": "development",
      "examples": ["n1-standard-1"]
    },
    "host.arch": {
      "description": "The CPU architecture the host system is running on.\n",
      "type": "string",
      "stability": "development",
      "examples": [
        "amd64",
        "arm32",
        "arm64",
        "ia64",
        "ppc32",
        "ppc64",
        "s390x",
        "x86"
      ]
    },
    "host.image.name": {
      "description": "Name of the VM image or OS install the host was instantiated from.\n",
      "type": "string",
      "stability": "development",
      "examples": [
        "infra-ami-eks-worker-node-7d4ec78312",
        "CentOS-8-x86_64-1905"
      ]
    },
    "host.image.id": {
      "description": "VM image ID or host OS image ID. For Cloud, this value is from the provider.\n",
      "type": "string",
      "stability": "development",
      "examples": ["ami-07b06b442921831e5"]
    },
    "host.image.version": {
      "description": "The version string of the VM image or host OS as defined in [Version Attributes](/docs/resource/README.md#version-attributes).\n",
      "type": "string",
      "stability": "development",
      "examples": ["0.1"]
    },
    "host.ip": {
      "description": "Available IP addresses of the host, excluding loopback interfaces.\n",
      "type": "string",
      "note": "IPv4 Addresses MUST be specified in dotted-quad notation. IPv6 addresses MUST be specified in the [RFC 5952](https://www.rfc-editor.org/rfc/rfc5952.html) format.\n",
      "stability": "development",
      "examples": ["[\"192.168.1.140\",\"fe80::abc2:4a28:737a:609e\"]"]
    },
    "host.mac": {
      "description": "Available MAC addresses of the host, excluding loopback interfaces.\n",
      "type": "string",
      "note": "MAC Addresses MUST be represented in [IEEE RA hexadecimal form](https://standards.ieee.org/wp-content/uploads/import/documents/tutorials/eui.pdf): as hyphen-separated octets in uppercase hexadecimal form from most to least significant.\n",
      "stability": "development",
      "examples": ["[\"AC-DE-48-23-45-67\",\"AC-DE-48-23-45-67-01-9F\"]"]
    },
    "host.cpu.vendor.id": {
      "description": "Processor manufacturer identifier. A maximum 12-character string.\n",
      "type": "string",
      "note": "[CPUID](https://wiki.osdev.org/CPUID) command returns the vendor ID string in EBX, EDX and ECX registers. Writing these to memory in this order results in a 12-character string.\n",
      "stability": "development",
      "examples": ["GenuineIntel"]
    },
    "host.cpu.family": {
      "description": "Family or generation of the CPU.\n",
      "type": "string",
      "stability": "development",
      "examples": ["6", "PA-RISC 1.1e"]
    },
    "host.cpu.model.id": {
      "description": "Model identifier. It provides more granular information about the CPU, distinguishing it from other CPUs within the same family.\n",
      "type": "string",
      "stability": "development",
      "examples": ["6", "9000/778/B180L"]
    },
    "host.cpu.model.name": {
      "description": "Model designation of the processor.\n",
      "type": "string",
      "stability": "development",
      "examples": ["11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz"]
    },
    "host.cpu.stepping": {
      "description": "Stepping or core revisions.\n",
      "type": "string",
      "stability": "development",
      "examples": ["1", "r1p1"]
    },
    "host.cpu.cache.l2.size": {
      "description": "The amount of level 2 memory cache available to the processor (in Bytes).\n",
      "type": "number",
      "stability": "development",
      "examples": ["12288000"]
    }
  }
}

```

--------------------------------------------------------------------------------
/packages/mcp-server-mocks/src/fixtures/issue.json:
--------------------------------------------------------------------------------

```json
{
  "id": "6507376925",
  "shareId": null,
  "shortId": "CLOUDFLARE-MCP-41",
  "title": "Error: Tool list_organizations is already registered",
  "culprit": "Object.fetch(index)",
  "permalink": "https://sentry-mcp-evals.sentry.io/issues/6507376925/",
  "logger": null,
  "level": "error",
  "status": "unresolved",
  "statusDetails": {},
  "substatus": "ongoing",
  "isPublic": false,
  "platform": "javascript",
  "project": {
    "id": "4509062593708032",
    "name": "CLOUDFLARE-MCP",
    "slug": "CLOUDFLARE-MCP",
    "platform": "bun"
  },
  "type": "error",
  "metadata": {
    "value": "Tool list_organizations is already registered",
    "type": "Error",
    "filename": "index.js",
    "function": "Object.fetch",
    "in_app_frame_mix": "in-app-only",
    "sdk": {
      "name": "sentry.javascript.cloudflare",
      "name_normalized": "sentry.javascript.cloudflare"
    },
    "severity": 0,
    "severity_reason": "ml",
    "initial_priority": 50,
    "title": "Error: Tool list_organizations is already registered"
  },
  "numComments": 0,
  "assignedTo": null,
  "isBookmarked": false,
  "isSubscribed": false,
  "subscriptionDetails": null,
  "hasSeen": true,
  "annotations": [],
  "issueType": "error",
  "issueCategory": "error",
  "priority": "medium",
  "priorityLockedAt": null,
  "isUnhandled": true,
  "count": "25",
  "userCount": 1,
  "firstSeen": "2025-04-03T22:51:19.403000Z",
  "lastSeen": "2025-04-12T11:34:11Z",
  "firstRelease": null,
  "lastRelease": null,
  "activity": [
    {
      "id": "4633815464",
      "user": null,
      "type": "auto_set_ongoing",
      "data": {
        "after_days": 7
      },
      "dateCreated": "2025-04-10T22:55:22.411699Z"
    },
    {
      "id": "0",
      "user": null,
      "type": "first_seen",
      "data": {
        "priority": "medium"
      },
      "dateCreated": "2025-04-03T22:51:19.403000Z"
    }
  ],
  "openPeriods": [
    {
      "start": "2025-04-03T22:51:19.403000Z",
      "end": null,
      "duration": null,
      "isOpen": true,
      "lastChecked": "2025-04-12T11:34:11.310000Z"
    }
  ],
  "seenBy": [
    {
      "id": "1",
      "name": "David Cramer",
      "username": "[email protected]",
      "email": "[email protected]",
      "avatarUrl": null,
      "isActive": true,
      "hasPasswordAuth": true,
      "isManaged": false,
      "dateJoined": "2012-01-14T22:08:29.270831Z",
      "lastLogin": "2025-04-13T14:00:11.516852Z",
      "has2fa": true,
      "lastActive": "2025-04-13T18:10:49.177605Z",
      "isSuperuser": true,
      "isStaff": true,
      "experiments": {},
      "emails": [
        {
          "id": "87429",
          "email": "[email protected]",
          "is_verified": true
        }
      ],
      "options": {
        "theme": "light",
        "language": "en",
        "stacktraceOrder": 2,
        "defaultIssueEvent": "recommended",
        "timezone": "US/Pacific",
        "clock24Hours": false
      },
      "flags": {
        "newsletter_consent_prompt": false
      },
      "avatar": {
        "avatarType": "upload",
        "avatarUuid": "51e63edabf31412aa2a955e9cf2c1ca0",
        "avatarUrl": "https://sentry.io/avatar/51e63edabf31412aa2a955e9cf2c1ca0/"
      },
      "identities": [],
      "lastSeen": "2025-04-08T23:15:26.569455Z"
    }
  ],
  "pluginActions": [],
  "pluginIssues": [],
  "pluginContexts": [],
  "userReportCount": 0,
  "stats": {
    "24h": [
      [1744480800, 0],
      [1744484400, 0],
      [1744488000, 0],
      [1744491600, 0],
      [1744495200, 0],
      [1744498800, 0],
      [1744502400, 0],
      [1744506000, 0],
      [1744509600, 0],
      [1744513200, 0],
      [1744516800, 0],
      [1744520400, 0],
      [1744524000, 0],
      [1744527600, 0],
      [1744531200, 0],
      [1744534800, 0],
      [1744538400, 0],
      [1744542000, 0],
      [1744545600, 0],
      [1744549200, 0],
      [1744552800, 0],
      [1744556400, 0],
      [1744560000, 0],
      [1744563600, 0],
      [1744567200, 0]
    ],
    "30d": [
      [1741910400, 0],
      [1741996800, 0],
      [1742083200, 0],
      [1742169600, 0],
      [1742256000, 0],
      [1742342400, 0],
      [1742428800, 0],
      [1742515200, 0],
      [1742601600, 0],
      [1742688000, 0],
      [1742774400, 0],
      [1742860800, 0],
      [1742947200, 0],
      [1743033600, 0],
      [1743120000, 0],
      [1743206400, 0],
      [1743292800, 0],
      [1743379200, 0],
      [1743465600, 0],
      [1743552000, 0],
      [1743638400, 1],
      [1743724800, 0],
      [1743811200, 0],
      [1743897600, 0],
      [1743984000, 0],
      [1744070400, 20],
      [1744156800, 1],
      [1744243200, 1],
      [1744329600, 0],
      [1744416000, 2],
      [1744502400, 0]
    ]
  },
  "participants": []
}

```

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

```typescript
import type { Issue } from "../../api-client";
import { getIssueUrl, getIssuesSearchUrl } from "../../utils/url-utils";
import * as Sentry from "@sentry/node";

/**
 * Format an explanation for how a natural language query was translated
 */
export function formatExplanation(explanation: string): string {
  return `## How I interpreted your query\n\n${explanation}`;
}

export interface FormatIssueResultsParams {
  issues: Issue[];
  organizationSlug: string;
  projectSlugOrId?: string;
  query?: string | null;
  regionUrl?: string;
  naturalLanguageQuery?: string;
  skipHeader?: boolean;
}

/**
 * Format issue search results for display
 */
export function formatIssueResults(params: FormatIssueResultsParams): string {
  const {
    issues,
    organizationSlug,
    projectSlugOrId,
    query,
    regionUrl,
    naturalLanguageQuery,
    skipHeader = false,
  } = params;

  const host = regionUrl ? new URL(regionUrl).host : "sentry.io";

  let output = "";

  // Skip header section if requested (when called from handler with includeExplanation)
  if (!skipHeader) {
    // Use natural language query in title if provided, otherwise fall back to org/project
    if (naturalLanguageQuery) {
      output = `# Search Results for "${naturalLanguageQuery}"\n\n`;
    } else {
      output = `# Issues in **${organizationSlug}`;
      if (projectSlugOrId) {
        output += `/${projectSlugOrId}`;
      }
      output += "**\n\n";
    }

    // Add display instructions for UI
    output += `⚠️ **IMPORTANT**: Display these issues as highlighted cards with status indicators, assignee info, and clickable Issue IDs.\n\n`;
  }

  if (issues.length === 0) {
    Sentry.logger.info(
      Sentry.logger
        .fmt`No issues found for query: ${naturalLanguageQuery || query}`,
      {
        query,
        organizationSlug,
        projectSlug: projectSlugOrId,
        naturalLanguageQuery,
      },
    );
    output += "No issues found matching your search criteria.\n\n";
    output += "Try adjusting your search criteria or time range.";
    return output;
  }

  // Generate search URL for viewing results
  const searchUrl = getIssuesSearchUrl(
    host,
    organizationSlug,
    query,
    projectSlugOrId,
  );

  // Add view link with emoji and guidance text (like search_events)
  output += `**View these results in Sentry**:\n${searchUrl}\n`;
  output += `_Please share this link with the user to view the search results in their Sentry dashboard._\n\n`;

  output += `Found **${issues.length}** issue${issues.length === 1 ? "" : "s"}:\n\n`;

  // Format each issue
  issues.forEach((issue, index) => {
    // Generate issue URL using the utility function
    const issueUrl = getIssueUrl(host, organizationSlug, issue.shortId);

    output += `## ${index + 1}. [${issue.shortId}](${issueUrl})\n\n`;
    output += `**${issue.title}**\n\n`;

    // Issue metadata
    // Issues don't have a level field in the API response
    output += `- **Status**: ${issue.status}\n`;
    output += `- **Users**: ${issue.userCount || 0}\n`;
    output += `- **Events**: ${issue.count || 0}\n`;

    if (issue.assignedTo) {
      const assignee = issue.assignedTo;
      if (typeof assignee === "string") {
        output += `- **Assigned to**: ${assignee}\n`;
      } else if (
        assignee &&
        typeof assignee === "object" &&
        "name" in assignee
      ) {
        output += `- **Assigned to**: ${assignee.name}\n`;
      }
    }

    output += `- **First seen**: ${formatDate(issue.firstSeen)}\n`;
    output += `- **Last seen**: ${formatDate(issue.lastSeen)}\n`;

    if (issue.culprit) {
      output += `- **Culprit**: \`${issue.culprit}\`\n`;
    }

    output += "\n";
  });

  // Add next steps section (like search_events)
  output += "## Next Steps\n\n";
  output +=
    "- Get more details about a specific issue: Use the Issue ID with get_issue_details\n";
  output +=
    "- Update issue status: Use update_issue to resolve or assign issues\n";
  output +=
    "- View event counts: Use search_events for aggregated statistics\n";

  return output;
}

/**
 * Format date for display
 */
function formatDate(dateString?: string | null): string {
  if (!dateString) return "N/A";

  const date = new Date(dateString);
  const now = new Date();
  const diffMs = now.getTime() - date.getTime();
  const diffHours = Math.floor(diffMs / (1000 * 60 * 60));

  if (diffHours < 1) {
    const diffMinutes = Math.floor(diffMs / (1000 * 60));
    return `${diffMinutes} minute${diffMinutes === 1 ? "" : "s"} ago`;
  }
  if (diffHours < 24) {
    return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`;
  }
  const diffDays = Math.floor(diffHours / 24);
  if (diffDays < 30) {
    return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
  }
  return date.toLocaleDateString();
}

```

--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------

```yaml
name: Deploy to Cloudflare

permissions:
  contents: read
  deployments: write
  checks: write

on:
  workflow_run:
    workflows: ["Test"]
    types:
      - completed
    branches: [main]
  workflow_dispatch:

jobs:
  deploy:
    name: Deploy to Cloudflare
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}

    steps:
      - uses: actions/checkout@v4

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

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

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

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

      - name: Install dependencies
        run: pnpm install

      # === BUILD AND DEPLOY CANARY WORKER ===
      - name: Build
        working-directory: packages/mcp-cloudflare
        run: pnpm build
        env:
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

      - name: Deploy to Canary Worker
        id: deploy_canary
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          workingDirectory: packages/mcp-cloudflare
          command: deploy --config wrangler.canary.jsonc
          packageManager: pnpm

      - name: Wait for Canary to Propagate
        if: success()
        run: |
          echo "Waiting 30 seconds for canary deployment to propagate..."
          sleep 30

      # === SMOKE TEST CANARY ===
      - name: Run Smoke Tests on Canary
        id: canary_smoke_tests
        if: success()
        env:
          PREVIEW_URL: https://sentry-mcp-canary.getsentry.workers.dev
        run: |
          echo "Running smoke tests against canary worker..."
          cd packages/smoke-tests
          pnpm test:ci

      - name: Publish Canary Smoke Test Report
        uses: mikepenz/action-junit-report@cf701569b05ccdd861a76b8607a66d76f6fd4857
        if: always() && steps.canary_smoke_tests.outcome != 'skipped'
        with:
          report_paths: "packages/smoke-tests/tests.junit.xml"
          check_name: "Canary Smoke Test Results"
          fail_on_failure: false

      # === DEPLOY PRODUCTION WORKER (only if canary tests pass) ===
      - name: Deploy to Production Worker
        id: deploy_production
        if: steps.canary_smoke_tests.outcome == 'success'
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          workingDirectory: packages/mcp-cloudflare
          command: deploy
          packageManager: pnpm

      - name: Wait for Production to Propagate
        if: steps.deploy_production.outcome == 'success'
        run: |
          echo "Waiting 30 seconds for production deployment to propagate..."
          sleep 30

      # === SMOKE TEST PRODUCTION ===
      - name: Run Smoke Tests on Production
        id: production_smoke_tests
        if: steps.deploy_production.outcome == 'success'
        env:
          PREVIEW_URL: https://mcp.sentry.dev
        run: |
          echo "Running smoke tests on production..."
          cd packages/smoke-tests
          pnpm test:ci

      - name: Publish Production Smoke Test Report
        uses: mikepenz/action-junit-report@cf701569b05ccdd861a76b8607a66d76f6fd4857
        if: always() && steps.production_smoke_tests.outcome != 'skipped'
        with:
          report_paths: "packages/smoke-tests/tests.junit.xml"
          check_name: "Production Smoke Test Results"
          fail_on_failure: false

      # === ROLLBACK IF PRODUCTION SMOKE TESTS FAIL ===
      - name: Rollback Production on Smoke Test Failure
        if: steps.production_smoke_tests.outcome == 'failure'
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          workingDirectory: packages/mcp-cloudflare
          command: rollback
          packageManager: pnpm
        continue-on-error: true

      - name: Fail Job if Production Smoke Tests Failed
        if: steps.production_smoke_tests.outcome == 'failure'
        run: |
          echo "Production smoke tests failed - job failed after rollback"
          exit 1

```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/components/chat/types.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Type definitions for Chat components
 */

import type React from "react";
import type { Message } from "ai/react";

// Re-export AI SDK types for convenience
export type { Message } from "ai/react";

// Extended message type that includes our custom metadata
export interface ExtendedMessage extends Message {
  data?: {
    type?: string;
    prompts?: any[];
    toolsDetailed?: Array<{ name: string; description: string }>;
    hasSlashCommands?: boolean;
    error?: string;
    // Prompt execution data
    promptName?: string;
    parameters?: Record<string, any>;
    wasExecuted?: boolean;
    simulateStreaming?: boolean;
    [key: string]: any;
  };
}

// Error handling types (simplified)
// We only keep this for potential server response parsing
export interface ChatErrorData {
  error?: string;
  name?: string;
  eventId?: string;
  statusCode?: number;
  message?: string;
}

// Authentication types
export interface AuthState {
  isLoading: boolean;
  isAuthenticated: boolean;
  authToken: string;
  isAuthenticating: boolean;
  authError: string;
}

export interface AuthActions {
  handleOAuthLogin: () => void;
  handleLogout: () => void;
  clearAuthState: () => void;
}

export type AuthContextType = AuthState & AuthActions;

// OAuth message types
export interface OAuthSuccessMessage {
  type: "SENTRY_AUTH_SUCCESS";
  data: Record<string, never>;
}

export interface OAuthErrorMessage {
  type: "SENTRY_AUTH_ERROR";
  error?: string;
}

export type OAuthMessage = OAuthSuccessMessage | OAuthErrorMessage;

// Tool invocation types
export interface ToolInvocationContent {
  type: "text";
  text: string;
}

export interface ToolInvocationUnknownContent {
  type: string;
  [key: string]: unknown;
}

export type ToolMessage = ToolInvocationContent | ToolInvocationUnknownContent;

// Define our own ToolInvocation interface since AI SDK's is not properly exported
export interface ChatToolInvocation {
  toolCallId: string;
  toolName: string;
  args: Record<string, unknown>;
  state: "partial-call" | "call" | "result";
  result?: {
    content: ToolMessage[];
  };
}

// Message processing types
export interface ProcessedMessagePart {
  part: NonNullable<Message["parts"]>[number];
  messageId: string;
  messageRole: string;
  partIndex: number;
  isStreaming: boolean;
}

// Component prop types
export interface ChatProps {
  isOpen: boolean;
  onClose: () => void;
  onLogout: () => void;
}

export interface ChatUIProps {
  messages: Message[];
  input: string;
  error?: Error | null;
  isChatLoading: boolean;
  isOpen?: boolean;
  showControls?: boolean;
  onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
  onStop?: () => void;
  onRetry?: () => void;
  onClose?: () => void;
  onLogout?: () => void;
  onSlashCommand?: (command: string) => void;
  onSendPrompt?: (prompt: string) => void;
}

export interface ChatMessagesProps {
  messages: Message[];
  isChatLoading: boolean;
  isLocalStreaming?: boolean;
  isMessageStreaming?: (messageId: string) => boolean;
  error?: Error | null;
  onRetry?: () => void;
  onSlashCommand?: (command: string) => void;
}

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

export interface AuthFormProps {
  authError: string;
  onOAuthLogin: () => void;
}

export interface PanelBackdropProps {
  isOpen: boolean;
  onClose: () => void;
}

export interface MessagePartProps {
  part: NonNullable<Message["parts"]>[number];
  messageId: string;
  messageRole: string;
  partIndex: number;
  isStreaming?: boolean;
  messageData?: any;
  onSlashCommand?: (command: string) => void;
}

export interface TextPartProps {
  text: string;
  role: string;
  messageId: string;
  isStreaming?: boolean;
  messageData?: any;
  onSlashCommand?: (command: string) => void;
}

export interface ToolPartProps {
  toolInvocation: ChatToolInvocation;
  messageId: string;
  partIndex: number;
}

export interface ToolInvocationProps {
  tool: ChatToolInvocation;
  messageId: string;
  index: number;
}

// Type guards
export function isTextMessage(
  message: ToolMessage,
): message is ToolInvocationContent {
  return message.type === "text";
}

export function isOAuthSuccessMessage(
  message: unknown,
): message is OAuthSuccessMessage {
  return (
    typeof message === "object" &&
    message !== null &&
    "type" in message &&
    message.type === "SENTRY_AUTH_SUCCESS"
  );
}

export function isOAuthErrorMessage(
  message: unknown,
): message is OAuthErrorMessage {
  return (
    typeof message === "object" &&
    message !== null &&
    "type" in message &&
    message.type === "SENTRY_AUTH_ERROR"
  );
}

```

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

```typescript
import { memo } from "react";
import { Markdown } from "../ui/markdown";
import { InteractiveMarkdown } from "../ui/interactive-markdown";
import { Typewriter } from "../ui/typewriter";
import { ToolInvocation } from "./tool-invocation";
import { Terminal } from "lucide-react";
import type {
  MessagePartProps,
  TextPartProps,
  ToolPartProps,
  ChatToolInvocation,
} from "./types";

// Component for rendering text parts
const TextPart = memo(function TextPart({
  text,
  role,
  messageId,
  isStreaming,
  messageData,
  onSlashCommand,
}: TextPartProps) {
  const isAssistant = role === "assistant";
  const isUser = role === "user";
  const isSlashCommand = isUser && text.startsWith("/");
  const isPromptExecution = isUser && messageData?.type === "prompt-execution";

  if (isUser) {
    // User messages: flexible width with background
    return (
      <div className="flex justify-end">
        <div
          className={`px-4 py-2 rounded max-w-3xl ${
            isSlashCommand
              ? "bg-blue-900/50 border border-blue-700/50"
              : isPromptExecution
                ? "bg-purple-900/50 border border-purple-700/50"
                : "bg-slate-800"
          }`}
        >
          {isSlashCommand ? (
            <div className="flex items-center gap-2">
              <Terminal className="h-4 w-4 text-blue-400" />
              <span className="text-blue-300 font-mono text-sm">{text}</span>
            </div>
          ) : isPromptExecution ? (
            <div className="space-y-2">
              <div className="flex items-center gap-2">
                <Terminal className="h-4 w-4 text-purple-400" />
                <span className="text-purple-300 font-semibold text-sm">
                  Prompt: {messageData.promptName}
                </span>
              </div>
              {messageData.parameters &&
                Object.keys(messageData.parameters).length > 0 && (
                  <div className="text-xs text-purple-200/80 ml-6">
                    {Object.entries(messageData.parameters).map(
                      ([key, value]) => (
                        <div key={key}>
                          <span className="text-purple-300">{key}:</span>{" "}
                          {String(value)}
                        </div>
                      ),
                    )}
                  </div>
                )}
              {messageData.wasExecuted && (
                <div className="text-xs text-purple-200/60 ml-6 italic">
                  ✓ Executed on server
                </div>
              )}
            </div>
          ) : (
            <Markdown>{text}</Markdown>
          )}
        </div>
      </div>
    );
  }

  // Assistant and system messages: no background, just text
  // System messages should animate if they're marked for streaming simulation
  const shouldAnimate =
    (isAssistant && isStreaming) ||
    (role === "system" && isStreaming && messageData?.simulateStreaming);
  const hasSlashCommands = messageData?.hasSlashCommands;

  return (
    <div className="mr-8">
      {shouldAnimate ? (
        <Typewriter text={text} speed={20}>
          {(displayedText) => (
            <InteractiveMarkdown
              hasSlashCommands={hasSlashCommands}
              onSlashCommand={onSlashCommand}
            >
              {displayedText}
            </InteractiveMarkdown>
          )}
        </Typewriter>
      ) : (
        <InteractiveMarkdown
          hasSlashCommands={hasSlashCommands}
          onSlashCommand={onSlashCommand}
        >
          {text}
        </InteractiveMarkdown>
      )}
    </div>
  );
});

// Component for rendering tool invocation parts
const ToolPart = memo(function ToolPart({
  toolInvocation,
  messageId,
  partIndex,
}: ToolPartProps) {
  return (
    <div className="mr-8">
      <ToolInvocation
        tool={toolInvocation}
        messageId={messageId}
        index={partIndex}
      />
    </div>
  );
});

// Main component for rendering individual message parts
const MessagePart = memo(function MessagePart({
  part,
  messageId,
  messageRole,
  partIndex,
  isStreaming,
  messageData,
  onSlashCommand,
}: MessagePartProps) {
  switch (part.type) {
    case "text":
      return (
        <TextPart
          text={part.text}
          role={messageRole}
          messageId={messageId}
          isStreaming={isStreaming}
          messageData={messageData}
          onSlashCommand={onSlashCommand}
        />
      );
    case "tool-invocation":
      return (
        <ToolPart
          toolInvocation={part.toolInvocation as ChatToolInvocation}
          messageId={messageId}
          partIndex={partIndex}
        />
      );
    default:
      // Fallback for unknown part types
      return null;
  }
});

// Export the memoized components
export { TextPart, ToolPart, MessagePart };

```

--------------------------------------------------------------------------------
/docs/cloudflare/architecture.md:
--------------------------------------------------------------------------------

```markdown
# Cloudflare Chat Agent Architecture

Technical architecture for the web-based chat interface hosted on Cloudflare Workers.

## Overview

The Cloudflare chat agent provides a web interface for interacting with Sentry through an AI assistant. It's built as a full-stack application using:

- **Frontend**: React with Tailwind CSS
- **Backend**: Cloudflare Workers with Hono framework
- **AI**: OpenAI GPT-4 via Vercel AI SDK
- **MCP Integration**: HTTP transport to core MCP server

## Package Structure

```
packages/mcp-cloudflare/
├── src/
│   ├── client/           # React frontend
│   │   ├── components/   # UI components
│   │   ├── contexts/     # React contexts
│   │   ├── hooks/        # Custom React hooks
│   │   └── utils/        # Client utilities
│   └── server/           # Cloudflare Workers backend
│       ├── lib/          # Server libraries
│       ├── routes/       # API routes
│       ├── types/        # TypeScript types
│       └── utils/        # Server utilities
├── public/               # Static assets
└── wrangler.toml         # Cloudflare configuration
```

## Key Components

### 1. OAuth Authentication

Handles Sentry OAuth flow for user authentication:

```typescript
// server/routes/auth.ts
export default new Hono()
  .get("/login", handleOAuthLogin)
  .get("/callback", handleOAuthCallback)
  .post("/logout", handleLogout);
```

**Features:**
- OAuth 2.0 flow with Sentry
- Token storage in Cloudflare KV
- Automatic token refresh
- Per-organization access control

### 2. Chat Interface

React-based chat UI with real-time streaming:

```typescript
// client/components/chat/chat.tsx
export function Chat() {
  const { messages, handleSubmit } = useChat({
    api: "/api/chat",
    headers: { Authorization: `Bearer ${authToken}` }
  });
}
```

**Features:**
- Message streaming with Vercel AI SDK
- Tool call visualization
- Slash commands (/help, /prompts, /clear)
- Prompt parameter dialogs
- Markdown rendering with syntax highlighting

### 3. MCP Integration

Connects to the core MCP server via HTTP transport:

```typescript
// server/routes/chat.ts
const mcpClient = await experimental_createMCPClient({
  name: "sentry",
  transport: {
    type: "sse",
    url: sseUrl,
    headers: { Authorization: `Bearer ${accessToken}` }
  }
});
```

**Features:**
- Server-sent events (SSE) for MCP communication
- Automatic tool discovery
- Prompt metadata endpoint
- Error handling with fallbacks

### 4. AI Assistant

GPT-4 integration with Sentry-specific system prompt:

```typescript
const result = streamText({
  model: openai("gpt-4o"),
  messages: processedMessages,
  tools: mcpTools,
  system: "You are an AI assistant for testing Sentry MCP..."
});
```

**Features:**
- Streaming responses
- Tool execution
- Prompt template processing
- Context-aware assistance

## Data Flow

1. **User Authentication**:
   ```
   User → OAuth Login → Sentry → OAuth Callback → KV Storage
   ```

2. **Chat Message Flow**:
   ```
   User Input → Chat API → Process Prompts → AI Model → Stream Response
                         ↓
                    MCP Server ← Tool Calls
   ```

3. **MCP Communication**:
   ```
   Chat Server → SSE Transport → MCP Server → Sentry API
   ```

## Deployment Architecture

### Cloudflare Resources

- **Workers**: Serverless compute for API routes
- **Pages**: Static asset hosting for React app
- **KV Namespace**: OAuth token storage
- **AI Binding**: Access to Cloudflare AI models (AutoRAG for docs search)
- **R2**: File storage (future)

### Environment Variables

Required for deployment:

```toml
[vars]
COOKIE_SECRET = "..."      # For session encryption
OPENAI_API_KEY = "..."     # For GPT-4 access
SENTRY_CLIENT_ID = "..."   # OAuth app ID
SENTRY_CLIENT_SECRET = "..." # OAuth app secret
```

### API Routes

- `/api/auth/*` - Authentication endpoints
- `/api/chat` - Main chat endpoint
- `/api/metadata` - MCP metadata endpoint
- `/sse` - Server-sent events for MCP

## Security Considerations

1. **Authentication**: OAuth tokens stored encrypted in KV
2. **Authorization**: Per-organization access control
3. **Rate Limiting**: Cloudflare rate limiter integration
4. **CORS**: Restricted to same-origin requests
5. **CSP**: Content Security Policy headers

## Performance Optimizations

1. **Edge Computing**: Runs at Cloudflare edge locations
2. **Caching**: Metadata endpoint with cache headers
3. **Streaming**: Server-sent events for real-time updates
4. **Bundle Splitting**: Optimized React build

## Monitoring

- Sentry integration for error tracking
- Cloudflare Analytics for usage metrics
- Custom telemetry for MCP operations

## Related Documentation

- See "OAuth Architecture" in @docs/cloudflare/oauth-architecture.md
- See "Chat Interface" in @docs/cloudflare/architecture.md
- See "Deployment" in @docs/cloudflare/deployment.md
- See "Architecture" in @docs/architecture.mdc

```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/find-releases.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,
  ParamProjectSlugOrAll,
} from "../schema";

export default defineTool({
  name: "find_releases",
  requiredScopes: ["project:read"],
  description: [
    "Find releases in Sentry.",
    "",
    "Use this tool when you need to:",
    "- Find recent releases in a Sentry organization",
    "- Find the most recent version released of a specific project",
    "- Determine when a release was deployed to an environment",
    "",
    "<examples>",
    "### Find the most recent releases in the 'my-organization' organization",
    "",
    "```",
    "find_releases(organizationSlug='my-organization')",
    "```",
    "",
    "### Find releases matching '2ce6a27' in the 'my-organization' organization",
    "",
    "```",
    "find_releases(organizationSlug='my-organization', query='2ce6a27')",
    "```",
    "</examples>",
    "",
    "<hints>",
    "- If the user passes a parameter in the form of name/otherName, its likely in the format of <organizationSlug>/<projectSlug>.",
    "</hints>",
  ].join("\n"),
  inputSchema: {
    organizationSlug: ParamOrganizationSlug,
    regionUrl: ParamRegionUrl.optional(),
    projectSlug: ParamProjectSlugOrAll.optional(),
    query: z
      .string()
      .trim()
      .describe("Search for versions which contain the provided string.")
      .optional(),
  },
  annotations: {
    readOnlyHint: true,
    openWorldHint: true,
  },
  async handler(params, context: ServerContext) {
    const apiService = apiServiceFromContext(context, {
      regionUrl: params.regionUrl,
    });
    const organizationSlug = params.organizationSlug;

    setTag("organization.slug", organizationSlug);

    const releases = await apiService.listReleases({
      organizationSlug,
      projectSlug: params.projectSlug,
      query: params.query,
    });
    let output = `# Releases in **${organizationSlug}${params.projectSlug ? `/${params.projectSlug}` : ""}**\n\n`;
    if (releases.length === 0) {
      output += "No releases found.\n";
      return output;
    }
    output += releases
      .map((release) => {
        const releaseInfo = [
          `## ${release.shortVersion}`,
          "",
          `**Created**: ${new Date(release.dateCreated).toISOString()}`,
        ];
        if (release.dateReleased) {
          releaseInfo.push(
            `**Released**: ${new Date(release.dateReleased).toISOString()}`,
          );
        }
        if (release.firstEvent) {
          releaseInfo.push(
            `**First Event**: ${new Date(release.firstEvent).toISOString()}`,
          );
        }
        if (release.lastEvent) {
          releaseInfo.push(
            `**Last Event**: ${new Date(release.lastEvent).toISOString()}`,
          );
        }
        if (release.newGroups !== undefined) {
          releaseInfo.push(`**New Issues**: ${release.newGroups}`);
        }
        if (release.projects && release.projects.length > 0) {
          releaseInfo.push(
            `**Projects**: ${release.projects.map((p) => p.name).join(", ")}`,
          );
        }
        if (release.lastCommit) {
          releaseInfo.push("", `### Last Commit`, "");
          releaseInfo.push(`**Commit ID**: ${release.lastCommit.id}`);
          releaseInfo.push(`**Commit Message**: ${release.lastCommit.message}`);
          releaseInfo.push(
            `**Commit Author**: ${release.lastCommit.author.name}`,
          );
          releaseInfo.push(
            `**Commit Date**: ${new Date(release.lastCommit.dateCreated).toISOString()}`,
          );
        }
        if (release.lastDeploy) {
          releaseInfo.push("", `### Last Deploy`, "");
          releaseInfo.push(`**Deploy ID**: ${release.lastDeploy.id}`);
          releaseInfo.push(
            `**Environment**: ${release.lastDeploy.environment}`,
          );
          if (release.lastDeploy.dateStarted) {
            releaseInfo.push(
              `**Deploy Started**: ${new Date(release.lastDeploy.dateStarted).toISOString()}`,
            );
          }
          if (release.lastDeploy.dateFinished) {
            releaseInfo.push(
              `**Deploy Finished**: ${new Date(release.lastDeploy.dateFinished).toISOString()}`,
            );
          }
        }
        return releaseInfo.join("\n");
      })
      .join("\n\n");
    output += "\n\n";
    output += "# Using this information\n\n";
    output += `- You can reference the Release version in commit messages or documentation.\n`;
    output += `- You can search for issues in a specific release using the \`find_errors()\` tool with the query \`release:${releases.length ? releases[0]!.shortVersion : "VERSION"}\`.\n`;
    return output;
  },
});

```

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

```typescript
/**
 * Reusable chat UI component
 * Extracts the common chat interface used in both mobile and desktop views
 */

import { LogOut, X, Bot, Sparkles } from "lucide-react";
import ScrollToBottom from "react-scroll-to-bottom";
import { Button } from "../ui/button";
import { ChatInput, ChatMessages } from ".";
import type { Message } from "ai/react";
import type { EndpointMode } from "../../hooks/use-endpoint-mode";

// Constant empty function to avoid creating new instances on every render
const EMPTY_FUNCTION = () => {};

// Sample prompts for quick access
const SAMPLE_PROMPTS = [
  {
    label: "Help",
    prompt: "/help",
  },
  {
    label: "React SDK Usage",
    prompt: "Show me how to set up the React SDK for error monitoring",
  },
  {
    label: "Recent Issues",
    prompt: "What are my most recent issues?",
  },
] as const;

interface ChatUIProps {
  messages: Message[];
  input: string;
  error?: Error | null;
  isChatLoading: boolean;
  isLocalStreaming?: boolean;
  isMessageStreaming?: (messageId: string) => boolean;
  isOpen?: boolean;
  showControls?: boolean;
  endpointMode?: EndpointMode;
  onInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
  onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
  onStop?: () => void;
  onRetry?: () => void;
  onClose?: () => void;
  onLogout?: () => void;
  onSlashCommand?: (command: string) => void;
  onSendPrompt?: (prompt: string) => void;
  onToggleEndpointMode?: () => void;
}

export const ChatUI = ({
  messages,
  input,
  error,
  isChatLoading,
  isLocalStreaming,
  isMessageStreaming,
  isOpen = true,
  showControls = false,
  endpointMode = "standard",
  onInputChange,
  onSubmit,
  onStop,
  onRetry,
  onClose,
  onLogout,
  onSlashCommand,
  onSendPrompt,
  onToggleEndpointMode,
}: ChatUIProps) => {
  const isAgentMode = endpointMode === "agent";

  return (
    <div className="h-full flex flex-col relative">
      {/* Floating Agent Mode Toggle - Top Right */}
      {onToggleEndpointMode && (
        <div className="absolute top-4 right-4 z-20">
          <Button
            type="button"
            onClick={onToggleEndpointMode}
            size="sm"
            variant={isAgentMode ? "default" : "outline"}
            title={
              isAgentMode
                ? "Agent mode: Only use_sentry tool (click to switch to standard)"
                : "Standard mode: All 19 tools available (click to switch to agent)"
            }
            className="shadow-lg"
          >
            {isAgentMode ? (
              <>
                <Sparkles className="h-4 w-4 mr-2" />
                Agent Mode
              </>
            ) : (
              <>
                <Bot className="h-4 w-4 mr-2" />
                Standard Mode
              </>
            )}
          </Button>
        </div>
      )}

      {/* Mobile header with close and logout buttons */}
      <div className="md:hidden flex items-center justify-between p-4 border-b border-slate-800 flex-shrink-0">
        {showControls && (
          <>
            <Button type="button" onClick={onClose} size="icon" title="Close">
              <X className="h-4 w-4" />
            </Button>

            <Button type="button" onClick={onLogout} size="icon" title="Logout">
              <LogOut className="h-4 w-4" />
            </Button>
          </>
        )}
      </div>

      {/* Chat Messages - Scrollable area */}
      <ScrollToBottom
        className="flex-1 mb-34 flex overflow-y-auto"
        scrollViewClassName="px-0"
        followButtonClassName="hidden"
        initialScrollBehavior="smooth"
      >
        <ChatMessages
          messages={messages}
          isChatLoading={isChatLoading}
          isLocalStreaming={isLocalStreaming}
          isMessageStreaming={isMessageStreaming}
          error={error}
          onRetry={onRetry}
          onSlashCommand={onSlashCommand}
        />
      </ScrollToBottom>

      {/* Chat Input - Always pinned at bottom */}
      <div className="py-4 px-6 bottom-0 left-0 right-0 absolute bg-slate-950/95 h-34 overflow-hidden z-10">
        {/* Sample Prompt Buttons - Always visible above input */}
        {onSendPrompt && (
          <div className="mb-4 flex flex-wrap gap-2 justify-center">
            {SAMPLE_PROMPTS.map((samplePrompt) => (
              <Button
                key={samplePrompt.label}
                type="button"
                onClick={() => onSendPrompt(samplePrompt.prompt)}
                size="sm"
                variant="outline"
              >
                {samplePrompt.label}
              </Button>
            ))}
          </div>
        )}

        <ChatInput
          input={input}
          isLoading={isChatLoading}
          isOpen={isOpen}
          onInputChange={onInputChange}
          onSubmit={onSubmit}
          onStop={onStop || EMPTY_FUNCTION}
          onSlashCommand={onSlashCommand}
        />
      </div>
    </div>
  );
};

```

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

```json
[
  {
    "children": [
      {
        "children": [],
        "errors": [],
        "occurrences": [],
        "event_id": "aa8e7f3384ef4ff5850ba966b29ed10d",
        "transaction_id": "aa8e7f3384ef4ff5850ba966b29ed10d",
        "project_id": 4509109107622913,
        "project_slug": "mcp-server",
        "profile_id": "",
        "profiler_id": "",
        "parent_span_id": "a4d1aae7216b47ff",
        "start_timestamp": 1713805458.405616,
        "end_timestamp": 1713805463.608875,
        "measurements": {},
        "description": "POST https://api.openai.com/v1/chat/completions",
        "duration": 1708,
        "trace": "b4d1aae7216b47ff8117cf4e09ce9d0b",
        "span_id": "ad0f7c48fb294de3",
        "organization": null,
        "op": "http.client",
        "hash": "4ed30c7c-4fae-4c79-b2f1-be95c24e7b04",
        "exclusive_time": 1708,
        "status": null,
        "is_segment": true,
        "sdk_name": "sentry.javascript.bun",
        "same_process_as_parent": true,
        "tags": {
          "http.method": "POST",
          "http.status_code": "200",
          "server_name": "mcp-server"
        },
        "timestamp": 1713805463.608875,
        "data": {}
      }
    ],
    "errors": [],
    "occurrences": [],
    "event_id": "aa8e7f3384ef4ff5850ba966b29ed10d",
    "transaction_id": "aa8e7f3384ef4ff5850ba966b29ed10d",
    "project_id": 4509109107622913,
    "project_slug": "mcp-server",
    "profile_id": "",
    "profiler_id": "",
    "parent_span_id": null,
    "start_timestamp": 1713805458.405616,
    "end_timestamp": 1713805463.608875,
    "measurements": {},
    "description": "tools/call search_events",
    "duration": 5203,
    "trace": "b4d1aae7216b47ff8117cf4e09ce9d0b",
    "span_id": "aa8e7f3384ef4ff5",
    "organization": null,
    "op": "function",
    "hash": "4ed30c7c-4fae-4c79-b2f1-be95c24e7b04",
    "exclusive_time": 3495,
    "status": null,
    "is_segment": true,
    "sdk_name": "sentry.javascript.bun",
    "same_process_as_parent": true,
    "tags": {
      "ai.input_messages": "1",
      "ai.model_id": "gpt-4o-2024-08-06",
      "ai.pipeline.name": "search_events",
      "ai.response.finish_reason": "stop",
      "ai.streaming": "false",
      "ai.total_tokens.used": "435",
      "server_name": "mcp-server"
    },
    "timestamp": 1713805463.608875,
    "data": {}
  },
  {
    "id": 6507376925,
    "issue_id": 6507376925,
    "project_id": 4509109107622913,
    "project_slug": "mcp-server",
    "title": "Error: Standalone issue not associated with spans",
    "culprit": "standalone-error.js:42",
    "type": "error",
    "timestamp": 1713805460.123456
  },
  {
    "children": [
      {
        "children": [],
        "errors": [],
        "occurrences": [],
        "event_id": "b4abfe5ed7984c2b",
        "transaction_id": "b4abfe5ed7984c2b",
        "project_id": 4509109107622913,
        "project_slug": "mcp-server",
        "profile_id": "",
        "profiler_id": "",
        "parent_span_id": "b4abfe5ed7984c2b",
        "start_timestamp": 1713805461.126859,
        "end_timestamp": 1713805462.534782,
        "measurements": {},
        "description": "/api/0/organizations/{organization_id_or_slug}/events/",
        "duration": 1408,
        "trace": "b4d1aae7216b47ff8117cf4e09ce9d0b",
        "span_id": "99a97a1d42c3489a",
        "organization": null,
        "op": "http.server",
        "hash": "another-hash-here",
        "exclusive_time": 1408,
        "status": "ok",
        "is_segment": true,
        "sdk_name": "sentry.python",
        "same_process_as_parent": false,
        "tags": {
          "http.method": "GET",
          "http.status_code": "200"
        },
        "timestamp": 1713805462.534782,
        "data": {}
      }
    ],
    "errors": [],
    "occurrences": [],
    "event_id": "b4abfe5ed7984c2b",
    "transaction_id": "b4abfe5ed7984c2b",
    "project_id": 4509109107622913,
    "project_slug": "mcp-server",
    "profile_id": "",
    "profiler_id": "",
    "parent_span_id": "aa8e7f3384ef4ff5",
    "start_timestamp": 1713805461.126859,
    "end_timestamp": 1713805462.608782,
    "measurements": {},
    "description": "GET https://us.sentry.io/api/0/organizations/example-org/events/",
    "duration": 1482,
    "trace": "b4d1aae7216b47ff8117cf4e09ce9d0b",
    "span_id": "b4abfe5ed7984c2b",
    "organization": null,
    "op": "http.client",
    "hash": "yet-another-hash",
    "exclusive_time": 74,
    "status": "ok",
    "is_segment": true,
    "sdk_name": "sentry.javascript.bun",
    "same_process_as_parent": true,
    "tags": {
      "http.method": "GET",
      "http.status_code": "200",
      "server_name": "mcp-server"
    },
    "timestamp": 1713805462.608782,
    "data": {}
  },
  {
    "id": 6507376926,
    "issue_id": 6507376926,
    "project_id": 4509109107622913,
    "project_slug": "mcp-server",
    "title": "TypeError: Cannot read property 'span' of undefined",
    "culprit": "trace-processor.js:156",
    "type": "error",
    "timestamp": 1713805462.234567
  }
]

```

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

```json
{
  "namespace": "cicd",
  "description": "This group describes attributes specific to pipelines within a Continuous Integration and Continuous Deployment (CI/CD) system. A [pipeline](https://wikipedia.org/wiki/Pipeline_(computing)) in this case is a series of steps that are performed in order to deliver a new version of software. This aligns with the [Britannica](https://www.britannica.com/dictionary/pipeline) definition of a pipeline where a **pipeline** is the system for developing and producing something. In the context of CI/CD, a pipeline produces or delivers software.\n",
  "attributes": {
    "cicd.pipeline.name": {
      "description": "The human readable name of the pipeline within a CI/CD system.\n",
      "type": "string",
      "stability": "development",
      "examples": [
        "Build and Test",
        "Lint",
        "Deploy Go Project",
        "deploy_to_environment"
      ]
    },
    "cicd.pipeline.run.id": {
      "description": "The unique identifier of a pipeline run within a CI/CD system.\n",
      "type": "string",
      "stability": "development",
      "examples": ["120912"]
    },
    "cicd.pipeline.run.url.full": {
      "description": "The [URL](https://wikipedia.org/wiki/URL) of the pipeline run, providing the complete address in order to locate and identify the pipeline run.\n",
      "type": "string",
      "stability": "development",
      "examples": [
        "https://github.com/open-telemetry/semantic-conventions/actions/runs/9753949763?pr=1075"
      ]
    },
    "cicd.pipeline.run.state": {
      "description": "The pipeline run goes through these states during its lifecycle.\n",
      "type": "string",
      "stability": "development",
      "examples": ["pending", "executing", "finalizing"]
    },
    "cicd.pipeline.task.name": {
      "description": "The human readable name of a task within a pipeline. Task here most closely aligns with a [computing process](https://wikipedia.org/wiki/Pipeline_(computing)) in a pipeline. Other terms for tasks include commands, steps, and procedures.\n",
      "type": "string",
      "stability": "development",
      "examples": ["Run GoLang Linter", "Go Build", "go-test", "deploy_binary"]
    },
    "cicd.pipeline.task.run.id": {
      "description": "The unique identifier of a task run within a pipeline.\n",
      "type": "string",
      "stability": "development",
      "examples": ["12097"]
    },
    "cicd.pipeline.task.run.url.full": {
      "description": "The [URL](https://wikipedia.org/wiki/URL) of the pipeline task run, providing the complete address in order to locate and identify the pipeline task run.\n",
      "type": "string",
      "stability": "development",
      "examples": [
        "https://github.com/open-telemetry/semantic-conventions/actions/runs/9753949763/job/26920038674?pr=1075"
      ]
    },
    "cicd.pipeline.task.run.result": {
      "description": "The result of a task run.\n",
      "type": "string",
      "stability": "development",
      "examples": [
        "success",
        "failure",
        "error",
        "timeout",
        "cancellation",
        "skip"
      ]
    },
    "cicd.pipeline.task.type": {
      "description": "The type of the task within a pipeline.\n",
      "type": "string",
      "stability": "development",
      "examples": ["build", "test", "deploy"]
    },
    "cicd.pipeline.result": {
      "description": "The result of a pipeline run.\n",
      "type": "string",
      "stability": "development",
      "examples": [
        "success",
        "failure",
        "error",
        "timeout",
        "cancellation",
        "skip"
      ]
    },
    "cicd.pipeline.action.name": {
      "description": "The kind of action a pipeline run is performing.\n",
      "type": "string",
      "stability": "development",
      "examples": ["BUILD", "RUN", "SYNC"]
    },
    "cicd.worker.id": {
      "description": "The unique identifier of a worker within a CICD system.",
      "type": "string",
      "stability": "development",
      "examples": ["abc123", "10.0.1.2", "controller"]
    },
    "cicd.worker.name": {
      "description": "The name of a worker within a CICD system.",
      "type": "string",
      "stability": "development",
      "examples": ["agent-abc", "controller", "Ubuntu LTS"]
    },
    "cicd.worker.url.full": {
      "description": "The [URL](https://wikipedia.org/wiki/URL) of the worker, providing the complete address in order to locate and identify the worker.",
      "type": "string",
      "stability": "development",
      "examples": ["https://cicd.example.org/worker/abc123"]
    },
    "cicd.worker.state": {
      "description": "The state of a CICD worker / agent.\n",
      "type": "string",
      "stability": "development",
      "examples": ["available", "busy", "offline"]
    },
    "cicd.system.component": {
      "description": "The name of a component of the CICD system.",
      "type": "string",
      "stability": "development",
      "examples": ["controller", "scheduler", "agent"]
    }
  }
}

```

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

```typescript
import { promises as fs } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";

export interface OAuthClientConfig {
  clientId: string;
  mcpHost: string;
  registeredAt: string;
  accessToken?: string;
  tokenExpiresAt?: string;
}

export interface ClientConfigFile {
  oauthClients: Record<string, OAuthClientConfig>;
}

export class ConfigManager {
  private configDir: string;
  private configFile: string;

  constructor() {
    this.configDir = join(homedir(), ".config", "sentry-mcp");
    this.configFile = join(this.configDir, "config.json");
  }

  /**
   * Ensure config directory exists
   */
  private async ensureConfigDir(): Promise<void> {
    try {
      await fs.mkdir(this.configDir, { recursive: true });
    } catch (error) {
      // Directory might already exist, ignore EEXIST errors
      if ((error as any).code !== "EEXIST") {
        throw error;
      }
    }
  }

  /**
   * Load config file
   */
  private async loadConfig(): Promise<ClientConfigFile> {
    try {
      const content = await fs.readFile(this.configFile, "utf-8");
      return JSON.parse(content);
    } catch (error) {
      // Config file doesn't exist or is invalid, return empty config
      return { oauthClients: {} };
    }
  }

  /**
   * Save config file
   */
  private async saveConfig(config: ClientConfigFile): Promise<void> {
    await this.ensureConfigDir();
    await fs.writeFile(
      this.configFile,
      JSON.stringify(config, null, 2),
      "utf-8",
    );
  }

  /**
   * Get OAuth client ID for a specific MCP host
   */
  async getOAuthClientId(mcpHost: string): Promise<string | null> {
    const config = await this.loadConfig();
    const clientConfig = config.oauthClients[mcpHost];
    return clientConfig?.clientId || null;
  }

  /**
   * Store OAuth client ID for a specific MCP host
   */
  async setOAuthClientId(mcpHost: string, clientId: string): Promise<void> {
    const config = await this.loadConfig();

    // Preserve existing access token if present
    const existing = config.oauthClients[mcpHost];
    config.oauthClients[mcpHost] = {
      clientId,
      mcpHost,
      registeredAt: new Date().toISOString(),
      accessToken: existing?.accessToken,
      tokenExpiresAt: existing?.tokenExpiresAt,
    };

    await this.saveConfig(config);
  }

  /**
   * Remove OAuth client configuration for a specific MCP host
   */
  async removeOAuthClientId(mcpHost: string): Promise<void> {
    const config = await this.loadConfig();
    delete config.oauthClients[mcpHost];
    await this.saveConfig(config);
  }

  /**
   * Get cached access token for a specific MCP host
   */
  async getAccessToken(mcpHost: string): Promise<string | null> {
    const config = await this.loadConfig();
    const clientConfig = config.oauthClients[mcpHost];

    if (!clientConfig?.accessToken) {
      return null;
    }

    // Check if token is expired
    if (clientConfig.tokenExpiresAt) {
      const expiresAt = new Date(clientConfig.tokenExpiresAt);
      const now = new Date();
      // Add 5 minute buffer before expiration
      const bufferTime = 5 * 60 * 1000;

      if (now.getTime() + bufferTime >= expiresAt.getTime()) {
        // Token is expired or will expire soon
        await this.removeAccessToken(mcpHost);
        return null;
      }
    }

    return clientConfig.accessToken;
  }

  /**
   * Store access token for a specific MCP host
   */
  async setAccessToken(
    mcpHost: string,
    accessToken: string,
    expiresIn?: number,
  ): Promise<void> {
    const config = await this.loadConfig();

    const existing = config.oauthClients[mcpHost];
    if (!existing) {
      throw new Error(`No OAuth client configuration found for ${mcpHost}`);
    }

    let tokenExpiresAt: string | undefined;
    if (expiresIn) {
      // expiresIn is in seconds, convert to milliseconds
      const expiresAtMs = Date.now() + expiresIn * 1000;
      tokenExpiresAt = new Date(expiresAtMs).toISOString();
    }

    config.oauthClients[mcpHost] = {
      ...existing,
      accessToken,
      tokenExpiresAt,
    };

    await this.saveConfig(config);
  }

  /**
   * Remove cached access token for a specific MCP host
   */
  async removeAccessToken(mcpHost: string): Promise<void> {
    const config = await this.loadConfig();
    const existing = config.oauthClients[mcpHost];

    if (existing) {
      config.oauthClients[mcpHost] = {
        ...existing,
        accessToken: undefined,
        tokenExpiresAt: undefined,
      };
      await this.saveConfig(config);
    }
  }

  /**
   * Clear all cached tokens (useful for logout)
   */
  async clearAllTokens(): Promise<void> {
    const config = await this.loadConfig();

    for (const [host, clientConfig] of Object.entries(config.oauthClients)) {
      config.oauthClients[host] = {
        ...clientConfig,
        accessToken: undefined,
        tokenExpiresAt: undefined,
      };
    }

    await this.saveConfig(config);
  }

  /**
   * List all registered OAuth clients
   */
  async listOAuthClients(): Promise<OAuthClientConfig[]> {
    const config = await this.loadConfig();
    return Object.values(config.oauthClients);
  }
}

```

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

```json
{
  "namespace": "network",
  "description": "These attributes may be used for any network related operation.\n",
  "attributes": {
    "network.carrier.icc": {
      "description": "The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network.",
      "type": "string",
      "stability": "development",
      "examples": ["DE"]
    },
    "network.carrier.mcc": {
      "description": "The mobile carrier country code.",
      "type": "string",
      "stability": "development",
      "examples": ["310"]
    },
    "network.carrier.mnc": {
      "description": "The mobile carrier network code.",
      "type": "string",
      "stability": "development",
      "examples": ["001"]
    },
    "network.carrier.name": {
      "description": "The name of the mobile carrier.",
      "type": "string",
      "stability": "development",
      "examples": ["sprint"]
    },
    "network.connection.subtype": {
      "description": "This describes more details regarding the connection.type. It may be the type of cell technology connection, but it could be used for describing details about a wifi connection.",
      "type": "string",
      "stability": "development",
      "examples": [
        "gprs",
        "edge",
        "umts",
        "cdma",
        "evdo_0",
        "evdo_a",
        "cdma2000_1xrtt",
        "hsdpa",
        "hsupa",
        "hspa",
        "iden",
        "evdo_b",
        "lte",
        "ehrpd",
        "hspap",
        "gsm",
        "td_scdma",
        "iwlan",
        "nr",
        "nrnsa",
        "lte_ca"
      ]
    },
    "network.connection.type": {
      "description": "The internet connection type.",
      "type": "string",
      "stability": "development",
      "examples": ["wifi", "wired", "cell", "unavailable", "unknown"]
    },
    "network.local.address": {
      "description": "Local address of the network connection - IP address or Unix domain socket name.",
      "type": "string",
      "stability": "stable",
      "examples": ["10.1.2.80", "/tmp/my.sock"]
    },
    "network.local.port": {
      "description": "Local port number of the network connection.",
      "type": "number",
      "stability": "stable",
      "examples": ["65123"]
    },
    "network.peer.address": {
      "description": "Peer address of the network connection - IP address or Unix domain socket name.",
      "type": "string",
      "stability": "stable",
      "examples": ["10.1.2.80", "/tmp/my.sock"]
    },
    "network.peer.port": {
      "description": "Peer port number of the network connection.",
      "type": "number",
      "stability": "stable",
      "examples": ["65123"]
    },
    "network.protocol.name": {
      "description": "[OSI application layer](https://wikipedia.org/wiki/Application_layer) or non-OSI equivalent.",
      "type": "string",
      "note": "The value SHOULD be normalized to lowercase.",
      "stability": "stable",
      "examples": ["amqp", "http", "mqtt"]
    },
    "network.protocol.version": {
      "description": "The actual version of the protocol used for network communication.",
      "type": "string",
      "note": "If protocol version is subject to negotiation (for example using [ALPN](https://www.rfc-editor.org/rfc/rfc7301.html)), this attribute SHOULD be set to the negotiated version. If the actual protocol version is not known, this attribute SHOULD NOT be set.\n",
      "stability": "stable",
      "examples": ["1.1", "2"]
    },
    "network.transport": {
      "description": "[OSI transport layer](https://wikipedia.org/wiki/Transport_layer) or [inter-process communication method](https://wikipedia.org/wiki/Inter-process_communication).\n",
      "type": "string",
      "note": "The value SHOULD be normalized to lowercase.\n\nConsider always setting the transport when setting a port number, since\na port number is ambiguous without knowing the transport. For example\ndifferent processes could be listening on TCP port 12345 and UDP port 12345.\n",
      "stability": "stable",
      "examples": ["tcp", "udp", "pipe", "unix", "quic"]
    },
    "network.type": {
      "description": "[OSI network layer](https://wikipedia.org/wiki/Network_layer) or non-OSI equivalent.",
      "type": "string",
      "note": "The value SHOULD be normalized to lowercase.",
      "stability": "stable",
      "examples": ["ipv4", "ipv6"]
    },
    "network.io.direction": {
      "description": "The network IO operation direction.",
      "type": "string",
      "stability": "development",
      "examples": ["transmit", "receive"]
    },
    "network.interface.name": {
      "description": "The network interface name.",
      "type": "string",
      "stability": "development",
      "examples": ["lo", "eth0"]
    },
    "network.connection.state": {
      "description": "The state of network connection",
      "type": "string",
      "note": "Connection states are defined as part of the [rfc9293](https://datatracker.ietf.org/doc/html/rfc9293#section-3.3.2)",
      "stability": "development",
      "examples": [
        "closed",
        "close_wait",
        "closing",
        "established",
        "fin_wait_1",
        "fin_wait_2",
        "last_ack",
        "listen",
        "syn_received",
        "syn_sent",
        "time_wait"
      ]
    }
  }
}

```

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

```json
{
  "namespace": "cloud",
  "description": "A cloud environment (e.g. GCP, Azure, AWS).\n",
  "attributes": {
    "cloud.provider": {
      "description": "Name of the cloud provider.\n",
      "type": "string",
      "stability": "development",
      "examples": [
        "alibaba_cloud",
        "aws",
        "azure",
        "gcp",
        "heroku",
        "ibm_cloud",
        "oracle_cloud",
        "tencent_cloud"
      ]
    },
    "cloud.account.id": {
      "description": "The cloud account ID the resource is assigned to.\n",
      "type": "string",
      "stability": "development",
      "examples": ["111111111111", "opentelemetry"]
    },
    "cloud.region": {
      "description": "The geographical region within a cloud provider. When associated with a resource, this attribute specifies the region where the resource operates. When calling services or APIs deployed on a cloud, this attribute identifies the region where the called destination is deployed.\n",
      "type": "string",
      "note": "Refer to your provider's docs to see the available regions, for example [Alibaba Cloud regions](https://www.alibabacloud.com/help/doc-detail/40654.htm), [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/global-infrastructure/geographies/), [Google Cloud regions](https://cloud.google.com/about/locations), or [Tencent Cloud regions](https://www.tencentcloud.com/document/product/213/6091).\n",
      "stability": "development",
      "examples": ["us-central1", "us-east-1"]
    },
    "cloud.resource_id": {
      "description": "Cloud provider-specific native identifier of the monitored cloud resource (e.g. an [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) on AWS, a [fully qualified resource ID](https://learn.microsoft.com/rest/api/resources/resources/get-by-id) on Azure, a [full resource name](https://google.aip.dev/122#full-resource-names) on GCP)\n",
      "type": "string",
      "note": "On some cloud providers, it may not be possible to determine the full ID at startup,\nso it may be necessary to set `cloud.resource_id` as a span attribute instead.\n\nThe exact value to use for `cloud.resource_id` depends on the cloud provider.\nThe following well-known definitions MUST be used if you set this attribute and they apply:\n\n- **AWS Lambda:** The function [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html).\n  Take care not to use the \"invoked ARN\" directly but replace any\n  [alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html)\n  with the resolved function version, as the same runtime instance may be invocable with\n  multiple different aliases.\n- **GCP:** The [URI of the resource](https://cloud.google.com/iam/docs/full-resource-names)\n- **Azure:** The [Fully Qualified Resource ID](https://learn.microsoft.com/rest/api/resources/resources/get-by-id) of the invoked function,\n  *not* the function app, having the form\n  `/subscriptions/<SUBSCRIPTION_GUID>/resourceGroups/<RG>/providers/Microsoft.Web/sites/<FUNCAPP>/functions/<FUNC>`.\n  This means that a span attribute MUST be used, as an Azure function app can host multiple functions that would usually share\n  a TracerProvider.\n",
      "stability": "development",
      "examples": [
        "arn:aws:lambda:REGION:ACCOUNT_ID:function:my-function",
        "//run.googleapis.com/projects/PROJECT_ID/locations/LOCATION_ID/services/SERVICE_ID",
        "/subscriptions/<SUBSCRIPTION_GUID>/resourceGroups/<RG>/providers/Microsoft.Web/sites/<FUNCAPP>/functions/<FUNC>"
      ]
    },
    "cloud.availability_zone": {
      "description": "Cloud regions often have multiple, isolated locations known as zones to increase availability. Availability zone represents the zone where the resource is running.\n",
      "type": "string",
      "note": "Availability zones are called \"zones\" on Alibaba Cloud and Google Cloud.\n",
      "stability": "development",
      "examples": ["us-east-1c"]
    },
    "cloud.platform": {
      "description": "The cloud platform in use.\n",
      "type": "string",
      "note": "The prefix of the service SHOULD match the one specified in `cloud.provider`.\n",
      "stability": "development",
      "examples": [
        "alibaba_cloud_ecs",
        "alibaba_cloud_fc",
        "alibaba_cloud_openshift",
        "aws_ec2",
        "aws_ecs",
        "aws_eks",
        "aws_lambda",
        "aws_elastic_beanstalk",
        "aws_app_runner",
        "aws_openshift",
        "azure.vm",
        "azure.container_apps",
        "azure.container_instances",
        "azure.aks",
        "azure.functions",
        "azure.app_service",
        "azure.openshift",
        "azure_vm",
        "azure_container_apps",
        "azure_container_instances",
        "azure_aks",
        "azure_functions",
        "azure_app_service",
        "azure_openshift",
        "gcp_bare_metal_solution",
        "gcp_compute_engine",
        "gcp_cloud_run",
        "gcp_kubernetes_engine",
        "gcp_cloud_functions",
        "gcp_app_engine",
        "gcp_openshift",
        "ibm_cloud_openshift",
        "oracle_cloud_compute",
        "oracle_cloud_oke",
        "tencent_cloud_cvm",
        "tencent_cloud_eks",
        "tencent_cloud_scf"
      ]
    }
  }
}

```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/update-issue.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from "vitest";
import updateIssue from "./update-issue.js";

describe("update_issue", () => {
  it("updates issue status", async () => {
    const result = await updateIssue.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: "CLOUDFLARE-MCP-41",
        status: "resolved",
        assignedTo: undefined,
        issueUrl: undefined,
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );
    expect(result).toMatchInlineSnapshot(`
      "# Issue CLOUDFLARE-MCP-41 Updated in **sentry-mcp-evals**

      **Issue**: Error: Tool list_organizations is already registered
      **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41

      ## Changes Made

      **Status**: unresolved → **resolved**

      ## Current Status

      **Status**: resolved
      **Assigned To**: Unassigned

      # Using this information

      - The issue has been successfully updated in Sentry
      - You can view the issue details using: \`get_issue_details(organizationSlug="sentry-mcp-evals", issueId="CLOUDFLARE-MCP-41")\`
      - The issue is now marked as resolved and will no longer generate alerts
      "
    `);
  });

  it("updates issue assignment", async () => {
    const result = await updateIssue.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: "CLOUDFLARE-MCP-41",
        status: undefined,
        assignedTo: "john.doe",
        issueUrl: undefined,
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );
    expect(result).toMatchInlineSnapshot(`
      "# Issue CLOUDFLARE-MCP-41 Updated in **sentry-mcp-evals**

      **Issue**: Error: Tool list_organizations is already registered
      **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41

      ## Changes Made

      **Assigned To**: Unassigned → **john.doe**

      ## Current Status

      **Status**: unresolved
      **Assigned To**: john.doe

      # Using this information

      - The issue has been successfully updated in Sentry
      - You can view the issue details using: \`get_issue_details(organizationSlug="sentry-mcp-evals", issueId="CLOUDFLARE-MCP-41")\`
      "
    `);
  });

  it("updates both status and assignment", async () => {
    const result = await updateIssue.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: "CLOUDFLARE-MCP-41",
        status: "resolved",
        assignedTo: "me",
        issueUrl: undefined,
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );
    expect(result).toMatchInlineSnapshot(`
      "# Issue CLOUDFLARE-MCP-41 Updated in **sentry-mcp-evals**

      **Issue**: Error: Tool list_organizations is already registered
      **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41

      ## Changes Made

      **Status**: unresolved → **resolved**
      **Assigned To**: Unassigned → **You**

      ## Current Status

      **Status**: resolved
      **Assigned To**: me

      # Using this information

      - The issue has been successfully updated in Sentry
      - You can view the issue details using: \`get_issue_details(organizationSlug="sentry-mcp-evals", issueId="CLOUDFLARE-MCP-41")\`
      - The issue is now marked as resolved and will no longer generate alerts
      "
    `);
  });

  it("validates required parameters", async () => {
    await expect(
      updateIssue.handler(
        {
          organizationSlug: undefined,
          issueId: undefined,
          status: undefined,
          assignedTo: undefined,
          issueUrl: undefined,
          regionUrl: undefined,
        },
        {
          constraints: {
            organizationSlug: null,
          },
          accessToken: "access-token",
          userId: "1",
        },
      ),
    ).rejects.toThrow("Either `issueId` or `issueUrl` must be provided");
  });

  it("validates organization slug when using issueId", async () => {
    await expect(
      updateIssue.handler(
        {
          organizationSlug: undefined,
          issueId: "CLOUDFLARE-MCP-41",
          status: "resolved",
          assignedTo: undefined,
          issueUrl: undefined,
          regionUrl: undefined,
        },
        {
          constraints: {
            organizationSlug: null,
          },
          accessToken: "access-token",
          userId: "1",
        },
      ),
    ).rejects.toThrow(
      "`organizationSlug` is required when providing `issueId`",
    );
  });

  it("validates update parameters", async () => {
    await expect(
      updateIssue.handler(
        {
          organizationSlug: "sentry-mcp-evals",
          issueId: "CLOUDFLARE-MCP-41",
          status: undefined,
          assignedTo: undefined,
          issueUrl: undefined,
          regionUrl: undefined,
        },
        {
          constraints: {
            organizationSlug: null,
          },
          accessToken: "access-token",
          userId: "1",
        },
      ),
    ).rejects.toThrow(
      "At least one of `status` or `assignedTo` must be provided to update the issue",
    );
  });
});

```

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

```json
{
  "namespace": "container",
  "description": "A container instance.\n",
  "attributes": {
    "container.name": {
      "description": "Container name used by container runtime.\n",
      "type": "string",
      "stability": "development",
      "examples": ["opentelemetry-autoconf"]
    },
    "container.id": {
      "description": "Container ID. Usually a UUID, as for example used to [identify Docker containers](https://docs.docker.com/engine/containers/run/#container-identification). The UUID might be abbreviated.\n",
      "type": "string",
      "stability": "development",
      "examples": ["a3bf90e006b2"]
    },
    "container.runtime": {
      "description": "The container runtime managing this container.\n",
      "type": "string",
      "stability": "development",
      "examples": ["docker", "containerd", "rkt"]
    },
    "container.image.name": {
      "description": "Name of the image the container was built on.\n",
      "type": "string",
      "stability": "development",
      "examples": ["gcr.io/opentelemetry/operator"]
    },
    "container.image.tags": {
      "description": "Container image tags. An example can be found in [Docker Image Inspect](https://docs.docker.com/engine/api/v1.43/#tag/Image/operation/ImageInspect). Should be only the `<tag>` section of the full name for example from `registry.example.com/my-org/my-image:<tag>`.\n",
      "type": "string",
      "stability": "development",
      "examples": ["[\"v1.27.1\",\"3.5.7-0\"]"]
    },
    "container.image.id": {
      "description": "Runtime specific image identifier. Usually a hash algorithm followed by a UUID.\n",
      "type": "string",
      "note": "Docker defines a sha256 of the image id; `container.image.id` corresponds to the `Image` field from the Docker container inspect [API](https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerInspect) endpoint.\nK8s defines a link to the container registry repository with digest `\"imageID\": \"registry.azurecr.io /namespace/service/dockerfile@sha256:bdeabd40c3a8a492eaf9e8e44d0ebbb84bac7ee25ac0cf8a7159d25f62555625\"`.\nThe ID is assigned by the container runtime and can vary in different environments. Consider using `oci.manifest.digest` if it is important to identify the same image in different environments/runtimes.\n",
      "stability": "development",
      "examples": [
        "sha256:19c92d0a00d1b66d897bceaa7319bee0dd38a10a851c60bcec9474aa3f01e50f"
      ]
    },
    "container.image.repo_digests": {
      "description": "Repo digests of the container image as provided by the container runtime.\n",
      "type": "string",
      "note": "[Docker](https://docs.docker.com/engine/api/v1.43/#tag/Image/operation/ImageInspect) and [CRI](https://github.com/kubernetes/cri-api/blob/c75ef5b473bbe2d0a4fc92f82235efd665ea8e9f/pkg/apis/runtime/v1/api.proto#L1237-L1238) report those under the `RepoDigests` field.\n",
      "stability": "development",
      "examples": [
        "[\"example@sha256:afcc7f1ac1b49db317a7196c902e61c6c3c4607d63599ee1a82d702d249a0ccb\",\"internal.registry.example.com:5000/example@sha256:b69959407d21e8a062e0416bf13405bb2b71ed7a84dde4158ebafacfa06f5578\"]"
      ]
    },
    "container.command": {
      "description": "The command used to run the container (i.e. the command name).\n",
      "type": "string",
      "note": "If using embedded credentials or sensitive data, it is recommended to remove them to prevent potential leakage.\n",
      "stability": "development",
      "examples": ["otelcontribcol"]
    },
    "container.command_line": {
      "description": "The full command run by the container as a single string representing the full command.\n",
      "type": "string",
      "stability": "development",
      "examples": ["otelcontribcol --config config.yaml"]
    },
    "container.command_args": {
      "description": "All the command arguments (including the command/executable itself) run by the container.\n",
      "type": "string",
      "stability": "development",
      "examples": ["[\"otelcontribcol\",\"--config\",\"config.yaml\"]"]
    },
    "container.label": {
      "description": "Container labels, `<key>` being the label name, the value being the label value.\n",
      "type": "string",
      "note": "For example, a docker container label `app` with value `nginx` SHOULD be recorded as the `container.label.app` attribute with value `\"nginx\"`.\n",
      "stability": "development",
      "examples": ["nginx"]
    },
    "container.csi.plugin.name": {
      "description": "The name of the CSI ([Container Storage Interface](https://github.com/container-storage-interface/spec)) plugin used by the volume.\n",
      "type": "string",
      "note": "This can sometimes be referred to as a \"driver\" in CSI implementations. This should represent the `name` field of the GetPluginInfo RPC.\n",
      "stability": "development",
      "examples": ["pd.csi.storage.gke.io"]
    },
    "container.csi.volume.id": {
      "description": "The unique volume ID returned by the CSI ([Container Storage Interface](https://github.com/container-storage-interface/spec)) plugin.\n",
      "type": "string",
      "note": "This can sometimes be referred to as a \"volume handle\" in CSI implementations. This should represent the `Volume.volume_id` field in CSI spec.\n",
      "stability": "development",
      "examples": [
        "projects/my-gcp-project/zones/my-gcp-zone/disks/my-gcp-disk"
      ]
    }
  }
}

```

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

```json
{
  "namespace": "file",
  "description": "Describes file attributes.",
  "attributes": {
    "file.accessed": {
      "description": "Time when the file was last accessed, in ISO 8601 format.\n",
      "type": "string",
      "note": "This attribute might not be supported by some file systems — NFS, FAT32, in embedded OS, etc.\n",
      "stability": "development",
      "examples": ["2021-01-01T12:00:00Z"]
    },
    "file.attributes": {
      "description": "Array of file attributes.\n",
      "type": "string",
      "note": "Attributes names depend on the OS or file system. Here’s a non-exhaustive list of values expected for this attribute: `archive`, `compressed`, `directory`, `encrypted`, `execute`, `hidden`, `immutable`, `journaled`, `read`, `readonly`, `symbolic link`, `system`, `temporary`, `write`.\n",
      "stability": "development",
      "examples": ["[\"readonly\",\"hidden\"]"]
    },
    "file.created": {
      "description": "Time when the file was created, in ISO 8601 format.\n",
      "type": "string",
      "note": "This attribute might not be supported by some file systems — NFS, FAT32, in embedded OS, etc.\n",
      "stability": "development",
      "examples": ["2021-01-01T12:00:00Z"]
    },
    "file.changed": {
      "description": "Time when the file attributes or metadata was last changed, in ISO 8601 format.\n",
      "type": "string",
      "note": "`file.changed` captures the time when any of the file's properties or attributes (including the content) are changed, while `file.modified` captures the timestamp when the file content is modified.\n",
      "stability": "development",
      "examples": ["2021-01-01T12:00:00Z"]
    },
    "file.directory": {
      "description": "Directory where the file is located. It should include the drive letter, when appropriate.\n",
      "type": "string",
      "stability": "development",
      "examples": ["/home/user", "C:\\Program Files\\MyApp"]
    },
    "file.extension": {
      "description": "File extension, excluding the leading dot.\n",
      "type": "string",
      "note": "When the file name has multiple extensions (example.tar.gz), only the last one should be captured (\"gz\", not \"tar.gz\").\n",
      "stability": "development",
      "examples": ["png", "gz"]
    },
    "file.fork_name": {
      "description": "Name of the fork. A fork is additional data associated with a filesystem object.\n",
      "type": "string",
      "note": "On Linux, a resource fork is used to store additional data with a filesystem object. A file always has at least one fork for the data portion, and additional forks may exist.\nOn NTFS, this is analogous to an Alternate Data Stream (ADS), and the default data stream for a file is just called $DATA. Zone.Identifier is commonly used by Windows to track contents downloaded from the Internet. An ADS is typically of the form: C:\\path\\to\\filename.extension:some_fork_name, and some_fork_name is the value that should populate `fork_name`. `filename.extension` should populate `file.name`, and `extension` should populate `file.extension`. The full path, `file.path`, will include the fork name.\n",
      "stability": "development",
      "examples": ["Zone.Identifer"]
    },
    "file.group.id": {
      "description": "Primary Group ID (GID) of the file.\n",
      "type": "string",
      "stability": "development",
      "examples": ["1000"]
    },
    "file.group.name": {
      "description": "Primary group name of the file.\n",
      "type": "string",
      "stability": "development",
      "examples": ["users"]
    },
    "file.inode": {
      "description": "Inode representing the file in the filesystem.\n",
      "type": "string",
      "stability": "development",
      "examples": ["256383"]
    },
    "file.mode": {
      "description": "Mode of the file in octal representation.\n",
      "type": "string",
      "stability": "development",
      "examples": ["0640"]
    },
    "file.modified": {
      "description": "Time when the file content was last modified, in ISO 8601 format.\n",
      "type": "string",
      "stability": "development",
      "examples": ["2021-01-01T12:00:00Z"]
    },
    "file.name": {
      "description": "Name of the file including the extension, without the directory.\n",
      "type": "string",
      "stability": "development",
      "examples": ["example.png"]
    },
    "file.owner.id": {
      "description": "The user ID (UID) or security identifier (SID) of the file owner.\n",
      "type": "string",
      "stability": "development",
      "examples": ["1000"]
    },
    "file.owner.name": {
      "description": "Username of the file owner.\n",
      "type": "string",
      "stability": "development",
      "examples": ["root"]
    },
    "file.path": {
      "description": "Full path to the file, including the file name. It should include the drive letter, when appropriate.\n",
      "type": "string",
      "stability": "development",
      "examples": [
        "/home/alice/example.png",
        "C:\\Program Files\\MyApp\\myapp.exe"
      ]
    },
    "file.size": {
      "description": "File size in bytes.\n",
      "type": "number",
      "stability": "development"
    },
    "file.symbolic_link.target_path": {
      "description": "Path to the target of a symbolic link.\n",
      "type": "string",
      "note": "This attribute is only applicable to symbolic links.\n",
      "stability": "development",
      "examples": ["/usr/bin/python3"]
    }
  }
}

```

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

```json
{
  "namespace": "cloudfoundry",
  "description": "CloudFoundry resource attributes.\n",
  "attributes": {
    "cloudfoundry.system.id": {
      "description": "A guid or another name describing the event source.\n",
      "type": "string",
      "note": "CloudFoundry defines the `source_id` in the [Loggregator v2 envelope](https://github.com/cloudfoundry/loggregator-api#v2-envelope).\nIt is used for logs and metrics emitted by CloudFoundry. It is\nsupposed to contain the component name, e.g. \"gorouter\", for\nCloudFoundry components.\n\nWhen system components are instrumented, values from the\n[Bosh spec](https://bosh.io/docs/jobs/#properties-spec)\nshould be used. The `system.id` should be set to\n`spec.deployment/spec.name`.\n",
      "stability": "development",
      "examples": ["cf/gorouter"]
    },
    "cloudfoundry.system.instance.id": {
      "description": "A guid describing the concrete instance of the event source.\n",
      "type": "string",
      "note": "CloudFoundry defines the `instance_id` in the [Loggregator v2 envelope](https://github.com/cloudfoundry/loggregator-api#v2-envelope).\nIt is used for logs and metrics emitted by CloudFoundry. It is\nsupposed to contain the vm id for CloudFoundry components.\n\nWhen system components are instrumented, values from the\n[Bosh spec](https://bosh.io/docs/jobs/#properties-spec)\nshould be used. The `system.instance.id` should be set to `spec.id`.\n",
      "stability": "development",
      "examples": ["218fc5a9-a5f1-4b54-aa05-46717d0ab26d"]
    },
    "cloudfoundry.app.name": {
      "description": "The name of the application.\n",
      "type": "string",
      "note": "Application instrumentation should use the value from environment\nvariable `VCAP_APPLICATION.application_name`. This is the same value\nas reported by `cf apps`.\n",
      "stability": "development",
      "examples": ["my-app-name"]
    },
    "cloudfoundry.app.id": {
      "description": "The guid of the application.\n",
      "type": "string",
      "note": "Application instrumentation should use the value from environment\nvariable `VCAP_APPLICATION.application_id`. This is the same value as\nreported by `cf app <app-name> --guid`.\n",
      "stability": "development",
      "examples": ["218fc5a9-a5f1-4b54-aa05-46717d0ab26d"]
    },
    "cloudfoundry.app.instance.id": {
      "description": "The index of the application instance. 0 when just one instance is active.\n",
      "type": "string",
      "note": "CloudFoundry defines the `instance_id` in the [Loggregator v2 envelope](https://github.com/cloudfoundry/loggregator-api#v2-envelope).\nIt is used for logs and metrics emitted by CloudFoundry. It is\nsupposed to contain the application instance index for applications\ndeployed on the runtime.\n\nApplication instrumentation should use the value from environment\nvariable `CF_INSTANCE_INDEX`.\n",
      "stability": "development",
      "examples": ["0", "1"]
    },
    "cloudfoundry.space.name": {
      "description": "The name of the CloudFoundry space the application is running in.\n",
      "type": "string",
      "note": "Application instrumentation should use the value from environment\nvariable `VCAP_APPLICATION.space_name`. This is the same value as\nreported by `cf spaces`.\n",
      "stability": "development",
      "examples": ["my-space-name"]
    },
    "cloudfoundry.space.id": {
      "description": "The guid of the CloudFoundry space the application is running in.\n",
      "type": "string",
      "note": "Application instrumentation should use the value from environment\nvariable `VCAP_APPLICATION.space_id`. This is the same value as\nreported by `cf space <space-name> --guid`.\n",
      "stability": "development",
      "examples": ["218fc5a9-a5f1-4b54-aa05-46717d0ab26d"]
    },
    "cloudfoundry.org.name": {
      "description": "The name of the CloudFoundry organization the app is running in.\n",
      "type": "string",
      "note": "Application instrumentation should use the value from environment\nvariable `VCAP_APPLICATION.org_name`. This is the same value as\nreported by `cf orgs`.\n",
      "stability": "development",
      "examples": ["my-org-name"]
    },
    "cloudfoundry.org.id": {
      "description": "The guid of the CloudFoundry org the application is running in.\n",
      "type": "string",
      "note": "Application instrumentation should use the value from environment\nvariable `VCAP_APPLICATION.org_id`. This is the same value as\nreported by `cf org <org-name> --guid`.\n",
      "stability": "development",
      "examples": ["218fc5a9-a5f1-4b54-aa05-46717d0ab26d"]
    },
    "cloudfoundry.process.id": {
      "description": "The UID identifying the process.\n",
      "type": "string",
      "note": "Application instrumentation should use the value from environment\nvariable `VCAP_APPLICATION.process_id`. It is supposed to be equal to\n`VCAP_APPLICATION.app_id` for applications deployed to the runtime.\nFor system components, this could be the actual PID.\n",
      "stability": "development",
      "examples": ["218fc5a9-a5f1-4b54-aa05-46717d0ab26d"]
    },
    "cloudfoundry.process.type": {
      "description": "The type of process.\n",
      "type": "string",
      "note": "CloudFoundry applications can consist of multiple jobs. Usually the\nmain process will be of type `web`. There can be additional background\ntasks or side-cars with different process types.\n",
      "stability": "development",
      "examples": ["web"]
    }
  }
}

```

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

```json
{
  "id": "a1b2c3d4e5f6789012345678901234567",
  "groupID": "7890123456",
  "eventID": "a1b2c3d4e5f6789012345678901234567",
  "projectID": "4509062593708032",
  "size": 8432,
  "type": "transaction",
  "title": "GET /api/users",
  "message": "",
  "platform": "python",
  "datetime": "2025-08-06T18:00:00.000000Z",
  "dateCreated": "2025-08-06T18:00:00.000000Z",
  "contexts": {
    "trace": {
      "trace_id": "abcdef1234567890abcdef1234567890",
      "span_id": "1234567890abcdef",
      "op": "http.server",
      "status": "ok",
      "exclusive_time": 250.5,
      "data": {
        "http.request.method": "GET",
        "url.path": "/api/users"
      }
    }
  },
  "occurrence": {
    "id": "occ_123456789",
    "projectId": 4509062593708032,
    "eventId": "a1b2c3d4e5f6789012345678901234567",
    "fingerprint": [
      "n_plus_one_db_queries",
      "SELECT * FROM users WHERE id = %s"
    ],
    "issueTitle": "N+1 Query: SELECT * FROM users WHERE id = %s",
    "subtitle": "Database query repeated 25 times",
    "resourceId": null,
    "type": 1006,
    "detectionTime": 1722963600,
    "level": "warning",
    "culprit": "SELECT * FROM users WHERE id = %s",
    "priority": 50,
    "assignee": null,
    "evidenceData": {
      "transactionName": "/api/users",
      "parentSpan": "GET /api/users",
      "parentSpanIds": ["parent123"],
      "repeatingSpansCompact": ["SELECT * FROM users WHERE id = %s"],
      "repeatingSpans": ["db - SELECT * FROM users WHERE id = %s"],
      "numberRepeatingSpans": "25",
      "numPatternRepetitions": 25,
      "offenderSpanIds": [
        "span001",
        "span002",
        "span003",
        "span004",
        "span005",
        "span006",
        "span007",
        "span008",
        "span009",
        "span010",
        "span011",
        "span012",
        "span013",
        "span014",
        "span015",
        "span016",
        "span017",
        "span018",
        "span019",
        "span020",
        "span021",
        "span022",
        "span023",
        "span024",
        "span025"
      ],
      "op": "db"
    },
    "evidenceDisplay": [
      {
        "name": "Offending Spans",
        "value": "SELECT * FROM users WHERE id = %s",
        "important": true
      },
      {
        "name": "Repeated",
        "value": "25 times",
        "important": true
      }
    ]
  },
  "entries": [
    {
      "type": "spans",
      "data": [
        {
          "span_id": "parent123",
          "trace_id": "abcdef1234567890abcdef1234567890",
          "parent_span_id": "1234567890abcdef",
          "op": "http.server",
          "description": "GET /api/users",
          "status": "ok",
          "start_timestamp": 1722963600.0,
          "timestamp": 1722963600.25,
          "data": {
            "http.request.method": "GET",
            "url.path": "/api/users"
          }
        },
        {
          "span_id": "span001",
          "trace_id": "abcdef1234567890abcdef1234567890",
          "parent_span_id": "parent123",
          "op": "db.query",
          "description": "SELECT * FROM users WHERE id = 1",
          "status": "ok",
          "start_timestamp": 1722963600.01,
          "timestamp": 1722963600.015,
          "data": {
            "db.system": "postgresql",
            "db.operation": "SELECT"
          }
        },
        {
          "span_id": "span002",
          "trace_id": "abcdef1234567890abcdef1234567890",
          "parent_span_id": "parent123",
          "op": "db.query",
          "description": "SELECT * FROM users WHERE id = 2",
          "status": "ok",
          "start_timestamp": 1722963600.02,
          "timestamp": 1722963600.025,
          "data": {
            "db.system": "postgresql",
            "db.operation": "SELECT"
          }
        },
        {
          "span_id": "span003",
          "trace_id": "abcdef1234567890abcdef1234567890",
          "parent_span_id": "parent123",
          "op": "db.query",
          "description": "SELECT * FROM users WHERE id = 3",
          "status": "ok",
          "start_timestamp": 1722963600.03,
          "timestamp": 1722963600.035,
          "data": {
            "db.system": "postgresql",
            "db.operation": "SELECT"
          }
        },
        {
          "span_id": "span004",
          "trace_id": "abcdef1234567890abcdef1234567890",
          "parent_span_id": "parent123",
          "op": "db.query",
          "description": "SELECT * FROM users WHERE id = 4",
          "status": "ok",
          "start_timestamp": 1722963600.04,
          "timestamp": 1722963600.045,
          "data": {
            "db.system": "postgresql",
            "db.operation": "SELECT"
          }
        },
        {
          "span_id": "span005",
          "trace_id": "abcdef1234567890abcdef1234567890",
          "parent_span_id": "parent123",
          "op": "db.query",
          "description": "SELECT * FROM users WHERE id = 5",
          "status": "ok",
          "start_timestamp": 1722963600.05,
          "timestamp": 1722963600.055,
          "data": {
            "db.system": "postgresql",
            "db.operation": "SELECT"
          }
        }
      ]
    },
    {
      "type": "request",
      "data": {
        "method": "GET",
        "url": "https://api.example.com/api/users",
        "query": [],
        "headers": [["Accept", "application/json"], ["Host", "api.example.com"]]
      }
    }
  ],
  "tags": [
    { "key": "environment", "value": "production" },
    { "key": "transaction", "value": "/api/users" }
  ]
}

```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/client/hooks/use-persisted-chat.ts:
--------------------------------------------------------------------------------

```typescript
import { useCallback, useMemo } from "react";
import type { Message } from "ai";

const CHAT_STORAGE_KEY = "sentry_chat_messages";
const TIMESTAMP_STORAGE_KEY = "sentry_chat_timestamp";
const MAX_STORED_MESSAGES = 100; // Limit storage size
const CACHE_EXPIRY_MS = 60 * 60 * 1000; // 1 hour in milliseconds

export function usePersistedChat(isAuthenticated: boolean) {
  // Check if cache is expired
  const isCacheExpired = useCallback(() => {
    try {
      const timestampStr = localStorage.getItem(TIMESTAMP_STORAGE_KEY);
      if (!timestampStr) return true;

      const timestamp = Number.parseInt(timestampStr, 10);
      const now = Date.now();
      return now - timestamp > CACHE_EXPIRY_MS;
    } catch {
      return true;
    }
  }, []);

  // Update timestamp to extend cache expiry
  const updateTimestamp = useCallback(() => {
    try {
      localStorage.setItem(TIMESTAMP_STORAGE_KEY, Date.now().toString());
    } catch (error) {
      console.error("Failed to update chat timestamp:", error);
    }
  }, []);

  // Validate a message to ensure it won't cause conversion errors
  const isValidMessage = useCallback((msg: Message): boolean => {
    // Check if message has parts (newer structure)
    if (msg.parts && Array.isArray(msg.parts)) {
      // Check each part for validity
      return msg.parts.every((part) => {
        // Text parts are always valid
        if (part.type === "text") {
          return true;
        }

        // Tool invocation parts must be complete (have result) if state is "call" or "result"
        if (part.type === "tool-invocation") {
          const invocation = part as any;
          // If it's in "call" or "result" state, it must have a result
          if (invocation.state === "call" || invocation.state === "result") {
            const content = invocation.result?.content;
            // Ensure content exists and is not an empty array
            return (
              content && (Array.isArray(content) ? content.length > 0 : true)
            );
          }
          // partial-call state is okay without result
          return true;
        }

        // Other part types are assumed valid
        return true;
      });
    }

    // Check if message has content (legacy structure)
    if (msg.content && typeof msg.content === "string") {
      return msg.content.trim() !== "";
    }

    return false;
  }, []);

  // Load initial messages from localStorage
  const initialMessages = useMemo(() => {
    if (!isAuthenticated) return [];

    // Check if cache is expired
    if (isCacheExpired()) {
      // Clear expired data
      localStorage.removeItem(CHAT_STORAGE_KEY);
      localStorage.removeItem(TIMESTAMP_STORAGE_KEY);
      return [];
    }

    try {
      const stored = localStorage.getItem(CHAT_STORAGE_KEY);
      if (stored) {
        const parsed = JSON.parse(stored) as Message[];
        // Validate the data structure
        if (Array.isArray(parsed) && parsed.length > 0) {
          // Filter out any invalid or incomplete messages
          const validMessages = parsed.filter(isValidMessage);
          if (validMessages.length > 0) {
            // Update timestamp since we're loading existing messages
            updateTimestamp();
            return validMessages;
          }
        }
      }
    } catch (error) {
      console.error("Failed to load chat history:", error);
      // Clear corrupted data
      localStorage.removeItem(CHAT_STORAGE_KEY);
      localStorage.removeItem(TIMESTAMP_STORAGE_KEY);
    }

    return [];
  }, [isAuthenticated, isCacheExpired, updateTimestamp, isValidMessage]);

  // Function to save messages
  const saveMessages = useCallback(
    (messages: Message[]) => {
      if (!isAuthenticated || messages.length === 0) return;

      try {
        // Filter out invalid messages before storing
        const validMessages = messages.filter(isValidMessage);

        // Only store the most recent valid messages to avoid storage limits
        const messagesToStore = validMessages.slice(-MAX_STORED_MESSAGES);

        // Don't save if there are no valid messages
        if (messagesToStore.length === 0) {
          localStorage.removeItem(CHAT_STORAGE_KEY);
          localStorage.removeItem(TIMESTAMP_STORAGE_KEY);
          return;
        }

        localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(messagesToStore));
        // Update timestamp when saving messages (extends expiry)
        updateTimestamp();
      } catch (error) {
        console.error("Failed to save chat history:", error);
        // If we hit storage quota, try to clear old messages
        if (
          error instanceof DOMException &&
          error.name === "QuotaExceededError"
        ) {
          try {
            const validMessages = messages.filter(isValidMessage);
            const recentMessages = validMessages.slice(-50); // Keep only last 50
            localStorage.setItem(
              CHAT_STORAGE_KEY,
              JSON.stringify(recentMessages),
            );
            updateTimestamp();
          } catch {
            // If still failing, clear the storage
            localStorage.removeItem(CHAT_STORAGE_KEY);
            localStorage.removeItem(TIMESTAMP_STORAGE_KEY);
          }
        }
      }
    },
    [isAuthenticated, updateTimestamp, isValidMessage],
  );

  // Clear persisted messages
  const clearPersistedMessages = useCallback(() => {
    localStorage.removeItem(CHAT_STORAGE_KEY);
    localStorage.removeItem(TIMESTAMP_STORAGE_KEY);
  }, []);

  return {
    initialMessages,
    saveMessages,
    clearPersistedMessages,
  };
}

```

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

```typescript
import { describe, expect, it } from "vitest";
import {
  extractIssueId,
  parseIssueId,
  parseIssueParams,
} from "./issue-helpers";

describe("extractIssueId", () => {
  it("should extract issue ID from a full Sentry URL", () => {
    expect(
      extractIssueId("https://sentry.sentry.io/issues/1234"),
    ).toMatchInlineSnapshot(`
      {
        "issueId": "1234",
        "organizationSlug": "sentry",
      }
    `);
  });

  it("should extract issue ID from a Sentry URL with organization in path", () => {
    expect(
      extractIssueId("https://sentry.io/sentry/issues/123"),
    ).toMatchInlineSnapshot(`
      {
        "issueId": "123",
        "organizationSlug": "sentry",
      }
    `);
  });

  it("should extract issue ID and org slug from URL with organizations path", () => {
    expect(
      extractIssueId("https://sentry.io/organizations/my-org/issues/123"),
    ).toMatchInlineSnapshot(`
      {
        "issueId": "123",
        "organizationSlug": "my-org",
      }
    `);
  });

  it("should extract issue ID and org slug from subdomain URL", () => {
    expect(extractIssueId("https://my-team.sentry.io/issues/123")).toEqual({
      issueId: "123",
      organizationSlug: "my-team",
    });
  });

  it("should extract issue ID and org slug from self-hosted Sentry with subdomain", () => {
    expect(
      extractIssueId("https://sentry.mycompany.com/issues/123"),
    ).toMatchInlineSnapshot(`
      {
        "issueId": "123",
        "organizationSlug": "sentry",
      }
    `);
  });

  it("should extract issue ID and org slug from self-hosted Sentry with organization path", () => {
    expect(
      extractIssueId("https://mycompany.com/my-team/issues/123"),
    ).toMatchInlineSnapshot(`
      {
        "issueId": "123",
        "organizationSlug": "my-team",
      }
    `);
  });

  it("should throw error for empty input", () => {
    expect(() => extractIssueId("")).toThrowErrorMatchingInlineSnapshot(
      `[UserInputError: Invalid Sentry issue URL. URL must be a non-empty string.]`,
    );
  });

  it("should throw error for invalid URL path", () => {
    expect(() =>
      extractIssueId("https://sentry.sentry.io/projects/123"),
    ).toThrowErrorMatchingInlineSnapshot(
      `[UserInputError: Invalid Sentry issue URL. Path must contain '/issues/{issue_id}']`,
    );
  });

  it("should throw error for non-numeric issue ID in URL", () => {
    expect(
      extractIssueId("https://sentry.sentry.io/issues/abc"),
    ).toMatchInlineSnapshot(`
      {
        "issueId": "abc",
        "organizationSlug": "sentry",
      }
    `);
  });

  it("should throw error for non-numeric standalone ID", () => {
    expect(() => extractIssueId("abc")).toThrowErrorMatchingInlineSnapshot(
      `[UserInputError: Invalid Sentry issue URL. Must start with http:// or https://]`,
    );
  });
});

describe("parseIssueId", () => {
  describe("cleaning", () => {
    it("should remove trailing punctuation", () => {
      expect(parseIssueId("CLOUDFLARE-MCP-41.!")).toBe("CLOUDFLARE-MCP-41");
    });

    it("should remove special characters except dash and underscore", () => {
      expect(parseIssueId("ID_123-456!@#")).toBe("ID_123-456");
    });
  });

  describe("format validation", () => {
    it("should accept pure numeric issue IDs", () => {
      expect(parseIssueId("12345")).toBe("12345");
    });

    it("should accept project-based IDs starting with letters", () => {
      expect(parseIssueId("PROJECT-123")).toBe("PROJECT-123");
      expect(parseIssueId("MCP-SERVER-E9E")).toBe("MCP-SERVER-E9E");
    });

    it("should accept project-based IDs starting with numbers", () => {
      expect(parseIssueId("3R-3")).toBe("3R-3");
      expect(parseIssueId("3R-AUTOMATION-SYSTEM-3")).toBe(
        "3R-AUTOMATION-SYSTEM-3",
      );
    });

    it("should throw error for invalid formats", () => {
      // Starting with hyphen
      expect(() => parseIssueId("-123")).toThrowError(
        /Invalid issue ID format/,
      );

      // Ending with hyphen
      expect(() => parseIssueId("PROJECT-")).toThrowError(
        /Invalid issue ID format/,
      );

      // Empty string after cleaning
      expect(() => parseIssueId("!!!")).toThrowError(
        /empty after removing special characters/,
      );
    });
  });
});

describe("parseIssueParams", () => {
  it("should parse from issueUrl", () => {
    expect(
      parseIssueParams({
        issueUrl: "https://sentry.io/sentry/issues/123",
      }),
    ).toEqual({ organizationSlug: "sentry", issueId: "123" });
  });

  it("should parse from issueId and organizationSlug", () => {
    expect(
      parseIssueParams({
        issueId: "CLOUDFLARE-MCP-41.!",
        organizationSlug: "sentry-mcp-evals",
      }),
    ).toEqual({
      organizationSlug: "sentry-mcp-evals",
      issueId: "CLOUDFLARE-MCP-41",
    });
  });

  it("should throw if neither issueId nor issueUrl is provided", () => {
    expect(() =>
      parseIssueParams({ organizationSlug: "foo" }),
    ).toThrowErrorMatchingInlineSnapshot(
      `[UserInputError: Either issueId or issueUrl must be provided]`,
    );
  });

  it("should throw if organizationSlug is missing and no issueUrl", () => {
    expect(() =>
      parseIssueParams({ issueId: "123" }),
    ).toThrowErrorMatchingInlineSnapshot(
      `[UserInputError: Organization slug is required]`,
    );
  });

  it("should throw if issueUrl is invalid", () => {
    expect(() =>
      parseIssueParams({ issueUrl: "not-a-url" }),
    ).toThrowErrorMatchingInlineSnapshot(
      `[UserInputError: Invalid Sentry issue URL. Must start with http:// or https://]`,
    );
  });
});

```

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

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

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

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

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

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

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

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

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

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

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

```

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

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

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

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

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

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

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

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

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

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

  return { attributes: customAttributes, fieldTypes };
}

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

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

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

      const recommendedFields = RECOMMENDED_FIELDS[dataset];

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

      return `Dataset: ${dataset}

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

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

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

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

Use this information to construct appropriate queries for the ${dataset} dataset.`;
    },
  });
}

```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/update-issue.ts:
--------------------------------------------------------------------------------

```typescript
import { setTag } from "@sentry/core";
import { defineTool } from "../internal/tool-helpers/define";
import { apiServiceFromContext } from "../internal/tool-helpers/api";
import { parseIssueParams } from "../internal/tool-helpers/issue";
import { formatAssignedTo } from "../internal/tool-helpers/formatting";
import { UserInputError } from "../errors";
import type { ServerContext } from "../types";
import {
  ParamOrganizationSlug,
  ParamRegionUrl,
  ParamIssueShortId,
  ParamIssueUrl,
  ParamIssueStatus,
  ParamAssignedTo,
} from "../schema";

export default defineTool({
  name: "update_issue",
  requiredScopes: ["event:write"],
  description: [
    "Update an issue's status or assignment in Sentry. This allows you to resolve, ignore, or reassign issues.",
    "",
    "Use this tool when you need to:",
    "- Resolve an issue that has been fixed",
    "- Assign an issue to a team member or team for investigation",
    "- Mark an issue as ignored to reduce noise",
    "- Reopen a resolved issue by setting status to 'unresolved'",
    "",
    "<examples>",
    "### Resolve an issue",
    "",
    "```",
    "update_issue(organizationSlug='my-organization', issueId='PROJECT-123', status='resolved')",
    "```",
    "",
    "### Assign an issue to a user (use whoami to get your user ID)",
    "",
    "```",
    "update_issue(organizationSlug='my-organization', issueId='PROJECT-123', assignedTo='user:123456')",
    "```",
    "",
    "### Assign an issue to a team",
    "",
    "```",
    "update_issue(organizationSlug='my-organization', issueId='PROJECT-123', assignedTo='team:789')",
    "```",
    "",
    "### Mark an issue as ignored",
    "",
    "```",
    "update_issue(organizationSlug='my-organization', issueId='PROJECT-123', status='ignored')",
    "```",
    "",
    "</examples>",
    "",
    "<hints>",
    "- If the user provides the `issueUrl`, you can ignore the other required parameters and extract them from the URL.",
    "- At least one of `status` or `assignedTo` must be provided to update the issue.",
    "- assignedTo format: Use 'user:ID' for users (e.g., 'user:123456') or 'team:ID' for teams (e.g., 'team:789')",
    "- To find your user ID, first use the whoami tool which returns your numeric user ID",
    "- Valid status values are: 'resolved', 'resolvedInNextRelease', 'unresolved', 'ignored'.",
    "</hints>",
  ].join("\n"),
  inputSchema: {
    organizationSlug: ParamOrganizationSlug.optional(),
    regionUrl: ParamRegionUrl.optional(),
    issueId: ParamIssueShortId.optional(),
    issueUrl: ParamIssueUrl.optional(),
    status: ParamIssueStatus.optional(),
    assignedTo: ParamAssignedTo.optional(),
  },
  annotations: {
    readOnlyHint: false,
    destructiveHint: true,
    idempotentHint: true,
    openWorldHint: true,
  },
  async handler(params, context: ServerContext) {
    const apiService = apiServiceFromContext(context, {
      regionUrl: params.regionUrl,
    });

    // Validate that we have the minimum required parameters
    if (!params.issueUrl && !params.issueId) {
      throw new UserInputError(
        "Either `issueId` or `issueUrl` must be provided",
      );
    }

    if (!params.issueUrl && !params.organizationSlug) {
      throw new UserInputError(
        "`organizationSlug` is required when providing `issueId`",
      );
    }

    // Validate that at least one update parameter is provided
    if (!params.status && !params.assignedTo) {
      throw new UserInputError(
        "At least one of `status` or `assignedTo` must be provided to update the issue",
      );
    }

    const { organizationSlug: orgSlug, issueId: parsedIssueId } =
      parseIssueParams({
        organizationSlug: params.organizationSlug,
        issueId: params.issueId,
        issueUrl: params.issueUrl,
      });

    setTag("organization.slug", orgSlug);

    // Get current issue details first
    const currentIssue = await apiService.getIssue({
      organizationSlug: orgSlug,
      issueId: parsedIssueId!,
    });

    // Update the issue
    const updatedIssue = await apiService.updateIssue({
      organizationSlug: orgSlug,
      issueId: parsedIssueId!,
      status: params.status,
      assignedTo: params.assignedTo,
    });

    let output = `# Issue ${updatedIssue.shortId} Updated in **${orgSlug}**\n\n`;
    output += `**Issue**: ${updatedIssue.title}\n`;
    output += `**URL**: ${apiService.getIssueUrl(orgSlug, updatedIssue.shortId)}\n\n`;

    // Show what changed
    output += "## Changes Made\n\n";

    if (params.status && currentIssue.status !== params.status) {
      output += `**Status**: ${currentIssue.status} → **${params.status}**\n`;
    }

    if (params.assignedTo) {
      const oldAssignee = formatAssignedTo(currentIssue.assignedTo ?? null);
      const newAssignee =
        params.assignedTo === "me" ? "You" : params.assignedTo;
      output += `**Assigned To**: ${oldAssignee} → **${newAssignee}**\n`;
    }

    output += "\n## Current Status\n\n";
    output += `**Status**: ${updatedIssue.status}\n`;
    const currentAssignee = formatAssignedTo(updatedIssue.assignedTo ?? null);
    output += `**Assigned To**: ${currentAssignee}\n`;

    output += "\n# Using this information\n\n";
    output += `- The issue has been successfully updated in Sentry\n`;
    output += `- You can view the issue details using: \`get_issue_details(organizationSlug="${orgSlug}", issueId="${updatedIssue.shortId}")\`\n`;

    if (params.status === "resolved") {
      output += `- The issue is now marked as resolved and will no longer generate alerts\n`;
    } else if (params.status === "ignored") {
      output += `- The issue is now ignored and will not generate alerts until it escalates\n`;
    }

    return output;
  },
});

```

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

```typescript
import { Hono } from "hono";
import type { AuthRequest } from "@cloudflare/workers-oauth-provider";
import {
  renderApprovalDialog,
  parseRedirectApproval,
} from "../../lib/approval-dialog";
import type { Env } from "../../types";
import { SENTRY_AUTH_URL } from "../constants";
import { getUpstreamAuthorizeUrl } from "../helpers";
import { SCOPES } from "../../../constants";
import { signState, type OAuthState } from "../state";
import { logWarn } from "@sentry/mcp-server/telem/logging";

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

async function redirectToUpstream(
  env: Env,
  request: Request,
  oauthReqInfo: AuthRequest | AuthRequestWithPermissions,
  headers: Record<string, string> = {},
  stateOverride?: string,
) {
  return new Response(null, {
    status: 302,
    headers: {
      ...headers,
      location: getUpstreamAuthorizeUrl({
        upstream_url: new URL(
          SENTRY_AUTH_URL,
          `https://${env.SENTRY_HOST || "sentry.io"}`,
        ).href,
        scope: Object.keys(SCOPES).join(" "),
        client_id: env.SENTRY_CLIENT_ID,
        redirect_uri: new URL("/oauth/callback", request.url).href,
        state: stateOverride ?? btoa(JSON.stringify(oauthReqInfo)),
      }),
    },
  });
}

// Export Hono app for /authorize endpoints
export default new Hono<{ Bindings: Env }>()
  /**
   * OAuth Authorization Endpoint (GET /oauth/authorize)
   *
   * This route initiates the OAuth flow when a user wants to log in.
   */
  .get("/", async (c) => {
    let oauthReqInfo: AuthRequest;
    try {
      oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw);
    } catch (err) {
      // Log invalid redirect URI errors without sending them to Sentry
      const errorMessage = err instanceof Error ? err.message : String(err);
      if (errorMessage.includes("Invalid redirect URI")) {
        logWarn(`OAuth authorization failed: ${errorMessage}`, {
          loggerScope: ["cloudflare", "oauth", "authorize"],
          extra: { error: errorMessage },
        });
        return c.text("Invalid redirect URI", 400);
      }
      // Re-throw other errors to be captured by Sentry
      throw err;
    }

    const { clientId } = oauthReqInfo;
    if (!clientId) {
      return c.text("Invalid request", 400);
    }

    // XXX(dcramer): we want to confirm permissions on each time
    // so you can always choose new ones
    // This shouldn't be highly visible to users, as clients should use refresh tokens
    // behind the scenes.
    //
    // 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,
    //   clientId,
    //   c.env.COOKIE_SECRET,
    // );
    // if (isApproved) {
    //   return redirectToUpstream(c.env, c.req.raw, oauthReqInfo);
    // }

    return renderApprovalDialog(c.req.raw, {
      client: await c.env.OAUTH_PROVIDER.lookupClient(clientId),
      server: {
        name: "Sentry MCP",
      },
      state: { oauthReqInfo }, // arbitrary data that flows through the form submission below
    });
  })

  /**
   * OAuth Authorization Endpoint (POST /oauth/authorize)
   *
   * This route handles the approval form submission and redirects to Sentry.
   */
  .post("/", async (c) => {
    // Validates form submission, extracts state, and generates Set-Cookie headers to skip approval dialog next time
    let result: Awaited<ReturnType<typeof parseRedirectApproval>>;
    try {
      result = await parseRedirectApproval(c.req.raw, c.env.COOKIE_SECRET);
    } catch (err) {
      logWarn("Failed to parse approval form", {
        loggerScope: ["cloudflare", "oauth", "authorize"],
        extra: { error: String(err) },
      });
      return c.text("Invalid request", 400);
    }

    const { state, headers, permissions } = result;

    if (!state.oauthReqInfo) {
      return c.text("Invalid request", 400);
    }

    // Store the selected permissions in the OAuth request info
    // This will be passed through to the callback via the state parameter
    const oauthReqWithPermissions = {
      ...state.oauthReqInfo,
      permissions,
    };

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

    // Build signed state for redirect to Sentry (10 minute validity)
    const now = Date.now();
    const payload: OAuthState = {
      req: oauthReqWithPermissions as unknown as Record<string, unknown>,
      iat: now,
      exp: now + 10 * 60 * 1000,
    };
    const signedState = await signState(payload, c.env.COOKIE_SECRET);

    return redirectToUpstream(
      c.env,
      c.req.raw,
      oauthReqWithPermissions,
      headers,
      signedState,
    );
  });

```

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

```typescript
import { describe, it, expect } from "vitest";
import { z } from "zod";
import { wrapToolForAgent } from "./tool-wrapper";
import type { ServerContext } from "../../types";
import type { ToolConfig } from "../types";

// Create a simple mock tool for testing
const mockTool: ToolConfig<{
  organizationSlug: z.ZodOptional<z.ZodString>;
  projectSlug: z.ZodOptional<z.ZodString>;
  someParam: z.ZodString;
}> = {
  name: "mock_tool",
  description: "A mock tool for testing",
  inputSchema: {
    organizationSlug: z.string().optional(), // Optional to test constraint injection
    projectSlug: z.string().optional(),
    someParam: z.string(),
  },
  requiredScopes: [],
  annotations: {
    readOnlyHint: true,
    openWorldHint: true,
  },
  handler: async (params, context) => {
    // Return the params so we can verify what was passed
    return JSON.stringify({
      params,
      contextOrg: context.constraints.organizationSlug,
      contextProject: context.constraints.projectSlug,
    });
  },
};

describe("wrapToolForAgent", () => {
  it("wraps a tool and calls it successfully", async () => {
    const context: ServerContext = {
      accessToken: "test-token",
      sentryHost: "sentry.io",
      userId: "1",
      clientId: "test-client",
      constraints: {},
      grantedScopes: new Set([]),
    };

    const wrappedTool = wrapToolForAgent(mockTool, { context });

    // Call the wrapped tool
    // AI SDK tools expect a toolContext parameter (messages, abortSignal, etc.)
    const result = await wrappedTool.execute(
      {
        organizationSlug: "my-org",
        someParam: "test-value",
      },
      {} as any, // Empty tool context for testing
    );

    // Verify the tool was called with correct params
    expect(result.result).toBeDefined();
    const parsed = JSON.parse(result.result as string);
    expect(parsed.params.organizationSlug).toBe("my-org");
    expect(parsed.params.someParam).toBe("test-value");
  });

  it("injects organizationSlug constraint", async () => {
    const context: ServerContext = {
      accessToken: "test-token",
      sentryHost: "sentry.io",
      userId: "1",
      clientId: "test-client",
      constraints: {
        organizationSlug: "constrained-org",
      },
      grantedScopes: new Set([]),
    };

    const wrappedTool = wrapToolForAgent(mockTool, { context });

    // Call without providing organizationSlug
    const result = await wrappedTool.execute(
      {
        someParam: "test-value",
      },
      {} as any,
    );

    // Verify the constraint was injected
    expect(result.result).toBeDefined();
    const parsed = JSON.parse(result.result as string);
    expect(parsed.params.organizationSlug).toBe("constrained-org");
    expect(parsed.contextOrg).toBe("constrained-org");
  });

  it("injects projectSlug constraint", async () => {
    const context: ServerContext = {
      accessToken: "test-token",
      sentryHost: "sentry.io",
      userId: "1",
      clientId: "test-client",
      constraints: {
        organizationSlug: "constrained-org",
        projectSlug: "constrained-project",
      },
      grantedScopes: new Set([]),
    };

    const wrappedTool = wrapToolForAgent(mockTool, { context });

    // Call without providing projectSlug
    const result = await wrappedTool.execute(
      {
        someParam: "test-value",
      },
      {} as any,
    );

    // Verify both constraints were injected
    expect(result.result).toBeDefined();
    const parsed = JSON.parse(result.result as string);
    expect(parsed.params.organizationSlug).toBe("constrained-org");
    expect(parsed.params.projectSlug).toBe("constrained-project");
    expect(parsed.contextProject).toBe("constrained-project");
  });

  it("allows agent-provided params to override constraints", async () => {
    const context: ServerContext = {
      accessToken: "test-token",
      sentryHost: "sentry.io",
      userId: "1",
      clientId: "test-client",
      constraints: {
        organizationSlug: "constrained-org",
      },
      grantedScopes: new Set([]),
    };

    const wrappedTool = wrapToolForAgent(mockTool, { context });

    // Provide organizationSlug explicitly (should NOT override since constraint injection doesn't override)
    const result = await wrappedTool.execute(
      {
        organizationSlug: "agent-provided-org",
        someParam: "test-value",
      },
      {} as any,
    );

    expect(result.result).toBeDefined();
    const parsed = JSON.parse(result.result as string);
    // The constraint injection only adds if not present, so agent's value should remain
    expect(parsed.params.organizationSlug).toBe("agent-provided-org");
  });

  it("handles tool errors via agentTool wrapper", async () => {
    const errorTool: ToolConfig<{ param: z.ZodString }> = {
      name: "error_tool",
      description: "A tool that throws an error",
      inputSchema: {
        param: z.string(),
      },
      requiredScopes: [],
      annotations: {
        readOnlyHint: true,
        openWorldHint: true,
      },
      handler: async () => {
        throw new Error("Test error from tool");
      },
    };

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

    const wrappedTool = wrapToolForAgent(errorTool, { context });

    // Call the tool that throws an error
    const result = await wrappedTool.execute({ param: "test" }, {} as any);

    // Verify the error was caught and returned in structured format
    // Generic errors are wrapped as "System Error" by agentTool for security
    expect(result.error).toBeDefined();
    expect(result.error).toContain("System Error");
    expect(result.error).toContain("Event ID:");
    expect(result.result).toBeUndefined();
  });
});

```

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

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

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

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

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

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

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

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

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

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

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

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

```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```

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

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

```
Page 4/12FirstPrevNextLast