#
tokens: 48491/50000 30/408 files (page 4/11)
lines: off (toggle) GitHub
raw markdown copy
This is page 4 of 11. Use http://codebase.md/getsentry/sentry-mcp?page={x} to view the full context.

# Directory Structure

```
├── .claude
│   ├── agents
│   │   └── claude-optimizer.md
│   ├── commands
│   │   ├── gh-pr.md
│   │   └── gh-review.md
│   └── settings.json
├── .craft.yml
├── .cursor
│   ├── mcp.json
│   └── rules
├── .env.example
├── .github
│   └── workflows
│       ├── deploy.yml
│       ├── eval.yml
│       ├── merge-jobs.yml
│       ├── release.yml
│       ├── smoke-tests.yml
│       └── test.yml
├── .gitignore
├── .mcp.json
├── .vscode
│   ├── extensions.json
│   ├── mcp.json
│   └── settings.json
├── AGENTS.md
├── bin
│   └── bump-version.sh
├── biome.json
├── CLAUDE.md
├── codecov.yml
├── core
├── docs
│   ├── adding-tools.mdc
│   ├── api-patterns.mdc
│   ├── architecture.mdc
│   ├── cloudflare
│   │   ├── architecture.md
│   │   ├── constraint-do-analysis.md
│   │   ├── deployment.md
│   │   ├── mcpagent-architecture.md
│   │   ├── oauth-architecture.md
│   │   └── overview.md
│   ├── coding-guidelines.mdc
│   ├── common-patterns.mdc
│   ├── cursor.mdc
│   ├── deployment.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
│   ├── search-events-api-patterns.md
│   ├── security.mdc
│   ├── specs
│   │   ├── README.md
│   │   ├── search-events.md
│   │   └── subpath-constraints.md
│   └── testing.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
│   │   │   │   │       ├── 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-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-agent.ts
│   │   │   │   │   ├── slug-validation.test.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
│   │   ├── 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.ts
│   │   │   ├── telem
│   │   │   │   ├── index.ts
│   │   │   │   ├── logging.ts
│   │   │   │   ├── sentry.test.ts
│   │   │   │   └── sentry.ts
│   │   │   ├── test-setup.ts
│   │   │   ├── test-utils
│   │   │   │   └── context.ts
│   │   │   ├── toolDefinitions.ts
│   │   │   ├── tools
│   │   │   │   ├── analyze-issue-with-seer.test.ts
│   │   │   │   ├── analyze-issue-with-seer.ts
│   │   │   │   ├── create-dsn.test.ts
│   │   │   │   ├── create-dsn.ts
│   │   │   │   ├── create-project.test.ts
│   │   │   │   ├── create-project.ts
│   │   │   │   ├── create-team.test.ts
│   │   │   │   ├── create-team.ts
│   │   │   │   ├── find-dsns.test.ts
│   │   │   │   ├── find-dsns.ts
│   │   │   │   ├── find-organizations.test.ts
│   │   │   │   ├── find-organizations.ts
│   │   │   │   ├── find-projects.test.ts
│   │   │   │   ├── find-projects.ts
│   │   │   │   ├── find-releases.test.ts
│   │   │   │   ├── find-releases.ts
│   │   │   │   ├── find-teams.test.ts
│   │   │   │   ├── find-teams.ts
│   │   │   │   ├── get-doc.test.ts
│   │   │   │   ├── get-doc.ts
│   │   │   │   ├── get-event-attachment.test.ts
│   │   │   │   ├── get-event-attachment.ts
│   │   │   │   ├── get-issue-details.test.ts
│   │   │   │   ├── get-issue-details.ts
│   │   │   │   ├── get-trace-details.test.ts
│   │   │   │   ├── get-trace-details.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── search-docs.test.ts
│   │   │   │   ├── search-docs.ts
│   │   │   │   ├── search-events
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── formatters.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── utils.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── search-events.test.ts
│   │   │   │   ├── search-issues
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── formatters.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── README.md
│   │   │   │   ├── tools.test.ts
│   │   │   │   ├── types.ts
│   │   │   │   ├── update-issue.test.ts
│   │   │   │   ├── update-issue.ts
│   │   │   │   ├── update-project.test.ts
│   │   │   │   ├── update-project.ts
│   │   │   │   ├── 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/types.ts:
--------------------------------------------------------------------------------

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

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

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

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

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

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

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

export type AuthContextType = AuthState & AuthActions;

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

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

export type OAuthMessage = OAuthSuccessMessage | OAuthErrorMessage;

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

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

export type ToolMessage = ToolInvocationContent | ToolInvocationUnknownContent;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```

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

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

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

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

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

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

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

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

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

```

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

```typescript
import { z } from "zod";
import { setTag } from "@sentry/core";
import { defineTool } from "../internal/tool-helpers/define";
import { apiServiceFromContext } from "../internal/tool-helpers/api";
import type { ServerContext } from "../types";
import {
  ParamOrganizationSlug,
  ParamRegionUrl,
  ParamProjectSlugOrAll,
} from "../schema";

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

    setTag("organization.slug", organizationSlug);

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

```

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

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

```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/search-docs.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import { defineTool } from "../internal/tool-helpers/define";
import { fetchWithTimeout } from "../internal/fetch-utils";
import { ApiError } from "../api-client/index";
import type { ServerContext } from "../types";
import type { SearchResponse } from "./types";
import { ParamSentryGuide } from "../schema";

export default defineTool({
  name: "search_docs",
  requiredScopes: [], // Documentation search doesn't require specific scopes
  description: [
    "Search Sentry documentation for SDK setup, instrumentation, and configuration guidance.",
    "",
    "Use this tool when you need to:",
    "- Set up Sentry SDK in any language (Python, JavaScript, Go, Ruby, etc.)",
    "- Configure specific features like performance monitoring, error sampling, or release tracking",
    "- Implement custom instrumentation (spans, transactions, breadcrumbs)",
    "- Set up integrations with frameworks (Django, Flask, Express, Next.js, etc.)",
    "- Configure data scrubbing, filtering, or sampling rules",
    "- Troubleshoot SDK issues or find best practices",
    "",
    "This tool searches technical documentation, NOT general information about Sentry as a company.",
    "",
    "<examples>",
    "### Setting up Sentry in a Python Django app",
    "",
    "```",
    "search_docs(query='Django setup configuration SENTRY_DSN', guide='python/django')",
    "```",
    "",
    "### Setting up source maps for Next.js",
    "",
    "```",
    "search_docs(query='source maps webpack upload', guide='javascript/nextjs')",
    "```",
    "",
    "### Configuring release tracking",
    "",
    "```",
    "search_docs(query='release tracking deployment integration CI/CD')",
    "```",
    "</examples>",
    "",
    "<hints>",
    "- Use guide parameter to filter results to specific technologies (e.g., 'javascript' or 'javascript/nextjs')",
    "- Include the programming language/framework in your query for SDK-specific results",
    "- Use technical terms like 'instrumentation', 'spans', 'transactions' for performance docs",
    "- Include specific feature names like 'beforeSend', 'tracesSampleRate', 'SENTRY_DSN'",
    "</hints>",
  ].join("\n"),
  inputSchema: {
    query: z
      .string()
      .trim()
      .min(
        2,
        "Search query is too short. Please provide at least 2 characters.",
      )
      .max(
        200,
        "Search query is too long. Please keep your query under 200 characters.",
      )
      .describe(
        "The search query in natural language. Be specific about what you're looking for.",
      ),
    maxResults: z
      .number()
      .int()
      .min(1)
      .max(10)
      .default(3)
      .describe("Maximum number of results to return (1-10)")
      .optional(),
    guide: ParamSentryGuide.optional(),
  },
  annotations: {
    readOnlyHint: true,
    openWorldHint: true,
  },
  async handler(params, context: ServerContext) {
    let output = `# Documentation Search Results\n\n`;
    output += `**Query**: "${params.query}"\n`;
    if (params.guide) {
      output += `**Guide**: ${params.guide}\n`;
    }
    output += `\n`;

    // Determine the URL - use context.mcpUrl if available, otherwise default to production
    const host = context.mcpUrl || "https://mcp.sentry.dev";
    const searchUrl = new URL("/api/search", host);

    const response = await fetchWithTimeout(
      searchUrl.toString(),
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          query: params.query,
          maxResults: params.maxResults,
          guide: params.guide,
        }),
      },
      15000, // 15 second timeout
    );

    if (!response.ok) {
      // TODO: improve error responses with types
      const errorData = (await response.json().catch(() => null)) as {
        error?: string;
      } | null;

      const errorMessage =
        errorData?.error || `Search failed with status ${response.status}`;
      throw new ApiError(errorMessage, response.status);
    }

    const data = (await response.json()) as SearchResponse;

    // Handle error in response
    if ("error" in data && data.error) {
      output += `**Error**: ${data.error}\n\n`;
      return output;
    }

    // Display results
    if (data.results.length === 0) {
      output += "No documentation found matching your query.\n\n";
      return output;
    }

    output += `Found ${data.results.length} match${data.results.length === 1 ? "" : "es"}\n\n`;

    output += `These are just snippets. Use \`get_doc(path='...')\` to fetch the full content.\n\n`;

    for (const [index, result] of data.results.entries()) {
      output += `## ${index + 1}. ${result.url}\n\n`;
      output += `**Path**: ${result.id}\n`;
      output += `**Relevance**: ${(result.relevance * 100).toFixed(1)}%\n\n`;
      if (index < 3) {
        output += "**Matching Context**\n";
        output += `> ${result.snippet.replace(/\n/g, "\n> ")}\n\n`;
      }
    }

    return output;
  },
});

```

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

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

```

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

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

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

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

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

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

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

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

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

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

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

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

    await this.saveConfig(config);
  }

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

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

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

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

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

    return clientConfig.accessToken;
  }

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

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

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

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

    await this.saveConfig(config);
  }

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

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

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

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

    await this.saveConfig(config);
  }

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

```

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

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

```

--------------------------------------------------------------------------------
/docs/cloudflare/constraint-do-analysis.md:
--------------------------------------------------------------------------------

```markdown
# Analysis: Constraint-Based Durable Objects

## Current Architecture

Currently:
- **One DO per user**: All requests from same user go to same DO instance
- **Constraints change dynamically**: Same DO handles `/mcp/org1` and `/mcp/org2`
- **Server reconfiguration required**: When constraints change, we reconfigure the MCP server

## Proposed Alternative: Constraint-Based DOs

Create separate DO instances based on user + constraints:

```
DO ID = hash(userId + organizationSlug + projectSlug)
```

### Examples

User "alice" (ID: "user-123"):
- `/mcp` → DO ID: `user-123`
- `/mcp/sentry` → DO ID: `user-123:sentry`
- `/mcp/sentry/javascript` → DO ID: `user-123:sentry:javascript`
- `/mcp/acme` → DO ID: `user-123:acme` (different DO!)

## Pros

1. **No reconfiguration needed**: Each DO is configured once with its constraints
2. **Better isolation**: Different contexts are truly isolated
3. **Simpler code**: No need to detect constraint changes
4. **Natural caching**: Each context maintains its own state
5. **Parallel processing**: User can work in multiple contexts simultaneously

## Cons

1. **More DO instances**: Multiplies DOs by number of unique constraint combinations
2. **State fragmentation**: User state split across multiple DOs
3. **Cold starts**: Each new context requires DO initialization
4. **Memory usage**: More DOs = more memory overhead
5. **Billing implications**: More DO instances and storage usage

## Implementation Challenges

### 1. OAuth Provider Integration

The OAuth provider currently:
- Decrypts token and extracts props (userId, accessToken)
- Creates DO based on these props
- Passes props to the DO

To include constraints, we'd need to:
- Extract constraints BEFORE creating the DO
- Include constraints in the DO ID generation
- Pass constraints as part of props or separately

### 2. Agents Library Limitations

The `McpAgent.serve()` and `McpAgent.serveSSE()` methods:
- Are static methods that return handlers
- Don't have access to request context when creating DO ID
- Rely on OAuth provider to handle DO creation

We'd need to either:
- Modify how the agents library creates DOs (not feasible - external dependency)
- Intercept DO creation in the OAuth provider (complex)
- Create a custom wrapper that manages DO routing (very complex)

### 3. URL Rewriting Issue

The agents library rewrites URLs to `/streamable-http`, so:
- Original path with constraints is lost
- We currently use headers to pass constraints
- With constraint-based DOs, we'd need to ensure correct DO is selected BEFORE URL rewriting

## Feasibility Assessment

### Option 1: Modify OAuth Provider (Complex)

```typescript
// In OAuth provider's apiHandler processing
const extractConstraintsFromPath = (path: string) => {
  // Extract org/project from path
  return { org, project };
};

const createDOId = (userId: string, constraints: any) => {
  if (!constraints.org) return userId;
  if (!constraints.project) return `${userId}:${constraints.org}`;
  return `${userId}:${constraints.org}:${constraints.project}`;
};

// When creating DO
const constraints = extractConstraintsFromPath(request.url);
const doId = createDOId(props.userId, constraints);
const doInstance = env.MCP_OBJECT.idFromName(doId);
```

**Problem**: OAuth provider is external - we can't modify it easily.

### Option 2: Custom DO Router (Very Complex)

Create an intermediate DO that routes to constraint-specific DOs:

```typescript
class MCPRouter extends DurableObject {
  async fetch(request: Request) {
    const constraints = extractConstraints(request);
    const targetDOId = createConstraintBasedId(this.userId, constraints);
    const targetDO = this.env.MCP_OBJECT.idFromName(targetDOId);
    
    // Forward request to constraint-specific DO
    return this.env.MCP_OBJECT.get(targetDO).fetch(request);
  }
}
```

**Problems**:
- Adds complexity and latency
- Requires significant architectural changes
- Still need to handle OAuth integration

### Option 3: Keep Current Approach (Recommended)

The current approach of reconfiguring on constraint changes is actually simpler because:
- Works with existing OAuth provider
- Works with existing agents library
- Single source of truth for user state
- Already implemented and working

## Recommendation

**Keep the current architecture** with server reconfiguration because:

1. **Constraint changes are infrequent**: Users typically work in one context for extended periods
2. **Reconfiguration is fast**: Creating new McpServer instance is lightweight
3. **Simpler architecture**: One DO per user is easier to reason about
4. **Works with existing dependencies**: No need to modify OAuth provider or agents library
5. **Lower operational overhead**: Fewer DOs to manage and monitor

The performance cost of reconfiguration (milliseconds) is negligible compared to the complexity of implementing constraint-based DOs.

## Alternative Optimization

If reconfiguration performance becomes an issue, consider:

1. **Lazy reconfiguration**: Only reconfigure when actually calling a tool, not on every request
2. **Multiple server instances**: Keep a cache of configured servers for recent constraints
3. **Optimized reconfiguration**: Find ways to update constraints without full server recreation

But these optimizations should only be implemented if performance profiling shows they're needed.
```

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

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

```

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

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

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

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

      ## Changes Made

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

      ## Current Status

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

      # Using this information

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

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

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

      ## Changes Made

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

      ## Current Status

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

      # Using this information

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

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

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

      ## Changes Made

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

      ## Current Status

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

      # Using this information

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

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

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

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

```

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

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

```

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

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

```

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

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

```

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

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

```

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

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

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

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

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

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

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

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

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

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

    return false;
  }, []);

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

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

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

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

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

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

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

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

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

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

  return {
    initialMessages,
    saveMessages,
    clearPersistedMessages,
  };
}

```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```

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

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

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

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

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

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

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

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

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

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

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

```

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

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

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

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

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

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

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

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

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

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

  return { attributes: customAttributes, fieldTypes };
}

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

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

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

      const recommendedFields = RECOMMENDED_FIELDS[dataset];

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

      return `Dataset: ${dataset}

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

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

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

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

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

```

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

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

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

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

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

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

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

    setTag("organization.slug", orgSlug);

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

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

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

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

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

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

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

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

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

    return output;
  },
});

```

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

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

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

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

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

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

    // XXX(dcramer): we want to confirm permissions on each time
    // so you can always choose new ones
    // This shouldn't be highly visible to users, as clients should use refresh tokens
    // behind the scenes.
    //
    // because we share a clientId with the upstream provider, we need to ensure that the
    // downstream client has been approved by the end-user (e.g. for a new client)
    // https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/265
    // const isApproved = await clientIdAlreadyApproved(
    //   c.req.raw,
    //   clientId,
    //   c.env.COOKIE_SECRET,
    // );
    // if (isApproved) {
    //   return redirectToUpstream(c.env, c.req.raw, oauthReqInfo);
    // }

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

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

    const { state, headers, permissions } = result;

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

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

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

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

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

```

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

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

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

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

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

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

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

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

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

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

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

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

```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```

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

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

```

--------------------------------------------------------------------------------
/packages/mcp-cloudflare/src/server/lib/mcp-agent.ts:
--------------------------------------------------------------------------------

```typescript
import * as Sentry from "@sentry/cloudflare";
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { configureServer } from "@sentry/mcp-server/server";
import { expandScopes, parseScopes } from "@sentry/mcp-server/permissions";
import { logWarn } from "@sentry/mcp-server/telem/logging";
import type { Env, WorkerProps } from "../types";
import type { Constraints } from "@sentry/mcp-server/types";
import { LIB_VERSION } from "@sentry/mcp-server/version";
import getSentryConfig from "../sentry.config";
import { verifyConstraintsAccess } from "./constraint-utils";
import type { ExecutionContext } from "@cloudflare/workers-types";

/**
 * Sentry MCP Agent - A Durable Object that provides Model Context Protocol access to Sentry.
 *
 * This class extends the Cloudflare agents library McpAgent to provide authenticated,
 * constraint-scoped access to Sentry's API through MCP tools.
 *
 * ARCHITECTURE:
 *
 * Each MCP client connection creates a unique Durable Object instance via sessionId.
 * The agents library generates sessionIds based on the connection context, ensuring
 * that different constraint contexts (e.g., /mcp/org1/proj1 vs /mcp/org2/proj2)
 * get separate DO instances with immutable configurations.
 *
 * LIFECYCLE:
 *
 * 1. Connection: MCP client connects to URL like /mcp/sentry/my-project
 * 2. Authentication: OAuth flow provides user credentials and permissions
 * 3. DO Creation: Agents library creates DO with unique sessionId for this context
 * 4. Initialization: init() configures MCP server with user auth and URL constraints
 * 5. Request Handling: fetch() processes MCP protocol messages (tools)
 * 6. Hibernation: DO persists state and hibernates after inactivity
 * 7. Recovery: init() restores state when DO wakes from hibernation
 *
 * CONSTRAINT SCOPING:
 *
 * URL paths like /mcp/sentry/my-project are parsed to extract organization and project
 * constraints. These constraints scope all Sentry API calls to the specific org/project
 * context, ensuring users can only access data they're authorized for within that scope.
 *
 * Static serve() methods extract constraints from URLs and mutate props before DO instantiation,
 * ensuring immutable constraint configuration throughout the DO's lifetime.
 *
 * AUTHENTICATION:
 *
 * User authentication flows through OAuth, with credentials stored in encrypted props
 * that persist across DO hibernation cycles. The DO uses these credentials to authenticate
 * all Sentry API requests on behalf of the MCP client.
 */

/**
 * Sentry MCP Agent - A Durable Object that provides Model Context Protocol access to Sentry.
 *
 * Following the pattern from the gist: this class extends McpAgent and mutates props
 * directly on first request to include URL-extracted constraints. Props are then immutable
 * throughout the DO's lifetime.
 */
class SentryMCPBase extends McpAgent<
  Env,
  {
    constraints?: Constraints;
  },
  WorkerProps & {
    organizationSlug?: string;
    projectSlug?: string;
  }
> {
  // Create server once in constructor, as per Cloudflare MCP Agent API docs
  server = new McpServer({
    name: "Sentry MCP",
    version: LIB_VERSION,
  });

  // biome-ignore lint/complexity/noUselessConstructor: Need the constructor to match the durable object types.
  constructor(state: DurableObjectState, env: Env) {
    super(state, env);
  }

  /**
   * Initialize Durable Object state and MCP server configuration
   *
   * Called when the DO is first created or wakes from hibernation.
   * Configures the MCP server with user authentication and constraint scoping.
   *
   * Since props (including constraints) are set on first request and then immutable,
   * the server configuration remains constant throughout the DO's lifetime.
   */
  async init() {
    if (!this.state?.constraints) {
      this.setState({
        constraints: this.props.constraints,
      });
    }

    await configureServer({
      server: this.server,
      context: {
        userId: this.props.id,
        mcpUrl: process.env.MCP_URL,
        accessToken: this.props.accessToken,
        grantedScopes: this.props.grantedScopes
          ? (() => {
              const { valid, invalid } = parseScopes(this.props.grantedScopes);
              if (invalid.length > 0) {
                logWarn(`Ignoring invalid scopes from OAuth provider`, {
                  loggerScope: ["cloudflare", "mcp-agent"],
                  extra: {
                    invalidScopes: invalid,
                  },
                });
              }
              return expandScopes(valid);
            })()
          : undefined,
        constraints: this.state.constraints || {},
      },
      onToolComplete: () => {
        this.ctx.waitUntil(Sentry.flush(2000));
      },
    });
  }
}

export const SentryMCP = Sentry.instrumentDurableObjectWithSentry(
  getSentryConfig.partial({
    initialScope: {
      tags: {
        durable_object: true,
        "mcp.server_version": LIB_VERSION,
      },
    },
  }),
  SentryMCPBase,
);

export default {
  async fetch(request: Request, env: unknown, ctx: ExecutionContext) {
    const url = new URL(request.url);

    if (url.pathname === "/sse" || url.pathname === "/sse/message") {
      return SentryMCP.serveSSE("/sse").fetch(request, env, ctx);
    }

    const pattern = new URLPattern({ pathname: "/mcp/:org?/:project?" });
    const result = pattern.exec(url);
    if (result) {
      const { groups } = result.pathname;

      const organizationSlug = groups?.org ?? "";
      const projectSlug = groups?.project ?? "";

      // Verify access to org/project using OAuth token
      const verification = await verifyConstraintsAccess(
        { organizationSlug, projectSlug },
        {
          // @ts-ignore props is provided by OAuth provider → agents library
          accessToken: ctx.props?.accessToken,
          sentryHost: (env as any)?.SENTRY_HOST || "sentry.io",
        },
      );
      if (!verification.ok) {
        return new Response(verification.message, {
          status: verification.status ?? 500,
        });
      }

      ctx.props.constraints = verification.constraints;

      return SentryMCP.serve(pattern.pathname).fetch(request, env, ctx);
    }

    return new Response("Not found", { status: 404 });
  },
};

```

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

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

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

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

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

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

```

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

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

const POPUP_CHECK_INTERVAL = 1000;

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

interface AuthProviderProps {
  children: ReactNode;
}

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

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

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

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

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

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

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

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

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

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

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

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

    popupRef.current = popup;

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

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

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

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

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

    setIsAuthenticated(false);
  }, []);

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

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

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

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

```

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

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

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

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

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

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

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

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

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

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

      return contentParts;
    }

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

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

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

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

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

    return output;
  },
});

```

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

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

```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      const event: Sentry.Event = {
        message: "Deep nesting test",
        extra: deep,
      };

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

```
Page 4/11FirstPrevNextLast