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

# Directory Structure

```
├── .claude
│   ├── agents
│   │   └── claude-optimizer.md
│   ├── commands
│   │   ├── gh-pr.md
│   │   └── gh-review.md
│   └── settings.json
├── .craft.yml
├── .cursor
│   └── mcp.json
├── .env.example
├── .github
│   └── workflows
│       ├── deploy.yml
│       ├── eval.yml
│       ├── merge-jobs.yml
│       ├── release.yml
│       ├── smoke-tests.yml
│       ├── test.yml
│       └── token-cost.yml
├── .gitignore
├── .mcp.json
├── .vscode
│   ├── extensions.json
│   ├── mcp.json
│   └── settings.json
├── AGENTS.md
├── benchmark-agent.sh
├── bin
│   └── bump-version.sh
├── biome.json
├── CLAUDE.md
├── codecov.yml
├── core
├── docs
│   ├── adding-tools.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-cloudflare/src/client/components/chat/chat-input.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useEffect, useRef } from "react";
  2 | import { Send, CircleStop } from "lucide-react";
  3 | import { Button } from "../ui/button";
  4 | 
  5 | interface ChatInputProps {
  6 |   input: string;
  7 |   isLoading: boolean;
  8 |   isOpen: boolean;
  9 |   onInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
 10 |   onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
 11 |   onStop: () => void;
 12 |   onSlashCommand?: (command: string) => void;
 13 | }
 14 | 
 15 | export function ChatInput({
 16 |   input,
 17 |   isLoading,
 18 |   isOpen,
 19 |   onInputChange,
 20 |   onSubmit,
 21 |   onStop,
 22 |   onSlashCommand,
 23 | }: ChatInputProps) {
 24 |   const inputRef = useRef<HTMLTextAreaElement>(null);
 25 | 
 26 |   // Focus when dialog opens (with delay for mobile animation)
 27 |   useEffect(() => {
 28 |     if (isOpen) {
 29 |       // Add delay to ensure the slide-in animation completes on mobile
 30 |       const timer = setTimeout(() => {
 31 |         // Use requestAnimationFrame to ensure browser has finished layout
 32 |         requestAnimationFrame(() => {
 33 |           if (inputRef.current && !inputRef.current.disabled) {
 34 |             inputRef.current.focus({ preventScroll: false });
 35 |           }
 36 |         });
 37 |       }, 600); // Delay to account for 500ms animation
 38 |       return () => clearTimeout(timer);
 39 |     }
 40 |   }, [isOpen]);
 41 | 
 42 |   // Re-focus when loading finishes
 43 |   useEffect(() => {
 44 |     if (inputRef.current && !isLoading && isOpen) {
 45 |       inputRef.current.focus();
 46 |     }
 47 |   }, [isLoading, isOpen]);
 48 | 
 49 |   const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
 50 |     e.preventDefault();
 51 | 
 52 |     // Check if input is a slash command
 53 |     if (input.startsWith("/") && onSlashCommand) {
 54 |       const command = input.slice(1).toLowerCase().trim();
 55 |       // Pass all slash commands to the handler, let it decide what to do
 56 |       onSlashCommand(command);
 57 |       return;
 58 |     }
 59 | 
 60 |     // Otherwise, submit normally
 61 |     onSubmit(e);
 62 |   };
 63 | 
 64 |   // Handle keyboard shortcuts
 65 |   const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
 66 |     // Ctrl+J or Cmd+J: Insert newline
 67 |     if ((e.ctrlKey || e.metaKey) && e.key === "j") {
 68 |       e.preventDefault();
 69 |       const textarea = e.currentTarget;
 70 |       const start = textarea.selectionStart;
 71 |       const end = textarea.selectionEnd;
 72 |       const newValue = `${input.substring(0, start)}\n${input.substring(end)}`;
 73 | 
 74 |       // Create a synthetic event to update the input
 75 |       const syntheticEvent = {
 76 |         target: { value: newValue },
 77 |         currentTarget: { value: newValue },
 78 |       } as React.ChangeEvent<HTMLTextAreaElement>;
 79 | 
 80 |       onInputChange(syntheticEvent);
 81 | 
 82 |       // Move cursor after the inserted newline
 83 |       setTimeout(() => {
 84 |         if (textarea) {
 85 |           textarea.selectionStart = textarea.selectionEnd = start + 1;
 86 |         }
 87 |       }, 0);
 88 |       return;
 89 |     }
 90 | 
 91 |     // Enter without shift: Submit
 92 |     if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
 93 |       e.preventDefault();
 94 |       const form = e.currentTarget.form;
 95 |       if (form && input.trim()) {
 96 |         form.requestSubmit();
 97 |       }
 98 |     }
 99 |   };
100 | 
101 |   // Auto-resize textarea based on content
102 |   // biome-ignore lint/correctness/useExhaustiveDependencies: input is needed to trigger resize when content changes
103 |   useEffect(() => {
104 |     if (inputRef.current) {
105 |       inputRef.current.style.height = "auto";
106 |       inputRef.current.style.height = `${inputRef.current.scrollHeight}px`;
107 |     }
108 |   }, [input]);
109 | 
110 |   return (
111 |     <form onSubmit={handleSubmit} className="relative flex-1">
112 |       <div className="relative">
113 |         <textarea
114 |           ref={inputRef}
115 |           value={input}
116 |           onChange={onInputChange}
117 |           onKeyDown={handleKeyDown}
118 |           placeholder="Ask me anything about your Sentry data..."
119 |           disabled={isLoading}
120 |           rows={1}
121 |           className="w-full p-4 pr-12 rounded bg-slate-800/50 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-300 focus:border-transparent disabled:opacity-50 resize-none overflow-hidden"
122 |         />
123 |         <Button
124 |           type={isLoading ? "button" : "submit"}
125 |           variant="ghost"
126 |           onClick={isLoading ? onStop : undefined}
127 |           disabled={!isLoading && !input.trim()}
128 |           className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-slate-400 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:text-slate-400 disabled:hover:bg-transparent transition-colors"
129 |           title={isLoading ? "Stop generation" : "Send message"}
130 |         >
131 |           {isLoading ? (
132 |             <CircleStop className="h-4 w-4" />
133 |           ) : (
134 |             <Send className="h-4 w-4" />
135 |           )}
136 |         </Button>
137 |       </div>
138 |     </form>
139 |   );
140 | }
141 | 
```

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

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

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

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

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

```json
  1 | {
  2 |   "namespace": "host",
  3 |   "description": "A host is defined as a computing instance. For example, physical servers, virtual machines, switches or disk array.\n",
  4 |   "attributes": {
  5 |     "host.id": {
  6 |       "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",
  7 |       "type": "string",
  8 |       "stability": "development",
  9 |       "examples": ["fdbf79e8af94cb7f9e8df36789187052"]
 10 |     },
 11 |     "host.name": {
 12 |       "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",
 13 |       "type": "string",
 14 |       "stability": "development",
 15 |       "examples": ["opentelemetry-test"]
 16 |     },
 17 |     "host.type": {
 18 |       "description": "Type of host. For Cloud, this must be the machine type.\n",
 19 |       "type": "string",
 20 |       "stability": "development",
 21 |       "examples": ["n1-standard-1"]
 22 |     },
 23 |     "host.arch": {
 24 |       "description": "The CPU architecture the host system is running on.\n",
 25 |       "type": "string",
 26 |       "stability": "development",
 27 |       "examples": [
 28 |         "amd64",
 29 |         "arm32",
 30 |         "arm64",
 31 |         "ia64",
 32 |         "ppc32",
 33 |         "ppc64",
 34 |         "s390x",
 35 |         "x86"
 36 |       ]
 37 |     },
 38 |     "host.image.name": {
 39 |       "description": "Name of the VM image or OS install the host was instantiated from.\n",
 40 |       "type": "string",
 41 |       "stability": "development",
 42 |       "examples": [
 43 |         "infra-ami-eks-worker-node-7d4ec78312",
 44 |         "CentOS-8-x86_64-1905"
 45 |       ]
 46 |     },
 47 |     "host.image.id": {
 48 |       "description": "VM image ID or host OS image ID. For Cloud, this value is from the provider.\n",
 49 |       "type": "string",
 50 |       "stability": "development",
 51 |       "examples": ["ami-07b06b442921831e5"]
 52 |     },
 53 |     "host.image.version": {
 54 |       "description": "The version string of the VM image or host OS as defined in [Version Attributes](/docs/resource/README.md#version-attributes).\n",
 55 |       "type": "string",
 56 |       "stability": "development",
 57 |       "examples": ["0.1"]
 58 |     },
 59 |     "host.ip": {
 60 |       "description": "Available IP addresses of the host, excluding loopback interfaces.\n",
 61 |       "type": "string",
 62 |       "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",
 63 |       "stability": "development",
 64 |       "examples": ["[\"192.168.1.140\",\"fe80::abc2:4a28:737a:609e\"]"]
 65 |     },
 66 |     "host.mac": {
 67 |       "description": "Available MAC addresses of the host, excluding loopback interfaces.\n",
 68 |       "type": "string",
 69 |       "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",
 70 |       "stability": "development",
 71 |       "examples": ["[\"AC-DE-48-23-45-67\",\"AC-DE-48-23-45-67-01-9F\"]"]
 72 |     },
 73 |     "host.cpu.vendor.id": {
 74 |       "description": "Processor manufacturer identifier. A maximum 12-character string.\n",
 75 |       "type": "string",
 76 |       "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",
 77 |       "stability": "development",
 78 |       "examples": ["GenuineIntel"]
 79 |     },
 80 |     "host.cpu.family": {
 81 |       "description": "Family or generation of the CPU.\n",
 82 |       "type": "string",
 83 |       "stability": "development",
 84 |       "examples": ["6", "PA-RISC 1.1e"]
 85 |     },
 86 |     "host.cpu.model.id": {
 87 |       "description": "Model identifier. It provides more granular information about the CPU, distinguishing it from other CPUs within the same family.\n",
 88 |       "type": "string",
 89 |       "stability": "development",
 90 |       "examples": ["6", "9000/778/B180L"]
 91 |     },
 92 |     "host.cpu.model.name": {
 93 |       "description": "Model designation of the processor.\n",
 94 |       "type": "string",
 95 |       "stability": "development",
 96 |       "examples": ["11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz"]
 97 |     },
 98 |     "host.cpu.stepping": {
 99 |       "description": "Stepping or core revisions.\n",
100 |       "type": "string",
101 |       "stability": "development",
102 |       "examples": ["1", "r1p1"]
103 |     },
104 |     "host.cpu.cache.l2.size": {
105 |       "description": "The amount of level 2 memory cache available to the processor (in Bytes).\n",
106 |       "type": "number",
107 |       "stability": "development",
108 |       "examples": ["12288000"]
109 |     }
110 |   }
111 | }
112 | 
```

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

```json
  1 | {
  2 |   "id": "6507376925",
  3 |   "shareId": null,
  4 |   "shortId": "CLOUDFLARE-MCP-41",
  5 |   "title": "Error: Tool list_organizations is already registered",
  6 |   "culprit": "Object.fetch(index)",
  7 |   "permalink": "https://sentry-mcp-evals.sentry.io/issues/6507376925/",
  8 |   "logger": null,
  9 |   "level": "error",
 10 |   "status": "unresolved",
 11 |   "statusDetails": {},
 12 |   "substatus": "ongoing",
 13 |   "isPublic": false,
 14 |   "platform": "javascript",
 15 |   "project": {
 16 |     "id": "4509062593708032",
 17 |     "name": "CLOUDFLARE-MCP",
 18 |     "slug": "CLOUDFLARE-MCP",
 19 |     "platform": "bun"
 20 |   },
 21 |   "type": "error",
 22 |   "metadata": {
 23 |     "value": "Tool list_organizations is already registered",
 24 |     "type": "Error",
 25 |     "filename": "index.js",
 26 |     "function": "Object.fetch",
 27 |     "in_app_frame_mix": "in-app-only",
 28 |     "sdk": {
 29 |       "name": "sentry.javascript.cloudflare",
 30 |       "name_normalized": "sentry.javascript.cloudflare"
 31 |     },
 32 |     "severity": 0,
 33 |     "severity_reason": "ml",
 34 |     "initial_priority": 50,
 35 |     "title": "Error: Tool list_organizations is already registered"
 36 |   },
 37 |   "numComments": 0,
 38 |   "assignedTo": null,
 39 |   "isBookmarked": false,
 40 |   "isSubscribed": false,
 41 |   "subscriptionDetails": null,
 42 |   "hasSeen": true,
 43 |   "annotations": [],
 44 |   "issueType": "error",
 45 |   "issueCategory": "error",
 46 |   "priority": "medium",
 47 |   "priorityLockedAt": null,
 48 |   "isUnhandled": true,
 49 |   "count": "25",
 50 |   "userCount": 1,
 51 |   "firstSeen": "2025-04-03T22:51:19.403000Z",
 52 |   "lastSeen": "2025-04-12T11:34:11Z",
 53 |   "firstRelease": null,
 54 |   "lastRelease": null,
 55 |   "activity": [
 56 |     {
 57 |       "id": "4633815464",
 58 |       "user": null,
 59 |       "type": "auto_set_ongoing",
 60 |       "data": {
 61 |         "after_days": 7
 62 |       },
 63 |       "dateCreated": "2025-04-10T22:55:22.411699Z"
 64 |     },
 65 |     {
 66 |       "id": "0",
 67 |       "user": null,
 68 |       "type": "first_seen",
 69 |       "data": {
 70 |         "priority": "medium"
 71 |       },
 72 |       "dateCreated": "2025-04-03T22:51:19.403000Z"
 73 |     }
 74 |   ],
 75 |   "openPeriods": [
 76 |     {
 77 |       "start": "2025-04-03T22:51:19.403000Z",
 78 |       "end": null,
 79 |       "duration": null,
 80 |       "isOpen": true,
 81 |       "lastChecked": "2025-04-12T11:34:11.310000Z"
 82 |     }
 83 |   ],
 84 |   "seenBy": [
 85 |     {
 86 |       "id": "1",
 87 |       "name": "David Cramer",
 88 |       "username": "[email protected]",
 89 |       "email": "[email protected]",
 90 |       "avatarUrl": null,
 91 |       "isActive": true,
 92 |       "hasPasswordAuth": true,
 93 |       "isManaged": false,
 94 |       "dateJoined": "2012-01-14T22:08:29.270831Z",
 95 |       "lastLogin": "2025-04-13T14:00:11.516852Z",
 96 |       "has2fa": true,
 97 |       "lastActive": "2025-04-13T18:10:49.177605Z",
 98 |       "isSuperuser": true,
 99 |       "isStaff": true,
100 |       "experiments": {},
101 |       "emails": [
102 |         {
103 |           "id": "87429",
104 |           "email": "[email protected]",
105 |           "is_verified": true
106 |         }
107 |       ],
108 |       "options": {
109 |         "theme": "light",
110 |         "language": "en",
111 |         "stacktraceOrder": 2,
112 |         "defaultIssueEvent": "recommended",
113 |         "timezone": "US/Pacific",
114 |         "clock24Hours": false
115 |       },
116 |       "flags": {
117 |         "newsletter_consent_prompt": false
118 |       },
119 |       "avatar": {
120 |         "avatarType": "upload",
121 |         "avatarUuid": "51e63edabf31412aa2a955e9cf2c1ca0",
122 |         "avatarUrl": "https://sentry.io/avatar/51e63edabf31412aa2a955e9cf2c1ca0/"
123 |       },
124 |       "identities": [],
125 |       "lastSeen": "2025-04-08T23:15:26.569455Z"
126 |     }
127 |   ],
128 |   "pluginActions": [],
129 |   "pluginIssues": [],
130 |   "pluginContexts": [],
131 |   "userReportCount": 0,
132 |   "stats": {
133 |     "24h": [
134 |       [1744480800, 0],
135 |       [1744484400, 0],
136 |       [1744488000, 0],
137 |       [1744491600, 0],
138 |       [1744495200, 0],
139 |       [1744498800, 0],
140 |       [1744502400, 0],
141 |       [1744506000, 0],
142 |       [1744509600, 0],
143 |       [1744513200, 0],
144 |       [1744516800, 0],
145 |       [1744520400, 0],
146 |       [1744524000, 0],
147 |       [1744527600, 0],
148 |       [1744531200, 0],
149 |       [1744534800, 0],
150 |       [1744538400, 0],
151 |       [1744542000, 0],
152 |       [1744545600, 0],
153 |       [1744549200, 0],
154 |       [1744552800, 0],
155 |       [1744556400, 0],
156 |       [1744560000, 0],
157 |       [1744563600, 0],
158 |       [1744567200, 0]
159 |     ],
160 |     "30d": [
161 |       [1741910400, 0],
162 |       [1741996800, 0],
163 |       [1742083200, 0],
164 |       [1742169600, 0],
165 |       [1742256000, 0],
166 |       [1742342400, 0],
167 |       [1742428800, 0],
168 |       [1742515200, 0],
169 |       [1742601600, 0],
170 |       [1742688000, 0],
171 |       [1742774400, 0],
172 |       [1742860800, 0],
173 |       [1742947200, 0],
174 |       [1743033600, 0],
175 |       [1743120000, 0],
176 |       [1743206400, 0],
177 |       [1743292800, 0],
178 |       [1743379200, 0],
179 |       [1743465600, 0],
180 |       [1743552000, 0],
181 |       [1743638400, 1],
182 |       [1743724800, 0],
183 |       [1743811200, 0],
184 |       [1743897600, 0],
185 |       [1743984000, 0],
186 |       [1744070400, 20],
187 |       [1744156800, 1],
188 |       [1744243200, 1],
189 |       [1744329600, 0],
190 |       [1744416000, 2],
191 |       [1744502400, 0]
192 |     ]
193 |   },
194 |   "participants": []
195 | }
196 | 
```

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

```typescript
  1 | import type { Issue } from "../../api-client";
  2 | import { getIssueUrl, getIssuesSearchUrl } from "../../utils/url-utils";
  3 | import * as Sentry from "@sentry/node";
  4 | 
  5 | /**
  6 |  * Format an explanation for how a natural language query was translated
  7 |  */
  8 | export function formatExplanation(explanation: string): string {
  9 |   return `## How I interpreted your query\n\n${explanation}`;
 10 | }
 11 | 
 12 | export interface FormatIssueResultsParams {
 13 |   issues: Issue[];
 14 |   organizationSlug: string;
 15 |   projectSlugOrId?: string;
 16 |   query?: string | null;
 17 |   regionUrl?: string;
 18 |   naturalLanguageQuery?: string;
 19 |   skipHeader?: boolean;
 20 | }
 21 | 
 22 | /**
 23 |  * Format issue search results for display
 24 |  */
 25 | export function formatIssueResults(params: FormatIssueResultsParams): string {
 26 |   const {
 27 |     issues,
 28 |     organizationSlug,
 29 |     projectSlugOrId,
 30 |     query,
 31 |     regionUrl,
 32 |     naturalLanguageQuery,
 33 |     skipHeader = false,
 34 |   } = params;
 35 | 
 36 |   const host = regionUrl ? new URL(regionUrl).host : "sentry.io";
 37 | 
 38 |   let output = "";
 39 | 
 40 |   // Skip header section if requested (when called from handler with includeExplanation)
 41 |   if (!skipHeader) {
 42 |     // Use natural language query in title if provided, otherwise fall back to org/project
 43 |     if (naturalLanguageQuery) {
 44 |       output = `# Search Results for "${naturalLanguageQuery}"\n\n`;
 45 |     } else {
 46 |       output = `# Issues in **${organizationSlug}`;
 47 |       if (projectSlugOrId) {
 48 |         output += `/${projectSlugOrId}`;
 49 |       }
 50 |       output += "**\n\n";
 51 |     }
 52 | 
 53 |     // Add display instructions for UI
 54 |     output += `⚠️ **IMPORTANT**: Display these issues as highlighted cards with status indicators, assignee info, and clickable Issue IDs.\n\n`;
 55 |   }
 56 | 
 57 |   if (issues.length === 0) {
 58 |     Sentry.logger.info(
 59 |       Sentry.logger
 60 |         .fmt`No issues found for query: ${naturalLanguageQuery || query}`,
 61 |       {
 62 |         query,
 63 |         organizationSlug,
 64 |         projectSlug: projectSlugOrId,
 65 |         naturalLanguageQuery,
 66 |       },
 67 |     );
 68 |     output += "No issues found matching your search criteria.\n\n";
 69 |     output += "Try adjusting your search criteria or time range.";
 70 |     return output;
 71 |   }
 72 | 
 73 |   // Generate search URL for viewing results
 74 |   const searchUrl = getIssuesSearchUrl(
 75 |     host,
 76 |     organizationSlug,
 77 |     query,
 78 |     projectSlugOrId,
 79 |   );
 80 | 
 81 |   // Add view link with emoji and guidance text (like search_events)
 82 |   output += `**View these results in Sentry**:\n${searchUrl}\n`;
 83 |   output += `_Please share this link with the user to view the search results in their Sentry dashboard._\n\n`;
 84 | 
 85 |   output += `Found **${issues.length}** issue${issues.length === 1 ? "" : "s"}:\n\n`;
 86 | 
 87 |   // Format each issue
 88 |   issues.forEach((issue, index) => {
 89 |     // Generate issue URL using the utility function
 90 |     const issueUrl = getIssueUrl(host, organizationSlug, issue.shortId);
 91 | 
 92 |     output += `## ${index + 1}. [${issue.shortId}](${issueUrl})\n\n`;
 93 |     output += `**${issue.title}**\n\n`;
 94 | 
 95 |     // Issue metadata
 96 |     // Issues don't have a level field in the API response
 97 |     output += `- **Status**: ${issue.status}\n`;
 98 |     output += `- **Users**: ${issue.userCount || 0}\n`;
 99 |     output += `- **Events**: ${issue.count || 0}\n`;
100 | 
101 |     if (issue.assignedTo) {
102 |       const assignee = issue.assignedTo;
103 |       if (typeof assignee === "string") {
104 |         output += `- **Assigned to**: ${assignee}\n`;
105 |       } else if (
106 |         assignee &&
107 |         typeof assignee === "object" &&
108 |         "name" in assignee
109 |       ) {
110 |         output += `- **Assigned to**: ${assignee.name}\n`;
111 |       }
112 |     }
113 | 
114 |     output += `- **First seen**: ${formatDate(issue.firstSeen)}\n`;
115 |     output += `- **Last seen**: ${formatDate(issue.lastSeen)}\n`;
116 | 
117 |     if (issue.culprit) {
118 |       output += `- **Culprit**: \`${issue.culprit}\`\n`;
119 |     }
120 | 
121 |     output += "\n";
122 |   });
123 | 
124 |   // Add next steps section (like search_events)
125 |   output += "## Next Steps\n\n";
126 |   output +=
127 |     "- Get more details about a specific issue: Use the Issue ID with get_issue_details\n";
128 |   output +=
129 |     "- Update issue status: Use update_issue to resolve or assign issues\n";
130 |   output +=
131 |     "- View event counts: Use search_events for aggregated statistics\n";
132 | 
133 |   return output;
134 | }
135 | 
136 | /**
137 |  * Format date for display
138 |  */
139 | function formatDate(dateString?: string | null): string {
140 |   if (!dateString) return "N/A";
141 | 
142 |   const date = new Date(dateString);
143 |   const now = new Date();
144 |   const diffMs = now.getTime() - date.getTime();
145 |   const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
146 | 
147 |   if (diffHours < 1) {
148 |     const diffMinutes = Math.floor(diffMs / (1000 * 60));
149 |     return `${diffMinutes} minute${diffMinutes === 1 ? "" : "s"} ago`;
150 |   }
151 |   if (diffHours < 24) {
152 |     return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`;
153 |   }
154 |   const diffDays = Math.floor(diffHours / 24);
155 |   if (diffDays < 30) {
156 |     return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
157 |   }
158 |   return date.toLocaleDateString();
159 | }
160 | 
```

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

```yaml
  1 | name: Deploy to Cloudflare
  2 | 
  3 | permissions:
  4 |   contents: read
  5 |   deployments: write
  6 |   checks: write
  7 | 
  8 | on:
  9 |   workflow_run:
 10 |     workflows: ["Test"]
 11 |     types:
 12 |       - completed
 13 |     branches: [main]
 14 |   workflow_dispatch:
 15 | 
 16 | jobs:
 17 |   deploy:
 18 |     name: Deploy to Cloudflare
 19 |     runs-on: ubuntu-latest
 20 |     if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
 21 | 
 22 |     steps:
 23 |       - uses: actions/checkout@v4
 24 | 
 25 |       - name: Setup Node.js
 26 |         uses: actions/setup-node@v4
 27 |         with:
 28 |           node-version: "20"
 29 | 
 30 |       # pnpm/action-setup@v4
 31 |       - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda
 32 |         name: Install pnpm
 33 |         with:
 34 |           run_install: false
 35 | 
 36 |       - name: Get pnpm store directory
 37 |         shell: bash
 38 |         run: |
 39 |           echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
 40 | 
 41 |       - uses: actions/cache@v4
 42 |         name: Setup pnpm cache
 43 |         with:
 44 |           path: ${{ env.STORE_PATH }}
 45 |           key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
 46 |           restore-keys: |
 47 |             ${{ runner.os }}-pnpm-store-
 48 | 
 49 |       - name: Install dependencies
 50 |         run: pnpm install
 51 | 
 52 |       # === BUILD AND DEPLOY CANARY WORKER ===
 53 |       - name: Build
 54 |         working-directory: packages/mcp-cloudflare
 55 |         run: pnpm build
 56 |         env:
 57 |           SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
 58 | 
 59 |       - name: Deploy to Canary Worker
 60 |         id: deploy_canary
 61 |         uses: cloudflare/wrangler-action@v3
 62 |         with:
 63 |           apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
 64 |           accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
 65 |           workingDirectory: packages/mcp-cloudflare
 66 |           command: deploy --config wrangler.canary.jsonc
 67 |           packageManager: pnpm
 68 | 
 69 |       - name: Wait for Canary to Propagate
 70 |         if: success()
 71 |         run: |
 72 |           echo "Waiting 30 seconds for canary deployment to propagate..."
 73 |           sleep 30
 74 | 
 75 |       # === SMOKE TEST CANARY ===
 76 |       - name: Run Smoke Tests on Canary
 77 |         id: canary_smoke_tests
 78 |         if: success()
 79 |         env:
 80 |           PREVIEW_URL: https://sentry-mcp-canary.getsentry.workers.dev
 81 |         run: |
 82 |           echo "Running smoke tests against canary worker..."
 83 |           cd packages/smoke-tests
 84 |           pnpm test:ci
 85 | 
 86 |       - name: Publish Canary Smoke Test Report
 87 |         uses: mikepenz/action-junit-report@cf701569b05ccdd861a76b8607a66d76f6fd4857
 88 |         if: always() && steps.canary_smoke_tests.outcome != 'skipped'
 89 |         with:
 90 |           report_paths: "packages/smoke-tests/tests.junit.xml"
 91 |           check_name: "Canary Smoke Test Results"
 92 |           fail_on_failure: false
 93 | 
 94 |       # === DEPLOY PRODUCTION WORKER (only if canary tests pass) ===
 95 |       - name: Deploy to Production Worker
 96 |         id: deploy_production
 97 |         if: steps.canary_smoke_tests.outcome == 'success'
 98 |         uses: cloudflare/wrangler-action@v3
 99 |         with:
100 |           apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
101 |           accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
102 |           workingDirectory: packages/mcp-cloudflare
103 |           command: deploy
104 |           packageManager: pnpm
105 | 
106 |       - name: Wait for Production to Propagate
107 |         if: steps.deploy_production.outcome == 'success'
108 |         run: |
109 |           echo "Waiting 30 seconds for production deployment to propagate..."
110 |           sleep 30
111 | 
112 |       # === SMOKE TEST PRODUCTION ===
113 |       - name: Run Smoke Tests on Production
114 |         id: production_smoke_tests
115 |         if: steps.deploy_production.outcome == 'success'
116 |         env:
117 |           PREVIEW_URL: https://mcp.sentry.dev
118 |         run: |
119 |           echo "Running smoke tests on production..."
120 |           cd packages/smoke-tests
121 |           pnpm test:ci
122 | 
123 |       - name: Publish Production Smoke Test Report
124 |         uses: mikepenz/action-junit-report@cf701569b05ccdd861a76b8607a66d76f6fd4857
125 |         if: always() && steps.production_smoke_tests.outcome != 'skipped'
126 |         with:
127 |           report_paths: "packages/smoke-tests/tests.junit.xml"
128 |           check_name: "Production Smoke Test Results"
129 |           fail_on_failure: false
130 | 
131 |       # === ROLLBACK IF PRODUCTION SMOKE TESTS FAIL ===
132 |       - name: Rollback Production on Smoke Test Failure
133 |         if: steps.production_smoke_tests.outcome == 'failure'
134 |         uses: cloudflare/wrangler-action@v3
135 |         with:
136 |           apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
137 |           accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
138 |           workingDirectory: packages/mcp-cloudflare
139 |           command: rollback
140 |           packageManager: pnpm
141 |         continue-on-error: true
142 | 
143 |       - name: Fail Job if Production Smoke Tests Failed
144 |         if: steps.production_smoke_tests.outcome == 'failure'
145 |         run: |
146 |           echo "Production smoke tests failed - job failed after rollback"
147 |           exit 1
148 | 
```

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

```typescript
  1 | /**
  2 |  * Type definitions for Chat components
  3 |  */
  4 | 
  5 | import type React from "react";
  6 | import type { Message } from "ai/react";
  7 | 
  8 | // Re-export AI SDK types for convenience
  9 | export type { Message } from "ai/react";
 10 | 
 11 | // Extended message type that includes our custom metadata
 12 | export interface ExtendedMessage extends Message {
 13 |   data?: {
 14 |     type?: string;
 15 |     prompts?: any[];
 16 |     toolsDetailed?: Array<{ name: string; description: string }>;
 17 |     hasSlashCommands?: boolean;
 18 |     error?: string;
 19 |     // Prompt execution data
 20 |     promptName?: string;
 21 |     parameters?: Record<string, any>;
 22 |     wasExecuted?: boolean;
 23 |     simulateStreaming?: boolean;
 24 |     [key: string]: any;
 25 |   };
 26 | }
 27 | 
 28 | // Error handling types (simplified)
 29 | // We only keep this for potential server response parsing
 30 | export interface ChatErrorData {
 31 |   error?: string;
 32 |   name?: string;
 33 |   eventId?: string;
 34 |   statusCode?: number;
 35 |   message?: string;
 36 | }
 37 | 
 38 | // Authentication types
 39 | export interface AuthState {
 40 |   isLoading: boolean;
 41 |   isAuthenticated: boolean;
 42 |   authToken: string;
 43 |   isAuthenticating: boolean;
 44 |   authError: string;
 45 | }
 46 | 
 47 | export interface AuthActions {
 48 |   handleOAuthLogin: () => void;
 49 |   handleLogout: () => void;
 50 |   clearAuthState: () => void;
 51 | }
 52 | 
 53 | export type AuthContextType = AuthState & AuthActions;
 54 | 
 55 | // OAuth message types
 56 | export interface OAuthSuccessMessage {
 57 |   type: "SENTRY_AUTH_SUCCESS";
 58 |   data: Record<string, never>;
 59 | }
 60 | 
 61 | export interface OAuthErrorMessage {
 62 |   type: "SENTRY_AUTH_ERROR";
 63 |   error?: string;
 64 | }
 65 | 
 66 | export type OAuthMessage = OAuthSuccessMessage | OAuthErrorMessage;
 67 | 
 68 | // Tool invocation types
 69 | export interface ToolInvocationContent {
 70 |   type: "text";
 71 |   text: string;
 72 | }
 73 | 
 74 | export interface ToolInvocationUnknownContent {
 75 |   type: string;
 76 |   [key: string]: unknown;
 77 | }
 78 | 
 79 | export type ToolMessage = ToolInvocationContent | ToolInvocationUnknownContent;
 80 | 
 81 | // Define our own ToolInvocation interface since AI SDK's is not properly exported
 82 | export interface ChatToolInvocation {
 83 |   toolCallId: string;
 84 |   toolName: string;
 85 |   args: Record<string, unknown>;
 86 |   state: "partial-call" | "call" | "result";
 87 |   result?: {
 88 |     content: ToolMessage[];
 89 |   };
 90 | }
 91 | 
 92 | // Message processing types
 93 | export interface ProcessedMessagePart {
 94 |   part: NonNullable<Message["parts"]>[number];
 95 |   messageId: string;
 96 |   messageRole: string;
 97 |   partIndex: number;
 98 |   isStreaming: boolean;
 99 | }
100 | 
101 | // Component prop types
102 | export interface ChatProps {
103 |   isOpen: boolean;
104 |   onClose: () => void;
105 |   onLogout: () => void;
106 | }
107 | 
108 | export interface ChatUIProps {
109 |   messages: Message[];
110 |   input: string;
111 |   error?: Error | null;
112 |   isChatLoading: boolean;
113 |   isOpen?: boolean;
114 |   showControls?: boolean;
115 |   onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
116 |   onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
117 |   onStop?: () => void;
118 |   onRetry?: () => void;
119 |   onClose?: () => void;
120 |   onLogout?: () => void;
121 |   onSlashCommand?: (command: string) => void;
122 |   onSendPrompt?: (prompt: string) => void;
123 | }
124 | 
125 | export interface ChatMessagesProps {
126 |   messages: Message[];
127 |   isChatLoading: boolean;
128 |   isLocalStreaming?: boolean;
129 |   isMessageStreaming?: (messageId: string) => boolean;
130 |   error?: Error | null;
131 |   onRetry?: () => void;
132 |   onSlashCommand?: (command: string) => void;
133 | }
134 | 
135 | export interface ChatInputProps {
136 |   input: string;
137 |   isLoading: boolean;
138 |   isOpen: boolean;
139 |   onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
140 |   onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
141 |   onStop: () => void;
142 | }
143 | 
144 | export interface AuthFormProps {
145 |   authError: string;
146 |   onOAuthLogin: () => void;
147 | }
148 | 
149 | export interface PanelBackdropProps {
150 |   isOpen: boolean;
151 |   onClose: () => void;
152 | }
153 | 
154 | export interface MessagePartProps {
155 |   part: NonNullable<Message["parts"]>[number];
156 |   messageId: string;
157 |   messageRole: string;
158 |   partIndex: number;
159 |   isStreaming?: boolean;
160 |   messageData?: any;
161 |   onSlashCommand?: (command: string) => void;
162 | }
163 | 
164 | export interface TextPartProps {
165 |   text: string;
166 |   role: string;
167 |   messageId: string;
168 |   isStreaming?: boolean;
169 |   messageData?: any;
170 |   onSlashCommand?: (command: string) => void;
171 | }
172 | 
173 | export interface ToolPartProps {
174 |   toolInvocation: ChatToolInvocation;
175 |   messageId: string;
176 |   partIndex: number;
177 | }
178 | 
179 | export interface ToolInvocationProps {
180 |   tool: ChatToolInvocation;
181 |   messageId: string;
182 |   index: number;
183 | }
184 | 
185 | // Type guards
186 | export function isTextMessage(
187 |   message: ToolMessage,
188 | ): message is ToolInvocationContent {
189 |   return message.type === "text";
190 | }
191 | 
192 | export function isOAuthSuccessMessage(
193 |   message: unknown,
194 | ): message is OAuthSuccessMessage {
195 |   return (
196 |     typeof message === "object" &&
197 |     message !== null &&
198 |     "type" in message &&
199 |     message.type === "SENTRY_AUTH_SUCCESS"
200 |   );
201 | }
202 | 
203 | export function isOAuthErrorMessage(
204 |   message: unknown,
205 | ): message is OAuthErrorMessage {
206 |   return (
207 |     typeof message === "object" &&
208 |     message !== null &&
209 |     "type" in message &&
210 |     message.type === "SENTRY_AUTH_ERROR"
211 |   );
212 | }
213 | 
```

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

```typescript
  1 | import { memo } from "react";
  2 | import { Markdown } from "../ui/markdown";
  3 | import { InteractiveMarkdown } from "../ui/interactive-markdown";
  4 | import { Typewriter } from "../ui/typewriter";
  5 | import { ToolInvocation } from "./tool-invocation";
  6 | import { Terminal } from "lucide-react";
  7 | import type {
  8 |   MessagePartProps,
  9 |   TextPartProps,
 10 |   ToolPartProps,
 11 |   ChatToolInvocation,
 12 | } from "./types";
 13 | 
 14 | // Component for rendering text parts
 15 | const TextPart = memo(function TextPart({
 16 |   text,
 17 |   role,
 18 |   messageId,
 19 |   isStreaming,
 20 |   messageData,
 21 |   onSlashCommand,
 22 | }: TextPartProps) {
 23 |   const isAssistant = role === "assistant";
 24 |   const isUser = role === "user";
 25 |   const isSlashCommand = isUser && text.startsWith("/");
 26 |   const isPromptExecution = isUser && messageData?.type === "prompt-execution";
 27 | 
 28 |   if (isUser) {
 29 |     // User messages: flexible width with background
 30 |     return (
 31 |       <div className="flex justify-end">
 32 |         <div
 33 |           className={`px-4 py-2 rounded max-w-3xl ${
 34 |             isSlashCommand
 35 |               ? "bg-blue-900/50 border border-blue-700/50"
 36 |               : isPromptExecution
 37 |                 ? "bg-purple-900/50 border border-purple-700/50"
 38 |                 : "bg-slate-800"
 39 |           }`}
 40 |         >
 41 |           {isSlashCommand ? (
 42 |             <div className="flex items-center gap-2">
 43 |               <Terminal className="h-4 w-4 text-blue-400" />
 44 |               <span className="text-blue-300 font-mono text-sm">{text}</span>
 45 |             </div>
 46 |           ) : isPromptExecution ? (
 47 |             <div className="space-y-2">
 48 |               <div className="flex items-center gap-2">
 49 |                 <Terminal className="h-4 w-4 text-purple-400" />
 50 |                 <span className="text-purple-300 font-semibold text-sm">
 51 |                   Prompt: {messageData.promptName}
 52 |                 </span>
 53 |               </div>
 54 |               {messageData.parameters &&
 55 |                 Object.keys(messageData.parameters).length > 0 && (
 56 |                   <div className="text-xs text-purple-200/80 ml-6">
 57 |                     {Object.entries(messageData.parameters).map(
 58 |                       ([key, value]) => (
 59 |                         <div key={key}>
 60 |                           <span className="text-purple-300">{key}:</span>{" "}
 61 |                           {String(value)}
 62 |                         </div>
 63 |                       ),
 64 |                     )}
 65 |                   </div>
 66 |                 )}
 67 |               {messageData.wasExecuted && (
 68 |                 <div className="text-xs text-purple-200/60 ml-6 italic">
 69 |                   ✓ Executed on server
 70 |                 </div>
 71 |               )}
 72 |             </div>
 73 |           ) : (
 74 |             <Markdown>{text}</Markdown>
 75 |           )}
 76 |         </div>
 77 |       </div>
 78 |     );
 79 |   }
 80 | 
 81 |   // Assistant and system messages: no background, just text
 82 |   // System messages should animate if they're marked for streaming simulation
 83 |   const shouldAnimate =
 84 |     (isAssistant && isStreaming) ||
 85 |     (role === "system" && isStreaming && messageData?.simulateStreaming);
 86 |   const hasSlashCommands = messageData?.hasSlashCommands;
 87 | 
 88 |   return (
 89 |     <div className="mr-8">
 90 |       {shouldAnimate ? (
 91 |         <Typewriter text={text} speed={20}>
 92 |           {(displayedText) => (
 93 |             <InteractiveMarkdown
 94 |               hasSlashCommands={hasSlashCommands}
 95 |               onSlashCommand={onSlashCommand}
 96 |             >
 97 |               {displayedText}
 98 |             </InteractiveMarkdown>
 99 |           )}
100 |         </Typewriter>
101 |       ) : (
102 |         <InteractiveMarkdown
103 |           hasSlashCommands={hasSlashCommands}
104 |           onSlashCommand={onSlashCommand}
105 |         >
106 |           {text}
107 |         </InteractiveMarkdown>
108 |       )}
109 |     </div>
110 |   );
111 | });
112 | 
113 | // Component for rendering tool invocation parts
114 | const ToolPart = memo(function ToolPart({
115 |   toolInvocation,
116 |   messageId,
117 |   partIndex,
118 | }: ToolPartProps) {
119 |   return (
120 |     <div className="mr-8">
121 |       <ToolInvocation
122 |         tool={toolInvocation}
123 |         messageId={messageId}
124 |         index={partIndex}
125 |       />
126 |     </div>
127 |   );
128 | });
129 | 
130 | // Main component for rendering individual message parts
131 | const MessagePart = memo(function MessagePart({
132 |   part,
133 |   messageId,
134 |   messageRole,
135 |   partIndex,
136 |   isStreaming,
137 |   messageData,
138 |   onSlashCommand,
139 | }: MessagePartProps) {
140 |   switch (part.type) {
141 |     case "text":
142 |       return (
143 |         <TextPart
144 |           text={part.text}
145 |           role={messageRole}
146 |           messageId={messageId}
147 |           isStreaming={isStreaming}
148 |           messageData={messageData}
149 |           onSlashCommand={onSlashCommand}
150 |         />
151 |       );
152 |     case "tool-invocation":
153 |       return (
154 |         <ToolPart
155 |           toolInvocation={part.toolInvocation as ChatToolInvocation}
156 |           messageId={messageId}
157 |           partIndex={partIndex}
158 |         />
159 |       );
160 |     default:
161 |       // Fallback for unknown part types
162 |       return null;
163 |   }
164 | });
165 | 
166 | // Export the memoized components
167 | export { TextPart, ToolPart, MessagePart };
168 | 
```

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

```markdown
  1 | # Cloudflare Chat Agent Architecture
  2 | 
  3 | Technical architecture for the web-based chat interface hosted on Cloudflare Workers.
  4 | 
  5 | ## Overview
  6 | 
  7 | 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:
  8 | 
  9 | - **Frontend**: React with Tailwind CSS
 10 | - **Backend**: Cloudflare Workers with Hono framework
 11 | - **AI**: OpenAI GPT-4 via Vercel AI SDK
 12 | - **MCP Integration**: HTTP transport to core MCP server
 13 | 
 14 | ## Package Structure
 15 | 
 16 | ```
 17 | packages/mcp-cloudflare/
 18 | ├── src/
 19 | │   ├── client/           # React frontend
 20 | │   │   ├── components/   # UI components
 21 | │   │   ├── contexts/     # React contexts
 22 | │   │   ├── hooks/        # Custom React hooks
 23 | │   │   └── utils/        # Client utilities
 24 | │   └── server/           # Cloudflare Workers backend
 25 | │       ├── lib/          # Server libraries
 26 | │       ├── routes/       # API routes
 27 | │       ├── types/        # TypeScript types
 28 | │       └── utils/        # Server utilities
 29 | ├── public/               # Static assets
 30 | └── wrangler.toml         # Cloudflare configuration
 31 | ```
 32 | 
 33 | ## Key Components
 34 | 
 35 | ### 1. OAuth Authentication
 36 | 
 37 | Handles Sentry OAuth flow for user authentication:
 38 | 
 39 | ```typescript
 40 | // server/routes/auth.ts
 41 | export default new Hono()
 42 |   .get("/login", handleOAuthLogin)
 43 |   .get("/callback", handleOAuthCallback)
 44 |   .post("/logout", handleLogout);
 45 | ```
 46 | 
 47 | **Features:**
 48 | - OAuth 2.0 flow with Sentry
 49 | - Token storage in Cloudflare KV
 50 | - Automatic token refresh
 51 | - Per-organization access control
 52 | 
 53 | ### 2. Chat Interface
 54 | 
 55 | React-based chat UI with real-time streaming:
 56 | 
 57 | ```typescript
 58 | // client/components/chat/chat.tsx
 59 | export function Chat() {
 60 |   const { messages, handleSubmit } = useChat({
 61 |     api: "/api/chat",
 62 |     headers: { Authorization: `Bearer ${authToken}` }
 63 |   });
 64 | }
 65 | ```
 66 | 
 67 | **Features:**
 68 | - Message streaming with Vercel AI SDK
 69 | - Tool call visualization
 70 | - Slash commands (/help, /prompts, /clear)
 71 | - Prompt parameter dialogs
 72 | - Markdown rendering with syntax highlighting
 73 | 
 74 | ### 3. MCP Integration
 75 | 
 76 | Connects to the core MCP server via HTTP transport:
 77 | 
 78 | ```typescript
 79 | // server/routes/chat.ts
 80 | const mcpClient = await experimental_createMCPClient({
 81 |   name: "sentry",
 82 |   transport: {
 83 |     type: "sse",
 84 |     url: sseUrl,
 85 |     headers: { Authorization: `Bearer ${accessToken}` }
 86 |   }
 87 | });
 88 | ```
 89 | 
 90 | **Features:**
 91 | - Server-sent events (SSE) for MCP communication
 92 | - Automatic tool discovery
 93 | - Prompt metadata endpoint
 94 | - Error handling with fallbacks
 95 | 
 96 | ### 4. AI Assistant
 97 | 
 98 | GPT-4 integration with Sentry-specific system prompt:
 99 | 
100 | ```typescript
101 | const result = streamText({
102 |   model: openai("gpt-4o"),
103 |   messages: processedMessages,
104 |   tools: mcpTools,
105 |   system: "You are an AI assistant for testing Sentry MCP..."
106 | });
107 | ```
108 | 
109 | **Features:**
110 | - Streaming responses
111 | - Tool execution
112 | - Prompt template processing
113 | - Context-aware assistance
114 | 
115 | ## Data Flow
116 | 
117 | 1. **User Authentication**:
118 |    ```
119 |    User → OAuth Login → Sentry → OAuth Callback → KV Storage
120 |    ```
121 | 
122 | 2. **Chat Message Flow**:
123 |    ```
124 |    User Input → Chat API → Process Prompts → AI Model → Stream Response
125 |                          ↓
126 |                     MCP Server ← Tool Calls
127 |    ```
128 | 
129 | 3. **MCP Communication**:
130 |    ```
131 |    Chat Server → SSE Transport → MCP Server → Sentry API
132 |    ```
133 | 
134 | ## Deployment Architecture
135 | 
136 | ### Cloudflare Resources
137 | 
138 | - **Workers**: Serverless compute for API routes
139 | - **Pages**: Static asset hosting for React app
140 | - **KV Namespace**: OAuth token storage
141 | - **AI Binding**: Access to Cloudflare AI models (AutoRAG for docs search)
142 | - **R2**: File storage (future)
143 | 
144 | ### Environment Variables
145 | 
146 | Required for deployment:
147 | 
148 | ```toml
149 | [vars]
150 | COOKIE_SECRET = "..."      # For session encryption
151 | OPENAI_API_KEY = "..."     # For GPT-4 access
152 | SENTRY_CLIENT_ID = "..."   # OAuth app ID
153 | SENTRY_CLIENT_SECRET = "..." # OAuth app secret
154 | ```
155 | 
156 | ### API Routes
157 | 
158 | - `/api/auth/*` - Authentication endpoints
159 | - `/api/chat` - Main chat endpoint
160 | - `/api/metadata` - MCP metadata endpoint
161 | - `/sse` - Server-sent events for MCP
162 | 
163 | ## Security Considerations
164 | 
165 | 1. **Authentication**: OAuth tokens stored encrypted in KV
166 | 2. **Authorization**: Per-organization access control
167 | 3. **Rate Limiting**: Cloudflare rate limiter integration
168 | 4. **CORS**: Restricted to same-origin requests
169 | 5. **CSP**: Content Security Policy headers
170 | 
171 | ## Performance Optimizations
172 | 
173 | 1. **Edge Computing**: Runs at Cloudflare edge locations
174 | 2. **Caching**: Metadata endpoint with cache headers
175 | 3. **Streaming**: Server-sent events for real-time updates
176 | 4. **Bundle Splitting**: Optimized React build
177 | 
178 | ## Monitoring
179 | 
180 | - Sentry integration for error tracking
181 | - Cloudflare Analytics for usage metrics
182 | - Custom telemetry for MCP operations
183 | 
184 | ## Related Documentation
185 | 
186 | - See "OAuth Architecture" in @docs/cloudflare/oauth-architecture.md
187 | - See "Chat Interface" in @docs/cloudflare/architecture.md
188 | - See "Deployment" in @docs/cloudflare/deployment.md
189 | - See "Architecture" in @docs/architecture.mdc
190 | 
```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/find-releases.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import { setTag } from "@sentry/core";
  3 | import { defineTool } from "../internal/tool-helpers/define";
  4 | import { apiServiceFromContext } from "../internal/tool-helpers/api";
  5 | import type { ServerContext } from "../types";
  6 | import {
  7 |   ParamOrganizationSlug,
  8 |   ParamRegionUrl,
  9 |   ParamProjectSlugOrAll,
 10 | } from "../schema";
 11 | 
 12 | export default defineTool({
 13 |   name: "find_releases",
 14 |   requiredScopes: ["project:read"],
 15 |   description: [
 16 |     "Find releases in Sentry.",
 17 |     "",
 18 |     "Use this tool when you need to:",
 19 |     "- Find recent releases in a Sentry organization",
 20 |     "- Find the most recent version released of a specific project",
 21 |     "- Determine when a release was deployed to an environment",
 22 |     "",
 23 |     "<examples>",
 24 |     "### Find the most recent releases in the 'my-organization' organization",
 25 |     "",
 26 |     "```",
 27 |     "find_releases(organizationSlug='my-organization')",
 28 |     "```",
 29 |     "",
 30 |     "### Find releases matching '2ce6a27' in the 'my-organization' organization",
 31 |     "",
 32 |     "```",
 33 |     "find_releases(organizationSlug='my-organization', query='2ce6a27')",
 34 |     "```",
 35 |     "</examples>",
 36 |     "",
 37 |     "<hints>",
 38 |     "- If the user passes a parameter in the form of name/otherName, its likely in the format of <organizationSlug>/<projectSlug>.",
 39 |     "</hints>",
 40 |   ].join("\n"),
 41 |   inputSchema: {
 42 |     organizationSlug: ParamOrganizationSlug,
 43 |     regionUrl: ParamRegionUrl.optional(),
 44 |     projectSlug: ParamProjectSlugOrAll.optional(),
 45 |     query: z
 46 |       .string()
 47 |       .trim()
 48 |       .describe("Search for versions which contain the provided string.")
 49 |       .optional(),
 50 |   },
 51 |   annotations: {
 52 |     readOnlyHint: true,
 53 |     openWorldHint: true,
 54 |   },
 55 |   async handler(params, context: ServerContext) {
 56 |     const apiService = apiServiceFromContext(context, {
 57 |       regionUrl: params.regionUrl,
 58 |     });
 59 |     const organizationSlug = params.organizationSlug;
 60 | 
 61 |     setTag("organization.slug", organizationSlug);
 62 | 
 63 |     const releases = await apiService.listReleases({
 64 |       organizationSlug,
 65 |       projectSlug: params.projectSlug,
 66 |       query: params.query,
 67 |     });
 68 |     let output = `# Releases in **${organizationSlug}${params.projectSlug ? `/${params.projectSlug}` : ""}**\n\n`;
 69 |     if (releases.length === 0) {
 70 |       output += "No releases found.\n";
 71 |       return output;
 72 |     }
 73 |     output += releases
 74 |       .map((release) => {
 75 |         const releaseInfo = [
 76 |           `## ${release.shortVersion}`,
 77 |           "",
 78 |           `**Created**: ${new Date(release.dateCreated).toISOString()}`,
 79 |         ];
 80 |         if (release.dateReleased) {
 81 |           releaseInfo.push(
 82 |             `**Released**: ${new Date(release.dateReleased).toISOString()}`,
 83 |           );
 84 |         }
 85 |         if (release.firstEvent) {
 86 |           releaseInfo.push(
 87 |             `**First Event**: ${new Date(release.firstEvent).toISOString()}`,
 88 |           );
 89 |         }
 90 |         if (release.lastEvent) {
 91 |           releaseInfo.push(
 92 |             `**Last Event**: ${new Date(release.lastEvent).toISOString()}`,
 93 |           );
 94 |         }
 95 |         if (release.newGroups !== undefined) {
 96 |           releaseInfo.push(`**New Issues**: ${release.newGroups}`);
 97 |         }
 98 |         if (release.projects && release.projects.length > 0) {
 99 |           releaseInfo.push(
100 |             `**Projects**: ${release.projects.map((p) => p.name).join(", ")}`,
101 |           );
102 |         }
103 |         if (release.lastCommit) {
104 |           releaseInfo.push("", `### Last Commit`, "");
105 |           releaseInfo.push(`**Commit ID**: ${release.lastCommit.id}`);
106 |           releaseInfo.push(`**Commit Message**: ${release.lastCommit.message}`);
107 |           releaseInfo.push(
108 |             `**Commit Author**: ${release.lastCommit.author.name}`,
109 |           );
110 |           releaseInfo.push(
111 |             `**Commit Date**: ${new Date(release.lastCommit.dateCreated).toISOString()}`,
112 |           );
113 |         }
114 |         if (release.lastDeploy) {
115 |           releaseInfo.push("", `### Last Deploy`, "");
116 |           releaseInfo.push(`**Deploy ID**: ${release.lastDeploy.id}`);
117 |           releaseInfo.push(
118 |             `**Environment**: ${release.lastDeploy.environment}`,
119 |           );
120 |           if (release.lastDeploy.dateStarted) {
121 |             releaseInfo.push(
122 |               `**Deploy Started**: ${new Date(release.lastDeploy.dateStarted).toISOString()}`,
123 |             );
124 |           }
125 |           if (release.lastDeploy.dateFinished) {
126 |             releaseInfo.push(
127 |               `**Deploy Finished**: ${new Date(release.lastDeploy.dateFinished).toISOString()}`,
128 |             );
129 |           }
130 |         }
131 |         return releaseInfo.join("\n");
132 |       })
133 |       .join("\n\n");
134 |     output += "\n\n";
135 |     output += "# Using this information\n\n";
136 |     output += `- You can reference the Release version in commit messages or documentation.\n`;
137 |     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`;
138 |     return output;
139 |   },
140 | });
141 | 
```

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

```typescript
  1 | /**
  2 |  * Reusable chat UI component
  3 |  * Extracts the common chat interface used in both mobile and desktop views
  4 |  */
  5 | 
  6 | import { LogOut, X, Bot, Sparkles } from "lucide-react";
  7 | import ScrollToBottom from "react-scroll-to-bottom";
  8 | import { Button } from "../ui/button";
  9 | import { ChatInput, ChatMessages } from ".";
 10 | import type { Message } from "ai/react";
 11 | import type { EndpointMode } from "../../hooks/use-endpoint-mode";
 12 | 
 13 | // Constant empty function to avoid creating new instances on every render
 14 | const EMPTY_FUNCTION = () => {};
 15 | 
 16 | // Sample prompts for quick access
 17 | const SAMPLE_PROMPTS = [
 18 |   {
 19 |     label: "Help",
 20 |     prompt: "/help",
 21 |   },
 22 |   {
 23 |     label: "React SDK Usage",
 24 |     prompt: "Show me how to set up the React SDK for error monitoring",
 25 |   },
 26 |   {
 27 |     label: "Recent Issues",
 28 |     prompt: "What are my most recent issues?",
 29 |   },
 30 | ] as const;
 31 | 
 32 | interface ChatUIProps {
 33 |   messages: Message[];
 34 |   input: string;
 35 |   error?: Error | null;
 36 |   isChatLoading: boolean;
 37 |   isLocalStreaming?: boolean;
 38 |   isMessageStreaming?: (messageId: string) => boolean;
 39 |   isOpen?: boolean;
 40 |   showControls?: boolean;
 41 |   endpointMode?: EndpointMode;
 42 |   onInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
 43 |   onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
 44 |   onStop?: () => void;
 45 |   onRetry?: () => void;
 46 |   onClose?: () => void;
 47 |   onLogout?: () => void;
 48 |   onSlashCommand?: (command: string) => void;
 49 |   onSendPrompt?: (prompt: string) => void;
 50 |   onToggleEndpointMode?: () => void;
 51 | }
 52 | 
 53 | export const ChatUI = ({
 54 |   messages,
 55 |   input,
 56 |   error,
 57 |   isChatLoading,
 58 |   isLocalStreaming,
 59 |   isMessageStreaming,
 60 |   isOpen = true,
 61 |   showControls = false,
 62 |   endpointMode = "standard",
 63 |   onInputChange,
 64 |   onSubmit,
 65 |   onStop,
 66 |   onRetry,
 67 |   onClose,
 68 |   onLogout,
 69 |   onSlashCommand,
 70 |   onSendPrompt,
 71 |   onToggleEndpointMode,
 72 | }: ChatUIProps) => {
 73 |   const isAgentMode = endpointMode === "agent";
 74 | 
 75 |   return (
 76 |     <div className="h-full flex flex-col relative">
 77 |       {/* Floating Agent Mode Toggle - Top Right */}
 78 |       {onToggleEndpointMode && (
 79 |         <div className="absolute top-4 right-4 z-20">
 80 |           <Button
 81 |             type="button"
 82 |             onClick={onToggleEndpointMode}
 83 |             size="sm"
 84 |             variant={isAgentMode ? "default" : "outline"}
 85 |             title={
 86 |               isAgentMode
 87 |                 ? "Agent mode: Only use_sentry tool (click to switch to standard)"
 88 |                 : "Standard mode: All 19 tools available (click to switch to agent)"
 89 |             }
 90 |             className="shadow-lg"
 91 |           >
 92 |             {isAgentMode ? (
 93 |               <>
 94 |                 <Sparkles className="h-4 w-4 mr-2" />
 95 |                 Agent Mode
 96 |               </>
 97 |             ) : (
 98 |               <>
 99 |                 <Bot className="h-4 w-4 mr-2" />
100 |                 Standard Mode
101 |               </>
102 |             )}
103 |           </Button>
104 |         </div>
105 |       )}
106 | 
107 |       {/* Mobile header with close and logout buttons */}
108 |       <div className="md:hidden flex items-center justify-between p-4 border-b border-slate-800 flex-shrink-0">
109 |         {showControls && (
110 |           <>
111 |             <Button type="button" onClick={onClose} size="icon" title="Close">
112 |               <X className="h-4 w-4" />
113 |             </Button>
114 | 
115 |             <Button type="button" onClick={onLogout} size="icon" title="Logout">
116 |               <LogOut className="h-4 w-4" />
117 |             </Button>
118 |           </>
119 |         )}
120 |       </div>
121 | 
122 |       {/* Chat Messages - Scrollable area */}
123 |       <ScrollToBottom
124 |         className="flex-1 mb-34 flex overflow-y-auto"
125 |         scrollViewClassName="px-0"
126 |         followButtonClassName="hidden"
127 |         initialScrollBehavior="smooth"
128 |       >
129 |         <ChatMessages
130 |           messages={messages}
131 |           isChatLoading={isChatLoading}
132 |           isLocalStreaming={isLocalStreaming}
133 |           isMessageStreaming={isMessageStreaming}
134 |           error={error}
135 |           onRetry={onRetry}
136 |           onSlashCommand={onSlashCommand}
137 |         />
138 |       </ScrollToBottom>
139 | 
140 |       {/* Chat Input - Always pinned at bottom */}
141 |       <div className="py-4 px-6 bottom-0 left-0 right-0 absolute bg-slate-950/95 h-34 overflow-hidden z-10">
142 |         {/* Sample Prompt Buttons - Always visible above input */}
143 |         {onSendPrompt && (
144 |           <div className="mb-4 flex flex-wrap gap-2 justify-center">
145 |             {SAMPLE_PROMPTS.map((samplePrompt) => (
146 |               <Button
147 |                 key={samplePrompt.label}
148 |                 type="button"
149 |                 onClick={() => onSendPrompt(samplePrompt.prompt)}
150 |                 size="sm"
151 |                 variant="outline"
152 |               >
153 |                 {samplePrompt.label}
154 |               </Button>
155 |             ))}
156 |           </div>
157 |         )}
158 | 
159 |         <ChatInput
160 |           input={input}
161 |           isLoading={isChatLoading}
162 |           isOpen={isOpen}
163 |           onInputChange={onInputChange}
164 |           onSubmit={onSubmit}
165 |           onStop={onStop || EMPTY_FUNCTION}
166 |           onSlashCommand={onSlashCommand}
167 |         />
168 |       </div>
169 |     </div>
170 |   );
171 | };
172 | 
```

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

```json
  1 | [
  2 |   {
  3 |     "children": [
  4 |       {
  5 |         "children": [],
  6 |         "errors": [],
  7 |         "occurrences": [],
  8 |         "event_id": "aa8e7f3384ef4ff5850ba966b29ed10d",
  9 |         "transaction_id": "aa8e7f3384ef4ff5850ba966b29ed10d",
 10 |         "project_id": 4509109107622913,
 11 |         "project_slug": "mcp-server",
 12 |         "profile_id": "",
 13 |         "profiler_id": "",
 14 |         "parent_span_id": "a4d1aae7216b47ff",
 15 |         "start_timestamp": 1713805458.405616,
 16 |         "end_timestamp": 1713805463.608875,
 17 |         "measurements": {},
 18 |         "description": "POST https://api.openai.com/v1/chat/completions",
 19 |         "duration": 1708,
 20 |         "trace": "b4d1aae7216b47ff8117cf4e09ce9d0b",
 21 |         "span_id": "ad0f7c48fb294de3",
 22 |         "organization": null,
 23 |         "op": "http.client",
 24 |         "hash": "4ed30c7c-4fae-4c79-b2f1-be95c24e7b04",
 25 |         "exclusive_time": 1708,
 26 |         "status": null,
 27 |         "is_segment": true,
 28 |         "sdk_name": "sentry.javascript.bun",
 29 |         "same_process_as_parent": true,
 30 |         "tags": {
 31 |           "http.method": "POST",
 32 |           "http.status_code": "200",
 33 |           "server_name": "mcp-server"
 34 |         },
 35 |         "timestamp": 1713805463.608875,
 36 |         "data": {}
 37 |       }
 38 |     ],
 39 |     "errors": [],
 40 |     "occurrences": [],
 41 |     "event_id": "aa8e7f3384ef4ff5850ba966b29ed10d",
 42 |     "transaction_id": "aa8e7f3384ef4ff5850ba966b29ed10d",
 43 |     "project_id": 4509109107622913,
 44 |     "project_slug": "mcp-server",
 45 |     "profile_id": "",
 46 |     "profiler_id": "",
 47 |     "parent_span_id": null,
 48 |     "start_timestamp": 1713805458.405616,
 49 |     "end_timestamp": 1713805463.608875,
 50 |     "measurements": {},
 51 |     "description": "tools/call search_events",
 52 |     "duration": 5203,
 53 |     "trace": "b4d1aae7216b47ff8117cf4e09ce9d0b",
 54 |     "span_id": "aa8e7f3384ef4ff5",
 55 |     "organization": null,
 56 |     "op": "function",
 57 |     "hash": "4ed30c7c-4fae-4c79-b2f1-be95c24e7b04",
 58 |     "exclusive_time": 3495,
 59 |     "status": null,
 60 |     "is_segment": true,
 61 |     "sdk_name": "sentry.javascript.bun",
 62 |     "same_process_as_parent": true,
 63 |     "tags": {
 64 |       "ai.input_messages": "1",
 65 |       "ai.model_id": "gpt-4o-2024-08-06",
 66 |       "ai.pipeline.name": "search_events",
 67 |       "ai.response.finish_reason": "stop",
 68 |       "ai.streaming": "false",
 69 |       "ai.total_tokens.used": "435",
 70 |       "server_name": "mcp-server"
 71 |     },
 72 |     "timestamp": 1713805463.608875,
 73 |     "data": {}
 74 |   },
 75 |   {
 76 |     "id": 6507376925,
 77 |     "issue_id": 6507376925,
 78 |     "project_id": 4509109107622913,
 79 |     "project_slug": "mcp-server",
 80 |     "title": "Error: Standalone issue not associated with spans",
 81 |     "culprit": "standalone-error.js:42",
 82 |     "type": "error",
 83 |     "timestamp": 1713805460.123456
 84 |   },
 85 |   {
 86 |     "children": [
 87 |       {
 88 |         "children": [],
 89 |         "errors": [],
 90 |         "occurrences": [],
 91 |         "event_id": "b4abfe5ed7984c2b",
 92 |         "transaction_id": "b4abfe5ed7984c2b",
 93 |         "project_id": 4509109107622913,
 94 |         "project_slug": "mcp-server",
 95 |         "profile_id": "",
 96 |         "profiler_id": "",
 97 |         "parent_span_id": "b4abfe5ed7984c2b",
 98 |         "start_timestamp": 1713805461.126859,
 99 |         "end_timestamp": 1713805462.534782,
100 |         "measurements": {},
101 |         "description": "/api/0/organizations/{organization_id_or_slug}/events/",
102 |         "duration": 1408,
103 |         "trace": "b4d1aae7216b47ff8117cf4e09ce9d0b",
104 |         "span_id": "99a97a1d42c3489a",
105 |         "organization": null,
106 |         "op": "http.server",
107 |         "hash": "another-hash-here",
108 |         "exclusive_time": 1408,
109 |         "status": "ok",
110 |         "is_segment": true,
111 |         "sdk_name": "sentry.python",
112 |         "same_process_as_parent": false,
113 |         "tags": {
114 |           "http.method": "GET",
115 |           "http.status_code": "200"
116 |         },
117 |         "timestamp": 1713805462.534782,
118 |         "data": {}
119 |       }
120 |     ],
121 |     "errors": [],
122 |     "occurrences": [],
123 |     "event_id": "b4abfe5ed7984c2b",
124 |     "transaction_id": "b4abfe5ed7984c2b",
125 |     "project_id": 4509109107622913,
126 |     "project_slug": "mcp-server",
127 |     "profile_id": "",
128 |     "profiler_id": "",
129 |     "parent_span_id": "aa8e7f3384ef4ff5",
130 |     "start_timestamp": 1713805461.126859,
131 |     "end_timestamp": 1713805462.608782,
132 |     "measurements": {},
133 |     "description": "GET https://us.sentry.io/api/0/organizations/example-org/events/",
134 |     "duration": 1482,
135 |     "trace": "b4d1aae7216b47ff8117cf4e09ce9d0b",
136 |     "span_id": "b4abfe5ed7984c2b",
137 |     "organization": null,
138 |     "op": "http.client",
139 |     "hash": "yet-another-hash",
140 |     "exclusive_time": 74,
141 |     "status": "ok",
142 |     "is_segment": true,
143 |     "sdk_name": "sentry.javascript.bun",
144 |     "same_process_as_parent": true,
145 |     "tags": {
146 |       "http.method": "GET",
147 |       "http.status_code": "200",
148 |       "server_name": "mcp-server"
149 |     },
150 |     "timestamp": 1713805462.608782,
151 |     "data": {}
152 |   },
153 |   {
154 |     "id": 6507376926,
155 |     "issue_id": 6507376926,
156 |     "project_id": 4509109107622913,
157 |     "project_slug": "mcp-server",
158 |     "title": "TypeError: Cannot read property 'span' of undefined",
159 |     "culprit": "trace-processor.js:156",
160 |     "type": "error",
161 |     "timestamp": 1713805462.234567
162 |   }
163 | ]
164 | 
```

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

```json
  1 | {
  2 |   "namespace": "cicd",
  3 |   "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",
  4 |   "attributes": {
  5 |     "cicd.pipeline.name": {
  6 |       "description": "The human readable name of the pipeline within a CI/CD system.\n",
  7 |       "type": "string",
  8 |       "stability": "development",
  9 |       "examples": [
 10 |         "Build and Test",
 11 |         "Lint",
 12 |         "Deploy Go Project",
 13 |         "deploy_to_environment"
 14 |       ]
 15 |     },
 16 |     "cicd.pipeline.run.id": {
 17 |       "description": "The unique identifier of a pipeline run within a CI/CD system.\n",
 18 |       "type": "string",
 19 |       "stability": "development",
 20 |       "examples": ["120912"]
 21 |     },
 22 |     "cicd.pipeline.run.url.full": {
 23 |       "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",
 24 |       "type": "string",
 25 |       "stability": "development",
 26 |       "examples": [
 27 |         "https://github.com/open-telemetry/semantic-conventions/actions/runs/9753949763?pr=1075"
 28 |       ]
 29 |     },
 30 |     "cicd.pipeline.run.state": {
 31 |       "description": "The pipeline run goes through these states during its lifecycle.\n",
 32 |       "type": "string",
 33 |       "stability": "development",
 34 |       "examples": ["pending", "executing", "finalizing"]
 35 |     },
 36 |     "cicd.pipeline.task.name": {
 37 |       "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",
 38 |       "type": "string",
 39 |       "stability": "development",
 40 |       "examples": ["Run GoLang Linter", "Go Build", "go-test", "deploy_binary"]
 41 |     },
 42 |     "cicd.pipeline.task.run.id": {
 43 |       "description": "The unique identifier of a task run within a pipeline.\n",
 44 |       "type": "string",
 45 |       "stability": "development",
 46 |       "examples": ["12097"]
 47 |     },
 48 |     "cicd.pipeline.task.run.url.full": {
 49 |       "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",
 50 |       "type": "string",
 51 |       "stability": "development",
 52 |       "examples": [
 53 |         "https://github.com/open-telemetry/semantic-conventions/actions/runs/9753949763/job/26920038674?pr=1075"
 54 |       ]
 55 |     },
 56 |     "cicd.pipeline.task.run.result": {
 57 |       "description": "The result of a task run.\n",
 58 |       "type": "string",
 59 |       "stability": "development",
 60 |       "examples": [
 61 |         "success",
 62 |         "failure",
 63 |         "error",
 64 |         "timeout",
 65 |         "cancellation",
 66 |         "skip"
 67 |       ]
 68 |     },
 69 |     "cicd.pipeline.task.type": {
 70 |       "description": "The type of the task within a pipeline.\n",
 71 |       "type": "string",
 72 |       "stability": "development",
 73 |       "examples": ["build", "test", "deploy"]
 74 |     },
 75 |     "cicd.pipeline.result": {
 76 |       "description": "The result of a pipeline run.\n",
 77 |       "type": "string",
 78 |       "stability": "development",
 79 |       "examples": [
 80 |         "success",
 81 |         "failure",
 82 |         "error",
 83 |         "timeout",
 84 |         "cancellation",
 85 |         "skip"
 86 |       ]
 87 |     },
 88 |     "cicd.pipeline.action.name": {
 89 |       "description": "The kind of action a pipeline run is performing.\n",
 90 |       "type": "string",
 91 |       "stability": "development",
 92 |       "examples": ["BUILD", "RUN", "SYNC"]
 93 |     },
 94 |     "cicd.worker.id": {
 95 |       "description": "The unique identifier of a worker within a CICD system.",
 96 |       "type": "string",
 97 |       "stability": "development",
 98 |       "examples": ["abc123", "10.0.1.2", "controller"]
 99 |     },
100 |     "cicd.worker.name": {
101 |       "description": "The name of a worker within a CICD system.",
102 |       "type": "string",
103 |       "stability": "development",
104 |       "examples": ["agent-abc", "controller", "Ubuntu LTS"]
105 |     },
106 |     "cicd.worker.url.full": {
107 |       "description": "The [URL](https://wikipedia.org/wiki/URL) of the worker, providing the complete address in order to locate and identify the worker.",
108 |       "type": "string",
109 |       "stability": "development",
110 |       "examples": ["https://cicd.example.org/worker/abc123"]
111 |     },
112 |     "cicd.worker.state": {
113 |       "description": "The state of a CICD worker / agent.\n",
114 |       "type": "string",
115 |       "stability": "development",
116 |       "examples": ["available", "busy", "offline"]
117 |     },
118 |     "cicd.system.component": {
119 |       "description": "The name of a component of the CICD system.",
120 |       "type": "string",
121 |       "stability": "development",
122 |       "examples": ["controller", "scheduler", "agent"]
123 |     }
124 |   }
125 | }
126 | 
```

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

```typescript
  1 | import { promises as fs } from "node:fs";
  2 | import { join } from "node:path";
  3 | import { homedir } from "node:os";
  4 | 
  5 | export interface OAuthClientConfig {
  6 |   clientId: string;
  7 |   mcpHost: string;
  8 |   registeredAt: string;
  9 |   accessToken?: string;
 10 |   tokenExpiresAt?: string;
 11 | }
 12 | 
 13 | export interface ClientConfigFile {
 14 |   oauthClients: Record<string, OAuthClientConfig>;
 15 | }
 16 | 
 17 | export class ConfigManager {
 18 |   private configDir: string;
 19 |   private configFile: string;
 20 | 
 21 |   constructor() {
 22 |     this.configDir = join(homedir(), ".config", "sentry-mcp");
 23 |     this.configFile = join(this.configDir, "config.json");
 24 |   }
 25 | 
 26 |   /**
 27 |    * Ensure config directory exists
 28 |    */
 29 |   private async ensureConfigDir(): Promise<void> {
 30 |     try {
 31 |       await fs.mkdir(this.configDir, { recursive: true });
 32 |     } catch (error) {
 33 |       // Directory might already exist, ignore EEXIST errors
 34 |       if ((error as any).code !== "EEXIST") {
 35 |         throw error;
 36 |       }
 37 |     }
 38 |   }
 39 | 
 40 |   /**
 41 |    * Load config file
 42 |    */
 43 |   private async loadConfig(): Promise<ClientConfigFile> {
 44 |     try {
 45 |       const content = await fs.readFile(this.configFile, "utf-8");
 46 |       return JSON.parse(content);
 47 |     } catch (error) {
 48 |       // Config file doesn't exist or is invalid, return empty config
 49 |       return { oauthClients: {} };
 50 |     }
 51 |   }
 52 | 
 53 |   /**
 54 |    * Save config file
 55 |    */
 56 |   private async saveConfig(config: ClientConfigFile): Promise<void> {
 57 |     await this.ensureConfigDir();
 58 |     await fs.writeFile(
 59 |       this.configFile,
 60 |       JSON.stringify(config, null, 2),
 61 |       "utf-8",
 62 |     );
 63 |   }
 64 | 
 65 |   /**
 66 |    * Get OAuth client ID for a specific MCP host
 67 |    */
 68 |   async getOAuthClientId(mcpHost: string): Promise<string | null> {
 69 |     const config = await this.loadConfig();
 70 |     const clientConfig = config.oauthClients[mcpHost];
 71 |     return clientConfig?.clientId || null;
 72 |   }
 73 | 
 74 |   /**
 75 |    * Store OAuth client ID for a specific MCP host
 76 |    */
 77 |   async setOAuthClientId(mcpHost: string, clientId: string): Promise<void> {
 78 |     const config = await this.loadConfig();
 79 | 
 80 |     // Preserve existing access token if present
 81 |     const existing = config.oauthClients[mcpHost];
 82 |     config.oauthClients[mcpHost] = {
 83 |       clientId,
 84 |       mcpHost,
 85 |       registeredAt: new Date().toISOString(),
 86 |       accessToken: existing?.accessToken,
 87 |       tokenExpiresAt: existing?.tokenExpiresAt,
 88 |     };
 89 | 
 90 |     await this.saveConfig(config);
 91 |   }
 92 | 
 93 |   /**
 94 |    * Remove OAuth client configuration for a specific MCP host
 95 |    */
 96 |   async removeOAuthClientId(mcpHost: string): Promise<void> {
 97 |     const config = await this.loadConfig();
 98 |     delete config.oauthClients[mcpHost];
 99 |     await this.saveConfig(config);
100 |   }
101 | 
102 |   /**
103 |    * Get cached access token for a specific MCP host
104 |    */
105 |   async getAccessToken(mcpHost: string): Promise<string | null> {
106 |     const config = await this.loadConfig();
107 |     const clientConfig = config.oauthClients[mcpHost];
108 | 
109 |     if (!clientConfig?.accessToken) {
110 |       return null;
111 |     }
112 | 
113 |     // Check if token is expired
114 |     if (clientConfig.tokenExpiresAt) {
115 |       const expiresAt = new Date(clientConfig.tokenExpiresAt);
116 |       const now = new Date();
117 |       // Add 5 minute buffer before expiration
118 |       const bufferTime = 5 * 60 * 1000;
119 | 
120 |       if (now.getTime() + bufferTime >= expiresAt.getTime()) {
121 |         // Token is expired or will expire soon
122 |         await this.removeAccessToken(mcpHost);
123 |         return null;
124 |       }
125 |     }
126 | 
127 |     return clientConfig.accessToken;
128 |   }
129 | 
130 |   /**
131 |    * Store access token for a specific MCP host
132 |    */
133 |   async setAccessToken(
134 |     mcpHost: string,
135 |     accessToken: string,
136 |     expiresIn?: number,
137 |   ): Promise<void> {
138 |     const config = await this.loadConfig();
139 | 
140 |     const existing = config.oauthClients[mcpHost];
141 |     if (!existing) {
142 |       throw new Error(`No OAuth client configuration found for ${mcpHost}`);
143 |     }
144 | 
145 |     let tokenExpiresAt: string | undefined;
146 |     if (expiresIn) {
147 |       // expiresIn is in seconds, convert to milliseconds
148 |       const expiresAtMs = Date.now() + expiresIn * 1000;
149 |       tokenExpiresAt = new Date(expiresAtMs).toISOString();
150 |     }
151 | 
152 |     config.oauthClients[mcpHost] = {
153 |       ...existing,
154 |       accessToken,
155 |       tokenExpiresAt,
156 |     };
157 | 
158 |     await this.saveConfig(config);
159 |   }
160 | 
161 |   /**
162 |    * Remove cached access token for a specific MCP host
163 |    */
164 |   async removeAccessToken(mcpHost: string): Promise<void> {
165 |     const config = await this.loadConfig();
166 |     const existing = config.oauthClients[mcpHost];
167 | 
168 |     if (existing) {
169 |       config.oauthClients[mcpHost] = {
170 |         ...existing,
171 |         accessToken: undefined,
172 |         tokenExpiresAt: undefined,
173 |       };
174 |       await this.saveConfig(config);
175 |     }
176 |   }
177 | 
178 |   /**
179 |    * Clear all cached tokens (useful for logout)
180 |    */
181 |   async clearAllTokens(): Promise<void> {
182 |     const config = await this.loadConfig();
183 | 
184 |     for (const [host, clientConfig] of Object.entries(config.oauthClients)) {
185 |       config.oauthClients[host] = {
186 |         ...clientConfig,
187 |         accessToken: undefined,
188 |         tokenExpiresAt: undefined,
189 |       };
190 |     }
191 | 
192 |     await this.saveConfig(config);
193 |   }
194 | 
195 |   /**
196 |    * List all registered OAuth clients
197 |    */
198 |   async listOAuthClients(): Promise<OAuthClientConfig[]> {
199 |     const config = await this.loadConfig();
200 |     return Object.values(config.oauthClients);
201 |   }
202 | }
203 | 
```

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

```json
  1 | {
  2 |   "namespace": "network",
  3 |   "description": "These attributes may be used for any network related operation.\n",
  4 |   "attributes": {
  5 |     "network.carrier.icc": {
  6 |       "description": "The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network.",
  7 |       "type": "string",
  8 |       "stability": "development",
  9 |       "examples": ["DE"]
 10 |     },
 11 |     "network.carrier.mcc": {
 12 |       "description": "The mobile carrier country code.",
 13 |       "type": "string",
 14 |       "stability": "development",
 15 |       "examples": ["310"]
 16 |     },
 17 |     "network.carrier.mnc": {
 18 |       "description": "The mobile carrier network code.",
 19 |       "type": "string",
 20 |       "stability": "development",
 21 |       "examples": ["001"]
 22 |     },
 23 |     "network.carrier.name": {
 24 |       "description": "The name of the mobile carrier.",
 25 |       "type": "string",
 26 |       "stability": "development",
 27 |       "examples": ["sprint"]
 28 |     },
 29 |     "network.connection.subtype": {
 30 |       "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.",
 31 |       "type": "string",
 32 |       "stability": "development",
 33 |       "examples": [
 34 |         "gprs",
 35 |         "edge",
 36 |         "umts",
 37 |         "cdma",
 38 |         "evdo_0",
 39 |         "evdo_a",
 40 |         "cdma2000_1xrtt",
 41 |         "hsdpa",
 42 |         "hsupa",
 43 |         "hspa",
 44 |         "iden",
 45 |         "evdo_b",
 46 |         "lte",
 47 |         "ehrpd",
 48 |         "hspap",
 49 |         "gsm",
 50 |         "td_scdma",
 51 |         "iwlan",
 52 |         "nr",
 53 |         "nrnsa",
 54 |         "lte_ca"
 55 |       ]
 56 |     },
 57 |     "network.connection.type": {
 58 |       "description": "The internet connection type.",
 59 |       "type": "string",
 60 |       "stability": "development",
 61 |       "examples": ["wifi", "wired", "cell", "unavailable", "unknown"]
 62 |     },
 63 |     "network.local.address": {
 64 |       "description": "Local address of the network connection - IP address or Unix domain socket name.",
 65 |       "type": "string",
 66 |       "stability": "stable",
 67 |       "examples": ["10.1.2.80", "/tmp/my.sock"]
 68 |     },
 69 |     "network.local.port": {
 70 |       "description": "Local port number of the network connection.",
 71 |       "type": "number",
 72 |       "stability": "stable",
 73 |       "examples": ["65123"]
 74 |     },
 75 |     "network.peer.address": {
 76 |       "description": "Peer address of the network connection - IP address or Unix domain socket name.",
 77 |       "type": "string",
 78 |       "stability": "stable",
 79 |       "examples": ["10.1.2.80", "/tmp/my.sock"]
 80 |     },
 81 |     "network.peer.port": {
 82 |       "description": "Peer port number of the network connection.",
 83 |       "type": "number",
 84 |       "stability": "stable",
 85 |       "examples": ["65123"]
 86 |     },
 87 |     "network.protocol.name": {
 88 |       "description": "[OSI application layer](https://wikipedia.org/wiki/Application_layer) or non-OSI equivalent.",
 89 |       "type": "string",
 90 |       "note": "The value SHOULD be normalized to lowercase.",
 91 |       "stability": "stable",
 92 |       "examples": ["amqp", "http", "mqtt"]
 93 |     },
 94 |     "network.protocol.version": {
 95 |       "description": "The actual version of the protocol used for network communication.",
 96 |       "type": "string",
 97 |       "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",
 98 |       "stability": "stable",
 99 |       "examples": ["1.1", "2"]
100 |     },
101 |     "network.transport": {
102 |       "description": "[OSI transport layer](https://wikipedia.org/wiki/Transport_layer) or [inter-process communication method](https://wikipedia.org/wiki/Inter-process_communication).\n",
103 |       "type": "string",
104 |       "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",
105 |       "stability": "stable",
106 |       "examples": ["tcp", "udp", "pipe", "unix", "quic"]
107 |     },
108 |     "network.type": {
109 |       "description": "[OSI network layer](https://wikipedia.org/wiki/Network_layer) or non-OSI equivalent.",
110 |       "type": "string",
111 |       "note": "The value SHOULD be normalized to lowercase.",
112 |       "stability": "stable",
113 |       "examples": ["ipv4", "ipv6"]
114 |     },
115 |     "network.io.direction": {
116 |       "description": "The network IO operation direction.",
117 |       "type": "string",
118 |       "stability": "development",
119 |       "examples": ["transmit", "receive"]
120 |     },
121 |     "network.interface.name": {
122 |       "description": "The network interface name.",
123 |       "type": "string",
124 |       "stability": "development",
125 |       "examples": ["lo", "eth0"]
126 |     },
127 |     "network.connection.state": {
128 |       "description": "The state of network connection",
129 |       "type": "string",
130 |       "note": "Connection states are defined as part of the [rfc9293](https://datatracker.ietf.org/doc/html/rfc9293#section-3.3.2)",
131 |       "stability": "development",
132 |       "examples": [
133 |         "closed",
134 |         "close_wait",
135 |         "closing",
136 |         "established",
137 |         "fin_wait_1",
138 |         "fin_wait_2",
139 |         "last_ack",
140 |         "listen",
141 |         "syn_received",
142 |         "syn_sent",
143 |         "time_wait"
144 |       ]
145 |     }
146 |   }
147 | }
148 | 
```

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

```json
 1 | {
 2 |   "namespace": "cloud",
 3 |   "description": "A cloud environment (e.g. GCP, Azure, AWS).\n",
 4 |   "attributes": {
 5 |     "cloud.provider": {
 6 |       "description": "Name of the cloud provider.\n",
 7 |       "type": "string",
 8 |       "stability": "development",
 9 |       "examples": [
10 |         "alibaba_cloud",
11 |         "aws",
12 |         "azure",
13 |         "gcp",
14 |         "heroku",
15 |         "ibm_cloud",
16 |         "oracle_cloud",
17 |         "tencent_cloud"
18 |       ]
19 |     },
20 |     "cloud.account.id": {
21 |       "description": "The cloud account ID the resource is assigned to.\n",
22 |       "type": "string",
23 |       "stability": "development",
24 |       "examples": ["111111111111", "opentelemetry"]
25 |     },
26 |     "cloud.region": {
27 |       "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",
28 |       "type": "string",
29 |       "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",
30 |       "stability": "development",
31 |       "examples": ["us-central1", "us-east-1"]
32 |     },
33 |     "cloud.resource_id": {
34 |       "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",
35 |       "type": "string",
36 |       "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",
37 |       "stability": "development",
38 |       "examples": [
39 |         "arn:aws:lambda:REGION:ACCOUNT_ID:function:my-function",
40 |         "//run.googleapis.com/projects/PROJECT_ID/locations/LOCATION_ID/services/SERVICE_ID",
41 |         "/subscriptions/<SUBSCRIPTION_GUID>/resourceGroups/<RG>/providers/Microsoft.Web/sites/<FUNCAPP>/functions/<FUNC>"
42 |       ]
43 |     },
44 |     "cloud.availability_zone": {
45 |       "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",
46 |       "type": "string",
47 |       "note": "Availability zones are called \"zones\" on Alibaba Cloud and Google Cloud.\n",
48 |       "stability": "development",
49 |       "examples": ["us-east-1c"]
50 |     },
51 |     "cloud.platform": {
52 |       "description": "The cloud platform in use.\n",
53 |       "type": "string",
54 |       "note": "The prefix of the service SHOULD match the one specified in `cloud.provider`.\n",
55 |       "stability": "development",
56 |       "examples": [
57 |         "alibaba_cloud_ecs",
58 |         "alibaba_cloud_fc",
59 |         "alibaba_cloud_openshift",
60 |         "aws_ec2",
61 |         "aws_ecs",
62 |         "aws_eks",
63 |         "aws_lambda",
64 |         "aws_elastic_beanstalk",
65 |         "aws_app_runner",
66 |         "aws_openshift",
67 |         "azure.vm",
68 |         "azure.container_apps",
69 |         "azure.container_instances",
70 |         "azure.aks",
71 |         "azure.functions",
72 |         "azure.app_service",
73 |         "azure.openshift",
74 |         "azure_vm",
75 |         "azure_container_apps",
76 |         "azure_container_instances",
77 |         "azure_aks",
78 |         "azure_functions",
79 |         "azure_app_service",
80 |         "azure_openshift",
81 |         "gcp_bare_metal_solution",
82 |         "gcp_compute_engine",
83 |         "gcp_cloud_run",
84 |         "gcp_kubernetes_engine",
85 |         "gcp_cloud_functions",
86 |         "gcp_app_engine",
87 |         "gcp_openshift",
88 |         "ibm_cloud_openshift",
89 |         "oracle_cloud_compute",
90 |         "oracle_cloud_oke",
91 |         "tencent_cloud_cvm",
92 |         "tencent_cloud_eks",
93 |         "tencent_cloud_scf"
94 |       ]
95 |     }
96 |   }
97 | }
98 | 
```

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

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import updateIssue from "./update-issue.js";
  3 | 
  4 | describe("update_issue", () => {
  5 |   it("updates issue status", async () => {
  6 |     const result = await updateIssue.handler(
  7 |       {
  8 |         organizationSlug: "sentry-mcp-evals",
  9 |         issueId: "CLOUDFLARE-MCP-41",
 10 |         status: "resolved",
 11 |         assignedTo: undefined,
 12 |         issueUrl: undefined,
 13 |         regionUrl: undefined,
 14 |       },
 15 |       {
 16 |         constraints: {
 17 |           organizationSlug: null,
 18 |         },
 19 |         accessToken: "access-token",
 20 |         userId: "1",
 21 |       },
 22 |     );
 23 |     expect(result).toMatchInlineSnapshot(`
 24 |       "# Issue CLOUDFLARE-MCP-41 Updated in **sentry-mcp-evals**
 25 | 
 26 |       **Issue**: Error: Tool list_organizations is already registered
 27 |       **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41
 28 | 
 29 |       ## Changes Made
 30 | 
 31 |       **Status**: unresolved → **resolved**
 32 | 
 33 |       ## Current Status
 34 | 
 35 |       **Status**: resolved
 36 |       **Assigned To**: Unassigned
 37 | 
 38 |       # Using this information
 39 | 
 40 |       - The issue has been successfully updated in Sentry
 41 |       - You can view the issue details using: \`get_issue_details(organizationSlug="sentry-mcp-evals", issueId="CLOUDFLARE-MCP-41")\`
 42 |       - The issue is now marked as resolved and will no longer generate alerts
 43 |       "
 44 |     `);
 45 |   });
 46 | 
 47 |   it("updates issue assignment", async () => {
 48 |     const result = await updateIssue.handler(
 49 |       {
 50 |         organizationSlug: "sentry-mcp-evals",
 51 |         issueId: "CLOUDFLARE-MCP-41",
 52 |         status: undefined,
 53 |         assignedTo: "john.doe",
 54 |         issueUrl: undefined,
 55 |         regionUrl: undefined,
 56 |       },
 57 |       {
 58 |         constraints: {
 59 |           organizationSlug: null,
 60 |         },
 61 |         accessToken: "access-token",
 62 |         userId: "1",
 63 |       },
 64 |     );
 65 |     expect(result).toMatchInlineSnapshot(`
 66 |       "# Issue CLOUDFLARE-MCP-41 Updated in **sentry-mcp-evals**
 67 | 
 68 |       **Issue**: Error: Tool list_organizations is already registered
 69 |       **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41
 70 | 
 71 |       ## Changes Made
 72 | 
 73 |       **Assigned To**: Unassigned → **john.doe**
 74 | 
 75 |       ## Current Status
 76 | 
 77 |       **Status**: unresolved
 78 |       **Assigned To**: john.doe
 79 | 
 80 |       # Using this information
 81 | 
 82 |       - The issue has been successfully updated in Sentry
 83 |       - You can view the issue details using: \`get_issue_details(organizationSlug="sentry-mcp-evals", issueId="CLOUDFLARE-MCP-41")\`
 84 |       "
 85 |     `);
 86 |   });
 87 | 
 88 |   it("updates both status and assignment", async () => {
 89 |     const result = await updateIssue.handler(
 90 |       {
 91 |         organizationSlug: "sentry-mcp-evals",
 92 |         issueId: "CLOUDFLARE-MCP-41",
 93 |         status: "resolved",
 94 |         assignedTo: "me",
 95 |         issueUrl: undefined,
 96 |         regionUrl: undefined,
 97 |       },
 98 |       {
 99 |         constraints: {
100 |           organizationSlug: null,
101 |         },
102 |         accessToken: "access-token",
103 |         userId: "1",
104 |       },
105 |     );
106 |     expect(result).toMatchInlineSnapshot(`
107 |       "# Issue CLOUDFLARE-MCP-41 Updated in **sentry-mcp-evals**
108 | 
109 |       **Issue**: Error: Tool list_organizations is already registered
110 |       **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41
111 | 
112 |       ## Changes Made
113 | 
114 |       **Status**: unresolved → **resolved**
115 |       **Assigned To**: Unassigned → **You**
116 | 
117 |       ## Current Status
118 | 
119 |       **Status**: resolved
120 |       **Assigned To**: me
121 | 
122 |       # Using this information
123 | 
124 |       - The issue has been successfully updated in Sentry
125 |       - You can view the issue details using: \`get_issue_details(organizationSlug="sentry-mcp-evals", issueId="CLOUDFLARE-MCP-41")\`
126 |       - The issue is now marked as resolved and will no longer generate alerts
127 |       "
128 |     `);
129 |   });
130 | 
131 |   it("validates required parameters", async () => {
132 |     await expect(
133 |       updateIssue.handler(
134 |         {
135 |           organizationSlug: undefined,
136 |           issueId: undefined,
137 |           status: undefined,
138 |           assignedTo: undefined,
139 |           issueUrl: undefined,
140 |           regionUrl: undefined,
141 |         },
142 |         {
143 |           constraints: {
144 |             organizationSlug: null,
145 |           },
146 |           accessToken: "access-token",
147 |           userId: "1",
148 |         },
149 |       ),
150 |     ).rejects.toThrow("Either `issueId` or `issueUrl` must be provided");
151 |   });
152 | 
153 |   it("validates organization slug when using issueId", async () => {
154 |     await expect(
155 |       updateIssue.handler(
156 |         {
157 |           organizationSlug: undefined,
158 |           issueId: "CLOUDFLARE-MCP-41",
159 |           status: "resolved",
160 |           assignedTo: undefined,
161 |           issueUrl: undefined,
162 |           regionUrl: undefined,
163 |         },
164 |         {
165 |           constraints: {
166 |             organizationSlug: null,
167 |           },
168 |           accessToken: "access-token",
169 |           userId: "1",
170 |         },
171 |       ),
172 |     ).rejects.toThrow(
173 |       "`organizationSlug` is required when providing `issueId`",
174 |     );
175 |   });
176 | 
177 |   it("validates update parameters", async () => {
178 |     await expect(
179 |       updateIssue.handler(
180 |         {
181 |           organizationSlug: "sentry-mcp-evals",
182 |           issueId: "CLOUDFLARE-MCP-41",
183 |           status: undefined,
184 |           assignedTo: undefined,
185 |           issueUrl: undefined,
186 |           regionUrl: undefined,
187 |         },
188 |         {
189 |           constraints: {
190 |             organizationSlug: null,
191 |           },
192 |           accessToken: "access-token",
193 |           userId: "1",
194 |         },
195 |       ),
196 |     ).rejects.toThrow(
197 |       "At least one of `status` or `assignedTo` must be provided to update the issue",
198 |     );
199 |   });
200 | });
201 | 
```

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

```json
 1 | {
 2 |   "namespace": "container",
 3 |   "description": "A container instance.\n",
 4 |   "attributes": {
 5 |     "container.name": {
 6 |       "description": "Container name used by container runtime.\n",
 7 |       "type": "string",
 8 |       "stability": "development",
 9 |       "examples": ["opentelemetry-autoconf"]
10 |     },
11 |     "container.id": {
12 |       "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",
13 |       "type": "string",
14 |       "stability": "development",
15 |       "examples": ["a3bf90e006b2"]
16 |     },
17 |     "container.runtime": {
18 |       "description": "The container runtime managing this container.\n",
19 |       "type": "string",
20 |       "stability": "development",
21 |       "examples": ["docker", "containerd", "rkt"]
22 |     },
23 |     "container.image.name": {
24 |       "description": "Name of the image the container was built on.\n",
25 |       "type": "string",
26 |       "stability": "development",
27 |       "examples": ["gcr.io/opentelemetry/operator"]
28 |     },
29 |     "container.image.tags": {
30 |       "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",
31 |       "type": "string",
32 |       "stability": "development",
33 |       "examples": ["[\"v1.27.1\",\"3.5.7-0\"]"]
34 |     },
35 |     "container.image.id": {
36 |       "description": "Runtime specific image identifier. Usually a hash algorithm followed by a UUID.\n",
37 |       "type": "string",
38 |       "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",
39 |       "stability": "development",
40 |       "examples": [
41 |         "sha256:19c92d0a00d1b66d897bceaa7319bee0dd38a10a851c60bcec9474aa3f01e50f"
42 |       ]
43 |     },
44 |     "container.image.repo_digests": {
45 |       "description": "Repo digests of the container image as provided by the container runtime.\n",
46 |       "type": "string",
47 |       "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",
48 |       "stability": "development",
49 |       "examples": [
50 |         "[\"example@sha256:afcc7f1ac1b49db317a7196c902e61c6c3c4607d63599ee1a82d702d249a0ccb\",\"internal.registry.example.com:5000/example@sha256:b69959407d21e8a062e0416bf13405bb2b71ed7a84dde4158ebafacfa06f5578\"]"
51 |       ]
52 |     },
53 |     "container.command": {
54 |       "description": "The command used to run the container (i.e. the command name).\n",
55 |       "type": "string",
56 |       "note": "If using embedded credentials or sensitive data, it is recommended to remove them to prevent potential leakage.\n",
57 |       "stability": "development",
58 |       "examples": ["otelcontribcol"]
59 |     },
60 |     "container.command_line": {
61 |       "description": "The full command run by the container as a single string representing the full command.\n",
62 |       "type": "string",
63 |       "stability": "development",
64 |       "examples": ["otelcontribcol --config config.yaml"]
65 |     },
66 |     "container.command_args": {
67 |       "description": "All the command arguments (including the command/executable itself) run by the container.\n",
68 |       "type": "string",
69 |       "stability": "development",
70 |       "examples": ["[\"otelcontribcol\",\"--config\",\"config.yaml\"]"]
71 |     },
72 |     "container.label": {
73 |       "description": "Container labels, `<key>` being the label name, the value being the label value.\n",
74 |       "type": "string",
75 |       "note": "For example, a docker container label `app` with value `nginx` SHOULD be recorded as the `container.label.app` attribute with value `\"nginx\"`.\n",
76 |       "stability": "development",
77 |       "examples": ["nginx"]
78 |     },
79 |     "container.csi.plugin.name": {
80 |       "description": "The name of the CSI ([Container Storage Interface](https://github.com/container-storage-interface/spec)) plugin used by the volume.\n",
81 |       "type": "string",
82 |       "note": "This can sometimes be referred to as a \"driver\" in CSI implementations. This should represent the `name` field of the GetPluginInfo RPC.\n",
83 |       "stability": "development",
84 |       "examples": ["pd.csi.storage.gke.io"]
85 |     },
86 |     "container.csi.volume.id": {
87 |       "description": "The unique volume ID returned by the CSI ([Container Storage Interface](https://github.com/container-storage-interface/spec)) plugin.\n",
88 |       "type": "string",
89 |       "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",
90 |       "stability": "development",
91 |       "examples": [
92 |         "projects/my-gcp-project/zones/my-gcp-zone/disks/my-gcp-disk"
93 |       ]
94 |     }
95 |   }
96 | }
97 | 
```

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

```json
  1 | {
  2 |   "namespace": "file",
  3 |   "description": "Describes file attributes.",
  4 |   "attributes": {
  5 |     "file.accessed": {
  6 |       "description": "Time when the file was last accessed, in ISO 8601 format.\n",
  7 |       "type": "string",
  8 |       "note": "This attribute might not be supported by some file systems — NFS, FAT32, in embedded OS, etc.\n",
  9 |       "stability": "development",
 10 |       "examples": ["2021-01-01T12:00:00Z"]
 11 |     },
 12 |     "file.attributes": {
 13 |       "description": "Array of file attributes.\n",
 14 |       "type": "string",
 15 |       "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",
 16 |       "stability": "development",
 17 |       "examples": ["[\"readonly\",\"hidden\"]"]
 18 |     },
 19 |     "file.created": {
 20 |       "description": "Time when the file was created, in ISO 8601 format.\n",
 21 |       "type": "string",
 22 |       "note": "This attribute might not be supported by some file systems — NFS, FAT32, in embedded OS, etc.\n",
 23 |       "stability": "development",
 24 |       "examples": ["2021-01-01T12:00:00Z"]
 25 |     },
 26 |     "file.changed": {
 27 |       "description": "Time when the file attributes or metadata was last changed, in ISO 8601 format.\n",
 28 |       "type": "string",
 29 |       "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",
 30 |       "stability": "development",
 31 |       "examples": ["2021-01-01T12:00:00Z"]
 32 |     },
 33 |     "file.directory": {
 34 |       "description": "Directory where the file is located. It should include the drive letter, when appropriate.\n",
 35 |       "type": "string",
 36 |       "stability": "development",
 37 |       "examples": ["/home/user", "C:\\Program Files\\MyApp"]
 38 |     },
 39 |     "file.extension": {
 40 |       "description": "File extension, excluding the leading dot.\n",
 41 |       "type": "string",
 42 |       "note": "When the file name has multiple extensions (example.tar.gz), only the last one should be captured (\"gz\", not \"tar.gz\").\n",
 43 |       "stability": "development",
 44 |       "examples": ["png", "gz"]
 45 |     },
 46 |     "file.fork_name": {
 47 |       "description": "Name of the fork. A fork is additional data associated with a filesystem object.\n",
 48 |       "type": "string",
 49 |       "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",
 50 |       "stability": "development",
 51 |       "examples": ["Zone.Identifer"]
 52 |     },
 53 |     "file.group.id": {
 54 |       "description": "Primary Group ID (GID) of the file.\n",
 55 |       "type": "string",
 56 |       "stability": "development",
 57 |       "examples": ["1000"]
 58 |     },
 59 |     "file.group.name": {
 60 |       "description": "Primary group name of the file.\n",
 61 |       "type": "string",
 62 |       "stability": "development",
 63 |       "examples": ["users"]
 64 |     },
 65 |     "file.inode": {
 66 |       "description": "Inode representing the file in the filesystem.\n",
 67 |       "type": "string",
 68 |       "stability": "development",
 69 |       "examples": ["256383"]
 70 |     },
 71 |     "file.mode": {
 72 |       "description": "Mode of the file in octal representation.\n",
 73 |       "type": "string",
 74 |       "stability": "development",
 75 |       "examples": ["0640"]
 76 |     },
 77 |     "file.modified": {
 78 |       "description": "Time when the file content was last modified, in ISO 8601 format.\n",
 79 |       "type": "string",
 80 |       "stability": "development",
 81 |       "examples": ["2021-01-01T12:00:00Z"]
 82 |     },
 83 |     "file.name": {
 84 |       "description": "Name of the file including the extension, without the directory.\n",
 85 |       "type": "string",
 86 |       "stability": "development",
 87 |       "examples": ["example.png"]
 88 |     },
 89 |     "file.owner.id": {
 90 |       "description": "The user ID (UID) or security identifier (SID) of the file owner.\n",
 91 |       "type": "string",
 92 |       "stability": "development",
 93 |       "examples": ["1000"]
 94 |     },
 95 |     "file.owner.name": {
 96 |       "description": "Username of the file owner.\n",
 97 |       "type": "string",
 98 |       "stability": "development",
 99 |       "examples": ["root"]
100 |     },
101 |     "file.path": {
102 |       "description": "Full path to the file, including the file name. It should include the drive letter, when appropriate.\n",
103 |       "type": "string",
104 |       "stability": "development",
105 |       "examples": [
106 |         "/home/alice/example.png",
107 |         "C:\\Program Files\\MyApp\\myapp.exe"
108 |       ]
109 |     },
110 |     "file.size": {
111 |       "description": "File size in bytes.\n",
112 |       "type": "number",
113 |       "stability": "development"
114 |     },
115 |     "file.symbolic_link.target_path": {
116 |       "description": "Path to the target of a symbolic link.\n",
117 |       "type": "string",
118 |       "note": "This attribute is only applicable to symbolic links.\n",
119 |       "stability": "development",
120 |       "examples": ["/usr/bin/python3"]
121 |     }
122 |   }
123 | }
124 | 
```

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

```json
 1 | {
 2 |   "namespace": "cloudfoundry",
 3 |   "description": "CloudFoundry resource attributes.\n",
 4 |   "attributes": {
 5 |     "cloudfoundry.system.id": {
 6 |       "description": "A guid or another name describing the event source.\n",
 7 |       "type": "string",
 8 |       "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",
 9 |       "stability": "development",
10 |       "examples": ["cf/gorouter"]
11 |     },
12 |     "cloudfoundry.system.instance.id": {
13 |       "description": "A guid describing the concrete instance of the event source.\n",
14 |       "type": "string",
15 |       "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",
16 |       "stability": "development",
17 |       "examples": ["218fc5a9-a5f1-4b54-aa05-46717d0ab26d"]
18 |     },
19 |     "cloudfoundry.app.name": {
20 |       "description": "The name of the application.\n",
21 |       "type": "string",
22 |       "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",
23 |       "stability": "development",
24 |       "examples": ["my-app-name"]
25 |     },
26 |     "cloudfoundry.app.id": {
27 |       "description": "The guid of the application.\n",
28 |       "type": "string",
29 |       "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",
30 |       "stability": "development",
31 |       "examples": ["218fc5a9-a5f1-4b54-aa05-46717d0ab26d"]
32 |     },
33 |     "cloudfoundry.app.instance.id": {
34 |       "description": "The index of the application instance. 0 when just one instance is active.\n",
35 |       "type": "string",
36 |       "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",
37 |       "stability": "development",
38 |       "examples": ["0", "1"]
39 |     },
40 |     "cloudfoundry.space.name": {
41 |       "description": "The name of the CloudFoundry space the application is running in.\n",
42 |       "type": "string",
43 |       "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",
44 |       "stability": "development",
45 |       "examples": ["my-space-name"]
46 |     },
47 |     "cloudfoundry.space.id": {
48 |       "description": "The guid of the CloudFoundry space the application is running in.\n",
49 |       "type": "string",
50 |       "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",
51 |       "stability": "development",
52 |       "examples": ["218fc5a9-a5f1-4b54-aa05-46717d0ab26d"]
53 |     },
54 |     "cloudfoundry.org.name": {
55 |       "description": "The name of the CloudFoundry organization the app is running in.\n",
56 |       "type": "string",
57 |       "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",
58 |       "stability": "development",
59 |       "examples": ["my-org-name"]
60 |     },
61 |     "cloudfoundry.org.id": {
62 |       "description": "The guid of the CloudFoundry org the application is running in.\n",
63 |       "type": "string",
64 |       "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",
65 |       "stability": "development",
66 |       "examples": ["218fc5a9-a5f1-4b54-aa05-46717d0ab26d"]
67 |     },
68 |     "cloudfoundry.process.id": {
69 |       "description": "The UID identifying the process.\n",
70 |       "type": "string",
71 |       "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",
72 |       "stability": "development",
73 |       "examples": ["218fc5a9-a5f1-4b54-aa05-46717d0ab26d"]
74 |     },
75 |     "cloudfoundry.process.type": {
76 |       "description": "The type of process.\n",
77 |       "type": "string",
78 |       "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",
79 |       "stability": "development",
80 |       "examples": ["web"]
81 |     }
82 |   }
83 | }
84 | 
```

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

```json
  1 | {
  2 |   "id": "a1b2c3d4e5f6789012345678901234567",
  3 |   "groupID": "7890123456",
  4 |   "eventID": "a1b2c3d4e5f6789012345678901234567",
  5 |   "projectID": "4509062593708032",
  6 |   "size": 8432,
  7 |   "type": "transaction",
  8 |   "title": "GET /api/users",
  9 |   "message": "",
 10 |   "platform": "python",
 11 |   "datetime": "2025-08-06T18:00:00.000000Z",
 12 |   "dateCreated": "2025-08-06T18:00:00.000000Z",
 13 |   "contexts": {
 14 |     "trace": {
 15 |       "trace_id": "abcdef1234567890abcdef1234567890",
 16 |       "span_id": "1234567890abcdef",
 17 |       "op": "http.server",
 18 |       "status": "ok",
 19 |       "exclusive_time": 250.5,
 20 |       "data": {
 21 |         "http.request.method": "GET",
 22 |         "url.path": "/api/users"
 23 |       }
 24 |     }
 25 |   },
 26 |   "occurrence": {
 27 |     "id": "occ_123456789",
 28 |     "projectId": 4509062593708032,
 29 |     "eventId": "a1b2c3d4e5f6789012345678901234567",
 30 |     "fingerprint": [
 31 |       "n_plus_one_db_queries",
 32 |       "SELECT * FROM users WHERE id = %s"
 33 |     ],
 34 |     "issueTitle": "N+1 Query: SELECT * FROM users WHERE id = %s",
 35 |     "subtitle": "Database query repeated 25 times",
 36 |     "resourceId": null,
 37 |     "type": 1006,
 38 |     "detectionTime": 1722963600,
 39 |     "level": "warning",
 40 |     "culprit": "SELECT * FROM users WHERE id = %s",
 41 |     "priority": 50,
 42 |     "assignee": null,
 43 |     "evidenceData": {
 44 |       "transactionName": "/api/users",
 45 |       "parentSpan": "GET /api/users",
 46 |       "parentSpanIds": ["parent123"],
 47 |       "repeatingSpansCompact": ["SELECT * FROM users WHERE id = %s"],
 48 |       "repeatingSpans": ["db - SELECT * FROM users WHERE id = %s"],
 49 |       "numberRepeatingSpans": "25",
 50 |       "numPatternRepetitions": 25,
 51 |       "offenderSpanIds": [
 52 |         "span001",
 53 |         "span002",
 54 |         "span003",
 55 |         "span004",
 56 |         "span005",
 57 |         "span006",
 58 |         "span007",
 59 |         "span008",
 60 |         "span009",
 61 |         "span010",
 62 |         "span011",
 63 |         "span012",
 64 |         "span013",
 65 |         "span014",
 66 |         "span015",
 67 |         "span016",
 68 |         "span017",
 69 |         "span018",
 70 |         "span019",
 71 |         "span020",
 72 |         "span021",
 73 |         "span022",
 74 |         "span023",
 75 |         "span024",
 76 |         "span025"
 77 |       ],
 78 |       "op": "db"
 79 |     },
 80 |     "evidenceDisplay": [
 81 |       {
 82 |         "name": "Offending Spans",
 83 |         "value": "SELECT * FROM users WHERE id = %s",
 84 |         "important": true
 85 |       },
 86 |       {
 87 |         "name": "Repeated",
 88 |         "value": "25 times",
 89 |         "important": true
 90 |       }
 91 |     ]
 92 |   },
 93 |   "entries": [
 94 |     {
 95 |       "type": "spans",
 96 |       "data": [
 97 |         {
 98 |           "span_id": "parent123",
 99 |           "trace_id": "abcdef1234567890abcdef1234567890",
100 |           "parent_span_id": "1234567890abcdef",
101 |           "op": "http.server",
102 |           "description": "GET /api/users",
103 |           "status": "ok",
104 |           "start_timestamp": 1722963600.0,
105 |           "timestamp": 1722963600.25,
106 |           "data": {
107 |             "http.request.method": "GET",
108 |             "url.path": "/api/users"
109 |           }
110 |         },
111 |         {
112 |           "span_id": "span001",
113 |           "trace_id": "abcdef1234567890abcdef1234567890",
114 |           "parent_span_id": "parent123",
115 |           "op": "db.query",
116 |           "description": "SELECT * FROM users WHERE id = 1",
117 |           "status": "ok",
118 |           "start_timestamp": 1722963600.01,
119 |           "timestamp": 1722963600.015,
120 |           "data": {
121 |             "db.system": "postgresql",
122 |             "db.operation": "SELECT"
123 |           }
124 |         },
125 |         {
126 |           "span_id": "span002",
127 |           "trace_id": "abcdef1234567890abcdef1234567890",
128 |           "parent_span_id": "parent123",
129 |           "op": "db.query",
130 |           "description": "SELECT * FROM users WHERE id = 2",
131 |           "status": "ok",
132 |           "start_timestamp": 1722963600.02,
133 |           "timestamp": 1722963600.025,
134 |           "data": {
135 |             "db.system": "postgresql",
136 |             "db.operation": "SELECT"
137 |           }
138 |         },
139 |         {
140 |           "span_id": "span003",
141 |           "trace_id": "abcdef1234567890abcdef1234567890",
142 |           "parent_span_id": "parent123",
143 |           "op": "db.query",
144 |           "description": "SELECT * FROM users WHERE id = 3",
145 |           "status": "ok",
146 |           "start_timestamp": 1722963600.03,
147 |           "timestamp": 1722963600.035,
148 |           "data": {
149 |             "db.system": "postgresql",
150 |             "db.operation": "SELECT"
151 |           }
152 |         },
153 |         {
154 |           "span_id": "span004",
155 |           "trace_id": "abcdef1234567890abcdef1234567890",
156 |           "parent_span_id": "parent123",
157 |           "op": "db.query",
158 |           "description": "SELECT * FROM users WHERE id = 4",
159 |           "status": "ok",
160 |           "start_timestamp": 1722963600.04,
161 |           "timestamp": 1722963600.045,
162 |           "data": {
163 |             "db.system": "postgresql",
164 |             "db.operation": "SELECT"
165 |           }
166 |         },
167 |         {
168 |           "span_id": "span005",
169 |           "trace_id": "abcdef1234567890abcdef1234567890",
170 |           "parent_span_id": "parent123",
171 |           "op": "db.query",
172 |           "description": "SELECT * FROM users WHERE id = 5",
173 |           "status": "ok",
174 |           "start_timestamp": 1722963600.05,
175 |           "timestamp": 1722963600.055,
176 |           "data": {
177 |             "db.system": "postgresql",
178 |             "db.operation": "SELECT"
179 |           }
180 |         }
181 |       ]
182 |     },
183 |     {
184 |       "type": "request",
185 |       "data": {
186 |         "method": "GET",
187 |         "url": "https://api.example.com/api/users",
188 |         "query": [],
189 |         "headers": [["Accept", "application/json"], ["Host", "api.example.com"]]
190 |       }
191 |     }
192 |   ],
193 |   "tags": [
194 |     { "key": "environment", "value": "production" },
195 |     { "key": "transaction", "value": "/api/users" }
196 |   ]
197 | }
198 | 
```

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

```typescript
  1 | import { useCallback, useMemo } from "react";
  2 | import type { Message } from "ai";
  3 | 
  4 | const CHAT_STORAGE_KEY = "sentry_chat_messages";
  5 | const TIMESTAMP_STORAGE_KEY = "sentry_chat_timestamp";
  6 | const MAX_STORED_MESSAGES = 100; // Limit storage size
  7 | const CACHE_EXPIRY_MS = 60 * 60 * 1000; // 1 hour in milliseconds
  8 | 
  9 | export function usePersistedChat(isAuthenticated: boolean) {
 10 |   // Check if cache is expired
 11 |   const isCacheExpired = useCallback(() => {
 12 |     try {
 13 |       const timestampStr = localStorage.getItem(TIMESTAMP_STORAGE_KEY);
 14 |       if (!timestampStr) return true;
 15 | 
 16 |       const timestamp = Number.parseInt(timestampStr, 10);
 17 |       const now = Date.now();
 18 |       return now - timestamp > CACHE_EXPIRY_MS;
 19 |     } catch {
 20 |       return true;
 21 |     }
 22 |   }, []);
 23 | 
 24 |   // Update timestamp to extend cache expiry
 25 |   const updateTimestamp = useCallback(() => {
 26 |     try {
 27 |       localStorage.setItem(TIMESTAMP_STORAGE_KEY, Date.now().toString());
 28 |     } catch (error) {
 29 |       console.error("Failed to update chat timestamp:", error);
 30 |     }
 31 |   }, []);
 32 | 
 33 |   // Validate a message to ensure it won't cause conversion errors
 34 |   const isValidMessage = useCallback((msg: Message): boolean => {
 35 |     // Check if message has parts (newer structure)
 36 |     if (msg.parts && Array.isArray(msg.parts)) {
 37 |       // Check each part for validity
 38 |       return msg.parts.every((part) => {
 39 |         // Text parts are always valid
 40 |         if (part.type === "text") {
 41 |           return true;
 42 |         }
 43 | 
 44 |         // Tool invocation parts must be complete (have result) if state is "call" or "result"
 45 |         if (part.type === "tool-invocation") {
 46 |           const invocation = part as any;
 47 |           // If it's in "call" or "result" state, it must have a result
 48 |           if (invocation.state === "call" || invocation.state === "result") {
 49 |             const content = invocation.result?.content;
 50 |             // Ensure content exists and is not an empty array
 51 |             return (
 52 |               content && (Array.isArray(content) ? content.length > 0 : true)
 53 |             );
 54 |           }
 55 |           // partial-call state is okay without result
 56 |           return true;
 57 |         }
 58 | 
 59 |         // Other part types are assumed valid
 60 |         return true;
 61 |       });
 62 |     }
 63 | 
 64 |     // Check if message has content (legacy structure)
 65 |     if (msg.content && typeof msg.content === "string") {
 66 |       return msg.content.trim() !== "";
 67 |     }
 68 | 
 69 |     return false;
 70 |   }, []);
 71 | 
 72 |   // Load initial messages from localStorage
 73 |   const initialMessages = useMemo(() => {
 74 |     if (!isAuthenticated) return [];
 75 | 
 76 |     // Check if cache is expired
 77 |     if (isCacheExpired()) {
 78 |       // Clear expired data
 79 |       localStorage.removeItem(CHAT_STORAGE_KEY);
 80 |       localStorage.removeItem(TIMESTAMP_STORAGE_KEY);
 81 |       return [];
 82 |     }
 83 | 
 84 |     try {
 85 |       const stored = localStorage.getItem(CHAT_STORAGE_KEY);
 86 |       if (stored) {
 87 |         const parsed = JSON.parse(stored) as Message[];
 88 |         // Validate the data structure
 89 |         if (Array.isArray(parsed) && parsed.length > 0) {
 90 |           // Filter out any invalid or incomplete messages
 91 |           const validMessages = parsed.filter(isValidMessage);
 92 |           if (validMessages.length > 0) {
 93 |             // Update timestamp since we're loading existing messages
 94 |             updateTimestamp();
 95 |             return validMessages;
 96 |           }
 97 |         }
 98 |       }
 99 |     } catch (error) {
100 |       console.error("Failed to load chat history:", error);
101 |       // Clear corrupted data
102 |       localStorage.removeItem(CHAT_STORAGE_KEY);
103 |       localStorage.removeItem(TIMESTAMP_STORAGE_KEY);
104 |     }
105 | 
106 |     return [];
107 |   }, [isAuthenticated, isCacheExpired, updateTimestamp, isValidMessage]);
108 | 
109 |   // Function to save messages
110 |   const saveMessages = useCallback(
111 |     (messages: Message[]) => {
112 |       if (!isAuthenticated || messages.length === 0) return;
113 | 
114 |       try {
115 |         // Filter out invalid messages before storing
116 |         const validMessages = messages.filter(isValidMessage);
117 | 
118 |         // Only store the most recent valid messages to avoid storage limits
119 |         const messagesToStore = validMessages.slice(-MAX_STORED_MESSAGES);
120 | 
121 |         // Don't save if there are no valid messages
122 |         if (messagesToStore.length === 0) {
123 |           localStorage.removeItem(CHAT_STORAGE_KEY);
124 |           localStorage.removeItem(TIMESTAMP_STORAGE_KEY);
125 |           return;
126 |         }
127 | 
128 |         localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(messagesToStore));
129 |         // Update timestamp when saving messages (extends expiry)
130 |         updateTimestamp();
131 |       } catch (error) {
132 |         console.error("Failed to save chat history:", error);
133 |         // If we hit storage quota, try to clear old messages
134 |         if (
135 |           error instanceof DOMException &&
136 |           error.name === "QuotaExceededError"
137 |         ) {
138 |           try {
139 |             const validMessages = messages.filter(isValidMessage);
140 |             const recentMessages = validMessages.slice(-50); // Keep only last 50
141 |             localStorage.setItem(
142 |               CHAT_STORAGE_KEY,
143 |               JSON.stringify(recentMessages),
144 |             );
145 |             updateTimestamp();
146 |           } catch {
147 |             // If still failing, clear the storage
148 |             localStorage.removeItem(CHAT_STORAGE_KEY);
149 |             localStorage.removeItem(TIMESTAMP_STORAGE_KEY);
150 |           }
151 |         }
152 |       }
153 |     },
154 |     [isAuthenticated, updateTimestamp, isValidMessage],
155 |   );
156 | 
157 |   // Clear persisted messages
158 |   const clearPersistedMessages = useCallback(() => {
159 |     localStorage.removeItem(CHAT_STORAGE_KEY);
160 |     localStorage.removeItem(TIMESTAMP_STORAGE_KEY);
161 |   }, []);
162 | 
163 |   return {
164 |     initialMessages,
165 |     saveMessages,
166 |     clearPersistedMessages,
167 |   };
168 | }
169 | 
```

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

```typescript
  1 | import { describe, expect, it } from "vitest";
  2 | import {
  3 |   extractIssueId,
  4 |   parseIssueId,
  5 |   parseIssueParams,
  6 | } from "./issue-helpers";
  7 | 
  8 | describe("extractIssueId", () => {
  9 |   it("should extract issue ID from a full Sentry URL", () => {
 10 |     expect(
 11 |       extractIssueId("https://sentry.sentry.io/issues/1234"),
 12 |     ).toMatchInlineSnapshot(`
 13 |       {
 14 |         "issueId": "1234",
 15 |         "organizationSlug": "sentry",
 16 |       }
 17 |     `);
 18 |   });
 19 | 
 20 |   it("should extract issue ID from a Sentry URL with organization in path", () => {
 21 |     expect(
 22 |       extractIssueId("https://sentry.io/sentry/issues/123"),
 23 |     ).toMatchInlineSnapshot(`
 24 |       {
 25 |         "issueId": "123",
 26 |         "organizationSlug": "sentry",
 27 |       }
 28 |     `);
 29 |   });
 30 | 
 31 |   it("should extract issue ID and org slug from URL with organizations path", () => {
 32 |     expect(
 33 |       extractIssueId("https://sentry.io/organizations/my-org/issues/123"),
 34 |     ).toMatchInlineSnapshot(`
 35 |       {
 36 |         "issueId": "123",
 37 |         "organizationSlug": "my-org",
 38 |       }
 39 |     `);
 40 |   });
 41 | 
 42 |   it("should extract issue ID and org slug from subdomain URL", () => {
 43 |     expect(extractIssueId("https://my-team.sentry.io/issues/123")).toEqual({
 44 |       issueId: "123",
 45 |       organizationSlug: "my-team",
 46 |     });
 47 |   });
 48 | 
 49 |   it("should extract issue ID and org slug from self-hosted Sentry with subdomain", () => {
 50 |     expect(
 51 |       extractIssueId("https://sentry.mycompany.com/issues/123"),
 52 |     ).toMatchInlineSnapshot(`
 53 |       {
 54 |         "issueId": "123",
 55 |         "organizationSlug": "sentry",
 56 |       }
 57 |     `);
 58 |   });
 59 | 
 60 |   it("should extract issue ID and org slug from self-hosted Sentry with organization path", () => {
 61 |     expect(
 62 |       extractIssueId("https://mycompany.com/my-team/issues/123"),
 63 |     ).toMatchInlineSnapshot(`
 64 |       {
 65 |         "issueId": "123",
 66 |         "organizationSlug": "my-team",
 67 |       }
 68 |     `);
 69 |   });
 70 | 
 71 |   it("should throw error for empty input", () => {
 72 |     expect(() => extractIssueId("")).toThrowErrorMatchingInlineSnapshot(
 73 |       `[UserInputError: Invalid Sentry issue URL. URL must be a non-empty string.]`,
 74 |     );
 75 |   });
 76 | 
 77 |   it("should throw error for invalid URL path", () => {
 78 |     expect(() =>
 79 |       extractIssueId("https://sentry.sentry.io/projects/123"),
 80 |     ).toThrowErrorMatchingInlineSnapshot(
 81 |       `[UserInputError: Invalid Sentry issue URL. Path must contain '/issues/{issue_id}']`,
 82 |     );
 83 |   });
 84 | 
 85 |   it("should throw error for non-numeric issue ID in URL", () => {
 86 |     expect(
 87 |       extractIssueId("https://sentry.sentry.io/issues/abc"),
 88 |     ).toMatchInlineSnapshot(`
 89 |       {
 90 |         "issueId": "abc",
 91 |         "organizationSlug": "sentry",
 92 |       }
 93 |     `);
 94 |   });
 95 | 
 96 |   it("should throw error for non-numeric standalone ID", () => {
 97 |     expect(() => extractIssueId("abc")).toThrowErrorMatchingInlineSnapshot(
 98 |       `[UserInputError: Invalid Sentry issue URL. Must start with http:// or https://]`,
 99 |     );
100 |   });
101 | });
102 | 
103 | describe("parseIssueId", () => {
104 |   describe("cleaning", () => {
105 |     it("should remove trailing punctuation", () => {
106 |       expect(parseIssueId("CLOUDFLARE-MCP-41.!")).toBe("CLOUDFLARE-MCP-41");
107 |     });
108 | 
109 |     it("should remove special characters except dash and underscore", () => {
110 |       expect(parseIssueId("ID_123-456!@#")).toBe("ID_123-456");
111 |     });
112 |   });
113 | 
114 |   describe("format validation", () => {
115 |     it("should accept pure numeric issue IDs", () => {
116 |       expect(parseIssueId("12345")).toBe("12345");
117 |     });
118 | 
119 |     it("should accept project-based IDs starting with letters", () => {
120 |       expect(parseIssueId("PROJECT-123")).toBe("PROJECT-123");
121 |       expect(parseIssueId("MCP-SERVER-E9E")).toBe("MCP-SERVER-E9E");
122 |     });
123 | 
124 |     it("should accept project-based IDs starting with numbers", () => {
125 |       expect(parseIssueId("3R-3")).toBe("3R-3");
126 |       expect(parseIssueId("3R-AUTOMATION-SYSTEM-3")).toBe(
127 |         "3R-AUTOMATION-SYSTEM-3",
128 |       );
129 |     });
130 | 
131 |     it("should throw error for invalid formats", () => {
132 |       // Starting with hyphen
133 |       expect(() => parseIssueId("-123")).toThrowError(
134 |         /Invalid issue ID format/,
135 |       );
136 | 
137 |       // Ending with hyphen
138 |       expect(() => parseIssueId("PROJECT-")).toThrowError(
139 |         /Invalid issue ID format/,
140 |       );
141 | 
142 |       // Empty string after cleaning
143 |       expect(() => parseIssueId("!!!")).toThrowError(
144 |         /empty after removing special characters/,
145 |       );
146 |     });
147 |   });
148 | });
149 | 
150 | describe("parseIssueParams", () => {
151 |   it("should parse from issueUrl", () => {
152 |     expect(
153 |       parseIssueParams({
154 |         issueUrl: "https://sentry.io/sentry/issues/123",
155 |       }),
156 |     ).toEqual({ organizationSlug: "sentry", issueId: "123" });
157 |   });
158 | 
159 |   it("should parse from issueId and organizationSlug", () => {
160 |     expect(
161 |       parseIssueParams({
162 |         issueId: "CLOUDFLARE-MCP-41.!",
163 |         organizationSlug: "sentry-mcp-evals",
164 |       }),
165 |     ).toEqual({
166 |       organizationSlug: "sentry-mcp-evals",
167 |       issueId: "CLOUDFLARE-MCP-41",
168 |     });
169 |   });
170 | 
171 |   it("should throw if neither issueId nor issueUrl is provided", () => {
172 |     expect(() =>
173 |       parseIssueParams({ organizationSlug: "foo" }),
174 |     ).toThrowErrorMatchingInlineSnapshot(
175 |       `[UserInputError: Either issueId or issueUrl must be provided]`,
176 |     );
177 |   });
178 | 
179 |   it("should throw if organizationSlug is missing and no issueUrl", () => {
180 |     expect(() =>
181 |       parseIssueParams({ issueId: "123" }),
182 |     ).toThrowErrorMatchingInlineSnapshot(
183 |       `[UserInputError: Organization slug is required]`,
184 |     );
185 |   });
186 | 
187 |   it("should throw if issueUrl is invalid", () => {
188 |     expect(() =>
189 |       parseIssueParams({ issueUrl: "not-a-url" }),
190 |     ).toThrowErrorMatchingInlineSnapshot(
191 |       `[UserInputError: Invalid Sentry issue URL. Must start with http:// or https://]`,
192 |     );
193 |   });
194 | });
195 | 
```

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

```typescript
  1 | import { z } from "zod";
  2 | import { setTag } from "@sentry/core";
  3 | import { defineTool } from "../internal/tool-helpers/define";
  4 | import { apiServiceFromContext } from "../internal/tool-helpers/api";
  5 | import { logIssue } from "../telem/logging";
  6 | import { UserInputError } from "../errors";
  7 | import type { ServerContext } from "../types";
  8 | import type { Project } from "../api-client/index";
  9 | import {
 10 |   ParamOrganizationSlug,
 11 |   ParamRegionUrl,
 12 |   ParamProjectSlug,
 13 |   ParamPlatform,
 14 |   ParamTeamSlug,
 15 | } from "../schema";
 16 | 
 17 | export default defineTool({
 18 |   name: "update_project",
 19 |   requiredScopes: ["project:write"],
 20 |   description: [
 21 |     "Update project settings in Sentry, such as name, slug, platform, and team assignment.",
 22 |     "",
 23 |     "Be careful when using this tool!",
 24 |     "",
 25 |     "Use this tool when you need to:",
 26 |     "- Update a project's name or slug to fix onboarding mistakes",
 27 |     "- Change the platform assigned to a project",
 28 |     "- Update team assignment for a project",
 29 |     "",
 30 |     "<examples>",
 31 |     "### Update a project's name and slug",
 32 |     "",
 33 |     "```",
 34 |     "update_project(organizationSlug='my-organization', projectSlug='old-project', name='New Project Name', slug='new-project-slug')",
 35 |     "```",
 36 |     "",
 37 |     "### Assign a project to a different team",
 38 |     "",
 39 |     "```",
 40 |     "update_project(organizationSlug='my-organization', projectSlug='my-project', teamSlug='backend-team')",
 41 |     "```",
 42 |     "",
 43 |     "### Update platform",
 44 |     "",
 45 |     "```",
 46 |     "update_project(organizationSlug='my-organization', projectSlug='my-project', platform='python')",
 47 |     "```",
 48 |     "",
 49 |     "</examples>",
 50 |     "",
 51 |     "<hints>",
 52 |     "- If the user passes a parameter in the form of name/otherName, it's likely in the format of <organizationSlug>/<projectSlug>.",
 53 |     "- Team assignment is handled separately from other project settings",
 54 |     "- If any parameter is ambiguous, you should clarify with the user what they meant.",
 55 |     "- When updating the slug, the project will be accessible at the new slug after the update",
 56 |     "</hints>",
 57 |   ].join("\n"),
 58 |   inputSchema: {
 59 |     organizationSlug: ParamOrganizationSlug,
 60 |     regionUrl: ParamRegionUrl.optional(),
 61 |     projectSlug: ParamProjectSlug,
 62 |     name: z.string().trim().describe("The new name for the project").optional(),
 63 |     slug: z
 64 |       .string()
 65 |       .toLowerCase()
 66 |       .trim()
 67 |       .describe("The new slug for the project (must be unique)")
 68 |       .optional(),
 69 |     platform: ParamPlatform.optional(),
 70 |     teamSlug: ParamTeamSlug.optional().describe(
 71 |       "The team to assign this project to. Note: this will replace the current team assignment.",
 72 |     ),
 73 |   },
 74 |   annotations: {
 75 |     readOnlyHint: false,
 76 |     destructiveHint: true,
 77 |     idempotentHint: true,
 78 |     openWorldHint: true,
 79 |   },
 80 |   async handler(params, context: ServerContext) {
 81 |     const apiService = apiServiceFromContext(context, {
 82 |       regionUrl: params.regionUrl,
 83 |     });
 84 |     const organizationSlug = params.organizationSlug;
 85 | 
 86 |     setTag("organization.slug", organizationSlug);
 87 |     setTag("project.slug", params.projectSlug);
 88 | 
 89 |     // Handle team assignment separately if provided
 90 |     if (params.teamSlug) {
 91 |       setTag("team.slug", params.teamSlug);
 92 |       try {
 93 |         await apiService.addTeamToProject({
 94 |           organizationSlug,
 95 |           projectSlug: params.projectSlug,
 96 |           teamSlug: params.teamSlug,
 97 |         });
 98 |       } catch (err) {
 99 |         logIssue(err);
100 |         throw new Error(
101 |           `Failed to assign team ${params.teamSlug} to project ${params.projectSlug}: ${err instanceof Error ? err.message : "Unknown error"}`,
102 |         );
103 |       }
104 |     }
105 | 
106 |     // Update project settings if any are provided
107 |     const hasProjectUpdates = params.name || params.slug || params.platform;
108 | 
109 |     let project: Project | undefined;
110 |     if (hasProjectUpdates) {
111 |       try {
112 |         project = await apiService.updateProject({
113 |           organizationSlug,
114 |           projectSlug: params.projectSlug,
115 |           name: params.name,
116 |           slug: params.slug,
117 |           platform: params.platform,
118 |         });
119 |       } catch (err) {
120 |         logIssue(err);
121 |         throw new Error(
122 |           `Failed to update project ${params.projectSlug}: ${err instanceof Error ? err.message : "Unknown error"}`,
123 |         );
124 |       }
125 |     } else {
126 |       // If only team assignment, fetch current project data for display
127 |       const projects = await apiService.listProjects(organizationSlug);
128 |       project = projects.find((p) => p.slug === params.projectSlug);
129 |       if (!project) {
130 |         throw new UserInputError(`Project ${params.projectSlug} not found`);
131 |       }
132 |     }
133 | 
134 |     let output = `# Updated Project in **${organizationSlug}**\n\n`;
135 |     output += `**ID**: ${project.id}\n`;
136 |     output += `**Slug**: ${project.slug}\n`;
137 |     output += `**Name**: ${project.name}\n`;
138 |     if (project.platform) {
139 |       output += `**Platform**: ${project.platform}\n`;
140 |     }
141 | 
142 |     // Display what was updated
143 |     const updates: string[] = [];
144 |     if (params.name) updates.push(`name to "${params.name}"`);
145 |     if (params.slug) updates.push(`slug to "${params.slug}"`);
146 |     if (params.platform) updates.push(`platform to "${params.platform}"`);
147 |     if (params.teamSlug)
148 |       updates.push(`team assignment to "${params.teamSlug}"`);
149 | 
150 |     if (updates.length > 0) {
151 |       output += `\n## Updates Applied\n`;
152 |       output += updates.map((update) => `- Updated ${update}`).join("\n");
153 |       output += `\n`;
154 |     }
155 | 
156 |     output += "\n# Using this information\n\n";
157 |     output += `- The project is now accessible at slug: \`${project.slug}\`\n`;
158 |     if (params.teamSlug) {
159 |       output += `- The project is now assigned to the \`${params.teamSlug}\` team\n`;
160 |     }
161 |     return output;
162 |   },
163 | });
164 | 
```
Page 5/15FirstPrevNextLast