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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/packages/mcp-core/src/internal/formatting.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from "vitest";
import { formatEventOutput, formatFrameHeader } from "./formatting";
import type { Event } from "../api-client/types";
import {
  EventBuilder,
  createFrame,
  frameFactories,
  createStackTrace,
  createExceptionValue,
  createThread,
  testEvents,
  createFrameWithContext,
} from "./test-fixtures";

// Helper functions to reduce duplication in event creation
function createPythonExceptionEvent(
  errorType: string,
  errorMessage: string,
  frames: any[],
): Event {
  return new EventBuilder("python")
    .withException(
      createExceptionValue({
        type: errorType,
        value: errorMessage,
        stacktrace: createStackTrace(frames),
      }),
    )
    .build();
}

function createSimpleExceptionEvent(
  platform: string,
  errorType: string,
  errorMessage: string,
  frame: any,
): Event {
  const builder = new EventBuilder(platform);
  // Remove the contexts property to avoid "Additional Context" section
  const event = builder
    .withException(
      createExceptionValue({
        type: errorType,
        value: errorMessage,
        stacktrace: createStackTrace([frame]),
      }),
    )
    .build();
  // Remove contexts to match original test expectations
  event.contexts = undefined;
  return event;
}

describe("formatFrameHeader", () => {
  it("uses platform as fallback when language detection fails", () => {
    // Frame with no clear language indicators
    const unknownFrame = {
      filename: "/path/to/file.unknown",
      function: "someFunction",
      lineNo: 42,
    };

    // Without platform - should use generic format
    expect(formatFrameHeader(unknownFrame)).toBe(
      "    at someFunction (/path/to/file.unknown:42)",
    );

    // With platform python - should use Python format
    expect(formatFrameHeader(unknownFrame, undefined, "python")).toBe(
      '  File "/path/to/file.unknown", line 42, in someFunction',
    );

    // With platform java - should use Java format
    expect(formatFrameHeader(unknownFrame, undefined, "java")).toBe(
      "at UnknownClass.someFunction(/path/to/file.unknown:42)",
    );
  });
  it("formats Java stack traces correctly", () => {
    // With module and filename
    const javaFrame1 = {
      module: "com.example.ClassName",
      function: "methodName",
      filename: "ClassName.java",
      lineNo: 123,
    };
    expect(formatFrameHeader(javaFrame1)).toBe(
      "at com.example.ClassName.methodName(ClassName.java:123)",
    );

    // Without filename (common in Java) - needs platform hint
    const javaFrame2 = {
      module: "com.example.ClassName",
      function: "methodName",
      lineNo: 123,
    };
    expect(formatFrameHeader(javaFrame2, undefined, "java")).toBe(
      "at com.example.ClassName.methodName(Unknown Source:123)",
    );
  });

  it("formats Python stack traces correctly", () => {
    const pythonFrame = {
      filename: "/path/to/file.py",
      function: "function_name",
      lineNo: 42,
    };
    expect(formatFrameHeader(pythonFrame)).toBe(
      '  File "/path/to/file.py", line 42, in function_name',
    );

    // Module only (no filename) - needs platform hint
    const pythonModuleFrame = {
      module: "mymodule",
      function: "function_name",
      lineNo: 42,
    };
    expect(formatFrameHeader(pythonModuleFrame, undefined, "python")).toBe(
      '  File "mymodule", line 42, in function_name',
    );
  });

  it("formats JavaScript stack traces correctly", () => {
    // With column number
    const jsFrame1 = {
      filename: "/path/to/file.js",
      function: "functionName",
      lineNo: 10,
      colNo: 15,
    };
    expect(formatFrameHeader(jsFrame1)).toBe(
      "/path/to/file.js:10:15 (functionName)",
    );

    // Without column number but .js extension
    const jsFrame2 = {
      filename: "/path/to/file.js",
      function: "functionName",
      lineNo: 10,
    };
    expect(formatFrameHeader(jsFrame2)).toBe(
      "/path/to/file.js:10 (functionName)",
    );

    // Anonymous function (no function name)
    const jsFrame3 = {
      filename: "/path/to/file.js",
      lineNo: 10,
      colNo: 15,
    };
    expect(formatFrameHeader(jsFrame3)).toBe("/path/to/file.js:10:15");
  });

  it("formats Ruby stack traces correctly", () => {
    const rubyFrame = {
      filename: "/path/to/file.rb",
      function: "method_name",
      lineNo: 42,
    };
    expect(formatFrameHeader(rubyFrame)).toBe(
      "    from /path/to/file.rb:42:in `method_name`",
    );

    // Without function name
    const rubyFrame2 = {
      filename: "/path/to/file.rb",
      lineNo: 42,
    };
    expect(formatFrameHeader(rubyFrame2)).toBe(
      "    from /path/to/file.rb:42:in",
    );
  });

  it("formats PHP stack traces correctly", () => {
    // With frame index
    const phpFrame1 = {
      filename: "/path/to/file.php",
      function: "functionName",
      lineNo: 42,
    };
    expect(formatFrameHeader(phpFrame1, 0)).toBe(
      "#0 /path/to/file.php(42): functionName()",
    );

    // Without frame index
    const phpFrame2 = {
      filename: "/path/to/file.php",
      function: "functionName",
      lineNo: 42,
    };
    expect(formatFrameHeader(phpFrame2)).toBe(
      "/path/to/file.php(42): functionName()",
    );
  });

  it("formats unknown languages with generic format", () => {
    const unknownFrame = {
      filename: "/path/to/file.unknown",
      function: "someFunction",
      lineNo: 42,
    };
    expect(formatFrameHeader(unknownFrame)).toBe(
      "    at someFunction (/path/to/file.unknown:42)",
    );
  });

  it("prioritizes duck typing over platform when clear indicators exist", () => {
    // Java file but platform says python - should use Java format
    const javaFrame = {
      filename: "Example.java",
      module: "com.example.Example",
      function: "doSomething",
      lineNo: 42,
    };
    expect(formatFrameHeader(javaFrame, undefined, "python")).toBe(
      "at com.example.Example.doSomething(Example.java:42)",
    );

    // Python file but platform says java - should use Python format
    const pythonFrame = {
      filename: "/app/example.py",
      function: "do_something",
      lineNo: 42,
    };
    expect(formatFrameHeader(pythonFrame, undefined, "java")).toBe(
      '  File "/app/example.py", line 42, in do_something',
    );
  });
});

describe("formatEventOutput", () => {
  it("formats Java thread stack traces correctly", () => {
    const event = testEvents.javaThreadError(
      "Cannot use this function, please use update(String sql, PreparedStatementSetter pss) instead",
    );

    const output = formatEventOutput(event);

    expect(output).toMatchInlineSnapshot(`
      "### Error

      \`\`\`
      Cannot use this function, please use update(String sql, PreparedStatementSetter pss) instead
      \`\`\`

      **Thread** (CONTRACT_WORKER)

      **Stacktrace:**
      \`\`\`
      at java.lang.Thread.run(Thread.java:833)
      at com.citics.eqd.mq.aeron.AeronServer.lambda$start$3(AeronServer.java:110)
      \`\`\`
      "
    `);
  });

  it("formats Python exception traces correctly", () => {
    const event = testEvents.pythonException("Invalid value");

    const output = formatEventOutput(event);

    expect(output).toMatchInlineSnapshot(`
      "### Error

      \`\`\`
      ValueError: Invalid value
      \`\`\`

      **Stacktrace:**
      \`\`\`
        File "/app/main.py", line 42, in process_data
        File "/app/utils.py", line 15, in validate
      \`\`\`

      "
    `);
  });

  it("should render enhanced in-app frame with context lines", () => {
    const event = new EventBuilder("python")
      .withException(
        createExceptionValue({
          type: "ValueError",
          value: "Something went wrong",
          stacktrace: createStackTrace([
            createFrame({
              filename: "/usr/lib/python3.8/json/__init__.py",
              function: "loads",
              lineNo: 357,
              inApp: false,
            }),
            createFrameWithContext(
              {
                filename: "/app/services/payment.py",
                function: "process_payment",
                lineNo: 42,
              },
              [
                [37, "    def process_payment(self, amount, user_id):"],
                [38, "        user = self.get_user(user_id)"],
                [39, "        if not user:"],
                [40, '            raise ValueError("User not found")'],
                [41, "        "],
                [42, "        balance = user.account.balance"],
                [43, "        if balance < amount:"],
                [44, "            raise InsufficientFundsError()"],
                [45, "        "],
                [46, "        transaction = Transaction(user, amount)"],
              ],
            ),
          ]),
        }),
      )
      .build();

    const output = formatEventOutput(event);

    expect(output).toMatchInlineSnapshot(`
      "### Error

      \`\`\`
      ValueError: Something went wrong
      \`\`\`

      **Most Relevant Frame:**
      ─────────────────────
        File "/app/services/payment.py", line 42, in process_payment

          39 │         if not user:
          40 │             raise ValueError("User not found")
          41 │         
        → 42 │         balance = user.account.balance
          43 │         if balance < amount:
          44 │             raise InsufficientFundsError()
          45 │         

      **Full Stacktrace:**
      ────────────────
      \`\`\`
        File "/usr/lib/python3.8/json/__init__.py", line 357, in loads
        File "/app/services/payment.py", line 42, in process_payment
              balance = user.account.balance
      \`\`\`

      "
    `);
  });

  it("should render enhanced in-app frame with variables", () => {
    const event = new EventBuilder("python")
      .withException(
        createExceptionValue({
          type: "ValueError",
          value: "Something went wrong",
          stacktrace: createStackTrace([
            createFrame({
              filename: "/app/services/payment.py",
              function: "process_payment",
              lineNo: 42,
              inApp: true,
              vars: {
                amount: 150.0,
                user_id: "usr_123456",
                user: null,
                self: { type: "PaymentService", id: 1234 },
              },
            }),
          ]),
        }),
      )
      .build();

    const output = formatEventOutput(event);

    expect(output).toMatchInlineSnapshot(`
      "### Error

      \`\`\`
      ValueError: Something went wrong
      \`\`\`

      **Most Relevant Frame:**
      ─────────────────────
        File "/app/services/payment.py", line 42, in process_payment

      Local Variables:
      ├─ amount: 150
      ├─ user_id: "usr_123456"
      ├─ user: null
      └─ self: {"type":"PaymentService","id":1234}

      **Full Stacktrace:**
      ────────────────
      \`\`\`
        File "/app/services/payment.py", line 42, in process_payment
      \`\`\`

      "
    `);
  });

  it("should handle frames without in-app or enhanced data", () => {
    const event = new EventBuilder("python")
      .withException(
        createExceptionValue({
          type: "ValueError",
          value: "Something went wrong",
          stacktrace: createStackTrace([
            frameFactories.python({ lineNo: 10, function: "main" }),
          ]),
        }),
      )
      .build();

    const output = formatEventOutput(event);

    expect(output).toMatchInlineSnapshot(`
      "### Error

      \`\`\`
      ValueError: Something went wrong
      \`\`\`

      **Stacktrace:**
      \`\`\`
        File "/app/main.py", line 10, in main
      \`\`\`

      "
    `);
  });

  it("should work with thread interface containing in-app frame", () => {
    const event = new EventBuilder("java")
      .withThread(
        createThread({
          id: 1,
          crashed: true,
          name: "main",
          stacktrace: createStackTrace([
            frameFactories.java({
              module: "java.lang.Thread",
              function: "run",
              filename: "Thread.java",
              lineNo: 748,
              inApp: false,
            }),
            createFrameWithContext(
              {
                module: "com.example.PaymentService",
                function: "processPayment",
                filename: "PaymentService.java",
                lineNo: 42,
              },
              [
                [40, "        User user = getUser(userId);"],
                [41, "        if (user == null) {"],
                [42, "            throw new UserNotFoundException(userId);"],
                [43, "        }"],
                [44, "        return user.getBalance();"],
              ],
              {
                userId: "12345",
                user: null,
              },
            ),
          ]),
        }),
      )
      .build();

    const output = formatEventOutput(event);

    expect(output).toMatchInlineSnapshot(`
      "**Thread** (main)

      **Most Relevant Frame:**
      ─────────────────────
      at com.example.PaymentService.processPayment(PaymentService.java:42)

          40 │         User user = getUser(userId);
          41 │         if (user == null) {
        → 42 │             throw new UserNotFoundException(userId);
          43 │         }
          44 │         return user.getBalance();

      Local Variables:
      ├─ userId: "12345"
      └─ user: null

      **Full Stacktrace:**
      ────────────────
      \`\`\`
      at java.lang.Thread.run(Thread.java:748)
      at com.example.PaymentService.processPayment(PaymentService.java:42)
                  throw new UserNotFoundException(userId);
      \`\`\`
      "
    `);
  });

  describe("Enhanced frame rendering variations", () => {
    it("should handle Python format with enhanced frame", () => {
      const event = createSimpleExceptionEvent(
        "python",
        "AttributeError",
        "'NoneType' object has no attribute 'balance'",
        createFrameWithContext(
          {
            filename: "/app/models/user.py",
            function: "get_balance",
            lineNo: 25,
          },
          [
            [23, "    def get_balance(self):"],
            [24, "        # This will fail if account is None"],
            [25, "        return self.account.balance"],
            [26, ""],
            [27, "    def set_balance(self, amount):"],
          ],
          {
            self: { id: 123, account: null },
          },
        ),
      );

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        AttributeError: 'NoneType' object has no attribute 'balance'
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
          File "/app/models/user.py", line 25, in get_balance

            23 │     def get_balance(self):
            24 │         # This will fail if account is None
          → 25 │         return self.account.balance
            26 │ 
            27 │     def set_balance(self, amount):

        Local Variables:
        └─ self: {"id":123,"account":null}

        **Full Stacktrace:**
        ────────────────
        \`\`\`
          File "/app/models/user.py", line 25, in get_balance
                return self.account.balance
        \`\`\`

        "
      `);
    });

    it("should handle JavaScript format with enhanced frame", () => {
      const event = createSimpleExceptionEvent(
        "javascript",
        "TypeError",
        "Cannot read property 'name' of undefined",
        createFrameWithContext(
          {
            filename: "/src/components/UserProfile.tsx",
            function: "UserProfile",
            lineNo: 15,
            colNo: 28,
          },
          [
            [
              13,
              "export const UserProfile: React.FC<Props> = ({ userId }) => {",
            ],
            [14, "  const user = useUser(userId);"],
            [15, "  const displayName = user.profile.name;"],
            [16, "  "],
            [17, "  return ("],
          ],
          {
            userId: "usr_123",
            user: undefined,
            displayName: undefined,
          },
        ),
      );

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        TypeError: Cannot read property 'name' of undefined
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
        /src/components/UserProfile.tsx:15:28 (UserProfile)

            13 │ export const UserProfile: React.FC<Props> = ({ userId }) => {
            14 │   const user = useUser(userId);
          → 15 │   const displayName = user.profile.name;
            16 │   
            17 │   return (

        Local Variables:
        ├─ userId: "usr_123"
        ├─ user: undefined
        └─ displayName: undefined

        **Full Stacktrace:**
        ────────────────
        \`\`\`
        /src/components/UserProfile.tsx:15:28 (UserProfile)
          const displayName = user.profile.name;
        \`\`\`

        "
      `);
    });

    it("should handle Ruby format with enhanced frame", () => {
      const event = new EventBuilder("ruby")
        .withException(
          createExceptionValue({
            type: "NoMethodError",
            value: "undefined method `charge' for nil:NilClass",
            stacktrace: createStackTrace([
              createFrameWithContext(
                {
                  filename: "/app/services/payment_service.rb",
                  function: "process_payment",
                  lineNo: 8,
                },
                [
                  [6, "  def process_payment(amount)"],
                  [7, "    payment_method = user.payment_method"],
                  [8, "    payment_method.charge(amount)"],
                  [9, "  rescue => e"],
                  [10, "    Rails.logger.error(e)"],
                ],
                {
                  amount: 99.99,
                  payment_method: null,
                },
              ),
            ]),
          }),
        )
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        NoMethodError: undefined method \`charge' for nil:NilClass
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
            from /app/services/payment_service.rb:8:in \`process_payment\`

             6 │   def process_payment(amount)
             7 │     payment_method = user.payment_method
          →  8 │     payment_method.charge(amount)
             9 │   rescue => e
            10 │     Rails.logger.error(e)

        Local Variables:
        ├─ amount: 99.99
        └─ payment_method: null

        **Full Stacktrace:**
        ────────────────
        \`\`\`
            from /app/services/payment_service.rb:8:in \`process_payment\`
            payment_method.charge(amount)
        \`\`\`

        "
      `);
    });

    it("should handle PHP format with enhanced frame", () => {
      const event = new EventBuilder("php")
        .withException(
          createExceptionValue({
            type: "Error",
            value: "Call to a member function getName() on null",
            stacktrace: createStackTrace([
              createFrameWithContext(
                {
                  filename: "/var/www/app/User.php",
                  function: "getDisplayName",
                  lineNo: 45,
                },
                [
                  [43, "    public function getDisplayName() {"],
                  [44, "        $profile = $this->getProfile();"],
                  [45, "        return $profile->getName();"],
                  [46, "    }"],
                ],
                {
                  profile: null,
                },
              ),
            ]),
          }),
        )
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        Error: Call to a member function getName() on null
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
        /var/www/app/User.php(45): getDisplayName()

            43 │     public function getDisplayName() {
            44 │         $profile = $this->getProfile();
          → 45 │         return $profile->getName();
            46 │     }

        Local Variables:
        └─ profile: null

        **Full Stacktrace:**
        ────────────────
        \`\`\`
        /var/www/app/User.php(45): getDisplayName()
                return $profile->getName();
        \`\`\`

        "
      `);
    });

    it("should handle frame with context but no vars", () => {
      const event = new EventBuilder("python")
        .withException(
          createExceptionValue({
            type: "ValueError",
            value: "Invalid configuration",
            stacktrace: createStackTrace([
              createFrameWithContext(
                {
                  filename: "/app/config.py",
                  function: "load_config",
                  lineNo: 12,
                },
                [
                  [10, "def load_config():"],
                  [11, "    if not os.path.exists(CONFIG_FILE):"],
                  [12, "        raise ValueError('Invalid configuration')"],
                  [13, "    with open(CONFIG_FILE) as f:"],
                  [14, "        return json.load(f)"],
                ],
              ),
            ]),
          }),
        )
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        ValueError: Invalid configuration
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
          File "/app/config.py", line 12, in load_config

            10 │ def load_config():
            11 │     if not os.path.exists(CONFIG_FILE):
          → 12 │         raise ValueError('Invalid configuration')
            13 │     with open(CONFIG_FILE) as f:
            14 │         return json.load(f)

        **Full Stacktrace:**
        ────────────────
        \`\`\`
          File "/app/config.py", line 12, in load_config
                raise ValueError('Invalid configuration')
        \`\`\`

        "
      `);
    });

    it("should handle frame with vars but no context", () => {
      const event = createSimpleExceptionEvent(
        "python",
        "TypeError",
        "unsupported operand type(s)",
        createFrame({
          filename: "/app/calculator.py",
          function: "divide",
          lineNo: 5,
          inApp: true,
          vars: {
            numerator: 10,
            denominator: "0",
            result: undefined,
          },
        }),
      );

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        TypeError: unsupported operand type(s)
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
          File "/app/calculator.py", line 5, in divide

        Local Variables:
        ├─ numerator: 10
        ├─ denominator: "0"
        └─ result: undefined

        **Full Stacktrace:**
        ────────────────
        \`\`\`
          File "/app/calculator.py", line 5, in divide
        \`\`\`

        "
      `);
    });

    it("should handle complex variable types", () => {
      const event = createSimpleExceptionEvent(
        "python",
        "KeyError",
        "'missing_key'",
        createFrame({
          filename: "/app/processor.py",
          function: "process_data",
          lineNo: 30,
          inApp: true,
          vars: {
            string_var: "hello world",
            number_var: 42,
            float_var: 3.14,
            bool_var: true,
            null_var: null,
            undefined_var: undefined,
            array_var: [1, 2, 3],
            object_var: { type: "User", id: 123 },
            nested_object: {
              user: { name: "John", age: 30 },
              settings: { theme: "dark" },
            },
            empty_string: "",
            zero: 0,
            false_bool: false,
            long_string:
              "This is a very long string that should be handled properly in the output",
          },
        }),
      );

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        KeyError: 'missing_key'
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
          File "/app/processor.py", line 30, in process_data

        Local Variables:
        ├─ string_var: "hello world"
        ├─ number_var: 42
        ├─ float_var: 3.14
        ├─ bool_var: true
        ├─ null_var: null
        ├─ undefined_var: undefined
        ├─ array_var: [1,2,3]
        ├─ object_var: {"type":"User","id":123}
        ├─ nested_object: {"user":{"name":"John","age":30},"settings":{"theme":"dark"}}
        ├─ empty_string: ""
        ├─ zero: 0
        ├─ false_bool: false
        └─ long_string: "This is a very long string that should be handled properly in the output"

        **Full Stacktrace:**
        ────────────────
        \`\`\`
          File "/app/processor.py", line 30, in process_data
        \`\`\`

        "
      `);
    });

    it("should truncate very long objects and arrays", () => {
      const event = new EventBuilder("python")
        .withException(
          createExceptionValue({
            type: "ValueError",
            value: "Data processing error",
            stacktrace: createStackTrace([
              createFrame({
                filename: "/app/processor.py",
                function: "process_batch",
                lineNo: 45,
                inApp: true,
                vars: {
                  small_array: [1, 2, 3],
                  large_array: Array(100)
                    .fill(0)
                    .map((_, i) => i),
                  small_object: { name: "test", value: 123 },
                  large_object: {
                    data: Array(50)
                      .fill(0)
                      .reduce(
                        (acc, _, i) => {
                          acc[`field${i}`] = `value${i}`;
                          return acc;
                        },
                        {} as Record<string, string>,
                      ),
                  },
                },
              }),
            ]),
          }),
        )
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        ValueError: Data processing error
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
          File "/app/processor.py", line 45, in process_batch

        Local Variables:
        ├─ small_array: [1,2,3]
        ├─ large_array: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26, ...]
        ├─ small_object: {"name":"test","value":123}
        └─ large_object: {"data":{"field0":"value0","field1":"value1","field2":"value2", ...}

        **Full Stacktrace:**
        ────────────────
        \`\`\`
          File "/app/processor.py", line 45, in process_batch
        \`\`\`

        "
      `);
    });

    it("should show proper truncation format", () => {
      const event = new EventBuilder("javascript")
        .withException(
          createExceptionValue({
            type: "Error",
            value: "Test error",
            stacktrace: createStackTrace([
              createFrame({
                filename: "/app/test.js",
                function: "test",
                lineNo: 1,
                inApp: true,
                vars: {
                  shortArray: [1, 2, 3],
                  // This will be over 80 chars when stringified
                  longArray: [
                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
                    18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
                  ],
                  shortObject: { a: 1, b: 2 },
                  // This will be over 80 chars when stringified
                  longObject: {
                    field1: "value1",
                    field2: "value2",
                    field3: "value3",
                    field4: "value4",
                    field5: "value5",
                    field6: "value6",
                    field7: "value7",
                    field8: "value8",
                  },
                },
              }),
            ]),
          }),
        )
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        Error: Test error
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
        /app/test.js:1 (test)

        Local Variables:
        ├─ shortArray: [1,2,3]
        ├─ longArray: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27, ...]
        ├─ shortObject: {"a":1,"b":2}
        └─ longObject: {"field1":"value1","field2":"value2","field3":"value3","field4":"value4", ...}

        **Full Stacktrace:**
        ────────────────
        \`\`\`
        /app/test.js:1 (test)
        \`\`\`

        "
      `);
    });

    it("should handle circular references gracefully", () => {
      const circular: any = { name: "test" };
      circular.self = circular;

      const event = new EventBuilder("javascript")
        .withException(
          createExceptionValue({
            type: "TypeError",
            value: "Circular reference detected",
            stacktrace: createStackTrace([
              createFrame({
                filename: "/app/utils.js",
                function: "serialize",
                lineNo: 10,
                inApp: true,
                vars: {
                  normal: { a: 1, b: 2 },
                  circular: circular,
                },
              }),
            ]),
          }),
        )
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        TypeError: Circular reference detected
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
        /app/utils.js:10 (serialize)

        Local Variables:
        ├─ normal: {"a":1,"b":2}
        └─ circular: <object>

        **Full Stacktrace:**
        ────────────────
        \`\`\`
        /app/utils.js:10 (serialize)
        \`\`\`

        "
      `);
    });

    it("should handle empty vars object", () => {
      const event = createSimpleExceptionEvent(
        "python",
        "RuntimeError",
        "Something went wrong",
        createFrame({
          filename: "/app/main.py",
          function: "main",
          lineNo: 1,
          inApp: true,
          vars: {},
        }),
      );

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        RuntimeError: Something went wrong
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
          File "/app/main.py", line 1, in main

        **Full Stacktrace:**
        ────────────────
        \`\`\`
          File "/app/main.py", line 1, in main
        \`\`\`

        "
      `);
    });

    it("should handle large context with proper windowing", () => {
      const event = createSimpleExceptionEvent(
        "python",
        "IndexError",
        "list index out of range",
        createFrameWithContext(
          {
            filename: "/app/processor.py",
            function: "process_items",
            lineNo: 50,
          },
          [
            [45, "    # Setup phase"],
            [46, "    items = get_items()"],
            [47, "    results = []"],
            [48, "    "],
            [49, "    # This line causes the error"],
            [50, "    first_item = items[0]"],
            [51, "    "],
            [52, "    # Process items"],
            [53, "    for item in items:"],
            [54, "        results.append(process(item))"],
            [55, "    return results"],
          ],
          {
            items: [],
          },
        ),
      );

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        IndexError: list index out of range
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
          File "/app/processor.py", line 50, in process_items

            47 │     results = []
            48 │     
            49 │     # This line causes the error
          → 50 │     first_item = items[0]
            51 │     
            52 │     # Process items
            53 │     for item in items:

        Local Variables:
        └─ items: []

        **Full Stacktrace:**
        ────────────────
        \`\`\`
          File "/app/processor.py", line 50, in process_items
            first_item = items[0]
        \`\`\`

        "
      `);
    });

    it("should handle context at beginning of file", () => {
      const event = createSimpleExceptionEvent(
        "python",
        "ImportError",
        "No module named 'missing_module'",
        createFrameWithContext(
          {
            filename: "/app/startup.py",
            function: "<module>",
            lineNo: 2,
          },
          [
            [1, "import os"],
            [2, "import missing_module"],
            [3, "import json"],
            [4, ""],
            [5, "def main():"],
          ],
        ),
      );

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        ImportError: No module named 'missing_module'
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
          File "/app/startup.py", line 2, in <module>

            1 │ import os
          → 2 │ import missing_module
            3 │ import json
            4 │ 
            5 │ def main():

        **Full Stacktrace:**
        ────────────────
        \`\`\`
          File "/app/startup.py", line 2, in <module>
        import missing_module
        \`\`\`

        "
      `);
    });
  });

  describe("Chained exceptions", () => {
    it("should render multiple chained exceptions", () => {
      const event = new EventBuilder("python")
        .withChainedExceptions([
          createExceptionValue({
            type: "KeyError",
            value: "'user_id'",
            stacktrace: createStackTrace([
              createFrame({
                filename: "/app/database.py",
                function: "get_user",
                lineNo: 15,
                inApp: true,
              }),
            ]),
          }),
          createExceptionValue({
            type: "ValueError",
            value: "User not found",
            stacktrace: createStackTrace([
              createFrame({
                filename: "/app/services.py",
                function: "process_user",
                lineNo: 25,
                inApp: true,
              }),
            ]),
          }),
          createExceptionValue({
            type: "HTTPError",
            value: "500 Internal Server Error",
            stacktrace: createStackTrace([
              createFrame({
                filename: "/app/handlers.py",
                function: "handle_request",
                lineNo: 42,
                inApp: true,
              }),
            ]),
          }),
        ])
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        HTTPError: 500 Internal Server Error
        \`\`\`

        **Stacktrace:**
        \`\`\`
          File "/app/handlers.py", line 42, in handle_request
        \`\`\`

        **During handling of the above exception, another exception occurred:**

        ### ValueError: User not found

        **Stacktrace:**
        \`\`\`
          File "/app/services.py", line 25, in process_user
        \`\`\`

        **During handling of the above exception, another exception occurred:**

        ### KeyError: 'user_id'

        **Stacktrace:**
        \`\`\`
          File "/app/database.py", line 15, in get_user
        \`\`\`

        "
      `);
    });

    it("should render chained exceptions with enhanced frame on outermost exception", () => {
      const event = new EventBuilder("python")
        .withChainedExceptions([
          createExceptionValue({
            type: "KeyError",
            value: "'user_id'",
            stacktrace: createStackTrace([
              createFrameWithContext(
                {
                  filename: "/app/database.py",
                  function: "get_user",
                  lineNo: 15,
                  inApp: true,
                },
                [
                  [13, "def get_user(data):"],
                  [14, "    # This will fail if user_id is missing"],
                  [15, "    user_id = data['user_id']"],
                  [16, "    return db.find_user(user_id)"],
                ],
                {
                  data: {},
                },
              ),
            ]),
          }),
          createExceptionValue({
            type: "ValueError",
            value: "User not found",
            stacktrace: createStackTrace([
              createFrameWithContext(
                {
                  filename: "/app/services.py",
                  function: "process_user",
                  lineNo: 25,
                  inApp: true,
                },
                [
                  [23, "    try:"],
                  [24, "        user = get_user(request_data)"],
                  [25, "    except KeyError:"],
                  [26, "        raise ValueError('User not found')"],
                ],
                {
                  request_data: {},
                },
              ),
            ]),
          }),
        ])
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        ValueError: User not found
        \`\`\`

        **Most Relevant Frame:**
        ─────────────────────
          File "/app/services.py", line 25, in process_user

            23 │     try:
            24 │         user = get_user(request_data)
          → 25 │     except KeyError:
            26 │         raise ValueError('User not found')

        Local Variables:
        └─ request_data: {}

        **Full Stacktrace:**
        ────────────────
        \`\`\`
          File "/app/services.py", line 25, in process_user
            except KeyError:
        \`\`\`

        **During handling of the above exception, another exception occurred:**

        ### KeyError: 'user_id'

        **Stacktrace:**
        \`\`\`
          File "/app/database.py", line 15, in get_user
            user_id = data['user_id']
        \`\`\`

        "
      `);
    });

    it("should handle single exception in values array (not chained)", () => {
      const event = new EventBuilder("python")
        .withChainedExceptions([
          createExceptionValue({
            type: "RuntimeError",
            value: "Something went wrong",
            stacktrace: createStackTrace([
              createFrame({
                filename: "/app/main.py",
                function: "main",
                lineNo: 10,
                inApp: true,
              }),
            ]),
          }),
        ])
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        RuntimeError: Something went wrong
        \`\`\`

        **Stacktrace:**
        \`\`\`
          File "/app/main.py", line 10, in main
        \`\`\`

        "
      `);
    });

    it("should use Java-style 'Caused by' for Java platform", () => {
      const event = new EventBuilder("java")
        .withChainedExceptions([
          createExceptionValue({
            type: "SQLException",
            value: "Database connection failed",
            stacktrace: createStackTrace([
              frameFactories.java({
                module: "com.example.db.DatabaseConnector",
                function: "connect",
                filename: "DatabaseConnector.java",
                lineNo: 45,
              }),
            ]),
          }),
          createExceptionValue({
            type: "RuntimeException",
            value: "Failed to initialize service",
            stacktrace: createStackTrace([
              frameFactories.java({
                module: "com.example.service.UserService",
                function: "initialize",
                filename: "UserService.java",
                lineNo: 23,
              }),
            ]),
          }),
        ])
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        RuntimeException: Failed to initialize service
        \`\`\`

        **Stacktrace:**
        \`\`\`
        at com.example.service.UserService.initialize(UserService.java:23)
        \`\`\`

        **Caused by:**

        ### SQLException: Database connection failed

        **Stacktrace:**
        \`\`\`
        at com.example.db.DatabaseConnector.connect(DatabaseConnector.java:45)
        \`\`\`

        "
      `);
    });

    it("should use C#-style arrow notation for dotnet platform", () => {
      const event = new EventBuilder("csharp")
        .withChainedExceptions([
          createExceptionValue({
            type: "ArgumentNullException",
            value: "Value cannot be null. (Parameter 'userId')",
            stacktrace: createStackTrace([
              createFrame({
                filename: "UserRepository.cs",
                function: "GetUserById",
                lineNo: 15,
                inApp: true,
              }),
            ]),
          }),
          createExceptionValue({
            type: "ApplicationException",
            value: "Failed to load user profile",
            stacktrace: createStackTrace([
              createFrame({
                filename: "UserService.cs",
                function: "LoadProfile",
                lineNo: 42,
                inApp: true,
              }),
            ]),
          }),
        ])
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        ApplicationException: Failed to load user profile
        \`\`\`

        **Stacktrace:**
        \`\`\`
            at LoadProfile (UserService.cs:42)
        \`\`\`

        **---> Inner Exception:**

        ### ArgumentNullException: Value cannot be null. (Parameter 'userId')

        **Stacktrace:**
        \`\`\`
            at GetUserById (UserRepository.cs:15)
        \`\`\`

        "
      `);
    });

    it("should handle child exception without stacktrace", () => {
      const event = new EventBuilder("python")
        .withChainedExceptions([
          createExceptionValue({
            type: "KeyError",
            value: "'missing_key'",
            // No stacktrace for child exception
            stacktrace: undefined,
          }),
          createExceptionValue({
            type: "ValueError",
            value: "Data processing failed",
            stacktrace: createStackTrace([
              createFrame({
                filename: "/app/processor.py",
                function: "process_data",
                lineNo: 42,
                inApp: true,
              }),
            ]),
          }),
        ])
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "### Error

        \`\`\`
        ValueError: Data processing failed
        \`\`\`

        **Stacktrace:**
        \`\`\`
          File "/app/processor.py", line 42, in process_data
        \`\`\`

        **During handling of the above exception, another exception occurred:**

        ### KeyError: 'missing_key'

        **Stacktrace:**
        \`\`\`
        No stacktrace available
        \`\`\`

        "
      `);
    });
  });

  describe("Performance issue formatting", () => {
    it("should format N+1 query issue with evidence data", () => {
      const event = new EventBuilder("python")
        .withType("transaction")
        .withOccurrence({
          issueTitle: "N+1 Query",
          culprit: "SELECT * FROM users WHERE id = %s",
          type: 1006, // Performance issue type code
          issueType: "performance_n_plus_one_db_queries",
          evidenceData: {
            parentSpanIds: ["span_123"],
            parentSpan: "GET /api/users",
            repeatingSpansCompact: ["SELECT * FROM users WHERE id = %s"],
            numberRepeatingSpans: "5",
            offenderSpanIds: [
              "span_456",
              "span_457",
              "span_458",
              "span_459",
              "span_460",
            ],
            transactionName: "/api/users",
            op: "db",
          },
          evidenceDisplay: [
            {
              name: "Offending Spans",
              value: "UserService.get_users",
              important: true,
            },
          ],
        })
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "**Parent Operation:**
        GET /api/users

        ### Repeated Database Queries

        **Query executed 5 times:**
        \`\`\`sql
        SELECT * FROM users WHERE id = %s
        \`\`\`

        **Transaction:**
        /api/users

        **Offending Spans:**
        UserService.get_users

        "
      `);
    });

    it("should format N+1 query issue with spans data fallback", () => {
      const event = new EventBuilder("python")
        .withType("transaction")
        .withOccurrence({
          issueTitle: "N+1 Query detected",
          culprit: "database query",
        })
        .withEntry({
          type: "spans",
          data: [
            {
              op: "db.query",
              description: "SELECT * FROM posts WHERE user_id = 1",
              timestamp: 100.5,
              start_timestamp: 100.0,
            },
            {
              op: "db.query",
              description: "SELECT * FROM posts WHERE user_id = 2",
              timestamp: 101.0,
              start_timestamp: 100.5,
            },
            {
              op: "db.query",
              description: "SELECT * FROM posts WHERE user_id = 3",
              timestamp: 101.5,
              start_timestamp: 101.0,
            },
            {
              op: "http.client",
              description: "GET /api/external",
              timestamp: 102.0,
              start_timestamp: 101.5,
            },
          ],
        })
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`""`);
    });

    it("should format transaction event with non-repeated database queries", () => {
      const event = new EventBuilder("python")
        .withType("transaction")
        .withOccurrence({
          issueTitle: "Slow DB Query",
          culprit: "database",
        })
        .withEntry({
          type: "spans",
          data: [
            {
              op: "db.query",
              description: "SELECT COUNT(*) FROM users",
              timestamp: 100.5,
              start_timestamp: 100.0,
            },
            {
              op: "db.query",
              description: "SELECT * FROM settings WHERE key = 'theme'",
              timestamp: 101.0,
              start_timestamp: 100.5,
            },
          ],
        })
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`""`);
    });

    it("should format evidence with operation and offenderSpanIds", () => {
      const event = new EventBuilder("python")
        .withType("transaction")
        .withOccurrence({
          issueTitle: "N+1 Query",
          culprit: "database",
          issueType: "performance_n_plus_one_db_queries",
          evidenceData: {
            op: "db",
            offenderSpanIds: ["span_1", "span_2", "span_3", "span_4", "span_5"],
            numberRepeatingSpans: "5",
          },
        })
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`""`);
    });

    it("should render span tree for N+1 queries with evidence and spans data", () => {
      const event = new EventBuilder("python")
        .withType("transaction")
        .withOccurrence({
          issueTitle: "N+1 Query",
          culprit: "SELECT * FROM users WHERE id = %s",
          issueType: "performance_n_plus_one_db_queries",
          evidenceData: {
            parentSpanIds: ["parent123"],
            parentSpan: "GET /api/users",
            offenderSpanIds: ["span1", "span2", "span3"],
            repeatingSpansCompact: ["SELECT * FROM users WHERE id = %s"],
            numberRepeatingSpans: "3",
            op: "db",
          },
        })
        .withEntry({
          type: "spans",
          data: [
            {
              span_id: "parent123",
              op: "http.server",
              description: "GET /users",
              timestamp: 1722963600.25,
              start_timestamp: 1722963600.0,
            },
            {
              span_id: "span1",
              op: "db.query",
              description: "SELECT * FROM users WHERE id = 1",
              timestamp: 1722963600.013,
              start_timestamp: 1722963600.01,
            },
            {
              span_id: "span2",
              op: "db.query",
              description: "SELECT * FROM users WHERE id = 2",
              timestamp: 1722963600.018,
              start_timestamp: 1722963600.014,
            },
            {
              span_id: "span3",
              op: "db.query",
              description: "SELECT * FROM users WHERE id = 3",
              timestamp: 1722963600.027,
              start_timestamp: 1722963600.019,
            },
          ],
        })
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "**Parent Operation:**
        GET /api/users

        ### Repeated Database Queries

        **Query executed 3 times:**
        \`\`\`sql
        SELECT * FROM users WHERE id = %s
        \`\`\`

        ### Span Tree (Limited to 10 spans)

        \`\`\`
        GET /users [parent12 · http.server · 250ms]
           ├─ SELECT * FROM users WHERE id = 1 [span1 · db.query · 3ms] [N+1]
           ├─ SELECT * FROM users WHERE id = 2 [span2 · db.query · 4ms] [N+1]
           └─ SELECT * FROM users WHERE id = 3 [span3 · db.query · 8ms] [N+1]
        \`\`\`

        "
      `);
    });

    it("should render span tree using duration fields when timestamps are missing", () => {
      const event = new EventBuilder("python")
        .withType("transaction")
        .withOccurrence({
          issueTitle: "N+1 Query",
          issueType: "performance_n_plus_one_db_queries",
          evidenceData: {
            parentSpanIds: ["parentDur"],
            parentSpan: "GET /api/durations",
            offenderSpanIds: ["spanA", "spanB"],
            repeatingSpansCompact: [
              "SELECT * FROM durations WHERE bucket = %s",
            ],
            numberRepeatingSpans: "2",
          },
        })
        .withEntry({
          type: "spans",
          data: [
            {
              span_id: "parentDur",
              op: "http.server",
              description: "GET /durations",
              duration: 1250,
            },
            {
              span_id: "spanA",
              op: "db.query",
              description: "SELECT * FROM durations WHERE bucket = 'fast'",
              duration: 0.5,
            },
            {
              span_id: "spanB",
              op: "db.query",
              description: "SELECT * FROM durations WHERE bucket = 'slow'",
              duration: 1500,
            },
          ],
        })
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "**Parent Operation:**
        GET /api/durations

        ### Repeated Database Queries

        **Query executed 2 times:**
        \`\`\`sql
        SELECT * FROM durations WHERE bucket = %s
        \`\`\`

        ### Span Tree (Limited to 10 spans)

        \`\`\`
        GET /durations [parentDu · http.server · 1250ms]
           ├─ SELECT * FROM durations WHERE bucket = 'fast' [spanA · db.query · 1ms] [N+1]
           └─ SELECT * FROM durations WHERE bucket = 'slow' [spanB · db.query · 1500ms] [N+1]
        \`\`\`

        "
      `);
    });

    it("should handle transaction event without performance data", () => {
      const event = new EventBuilder("python")
        .withType("transaction")
        .withOccurrence({
          issueTitle: "Generic Performance Issue",
          culprit: "slow endpoint",
        })
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`""`);
    });

    it("should handle evidence data without repeating_spans", () => {
      const event = new EventBuilder("python")
        .withType("transaction")
        .withOccurrence({
          issueTitle: "Performance Issue",
          culprit: "database",
          issueType: "performance_slow_db_query", // A different type that we don't fully handle yet
          evidenceData: {
            parentSpan: "GET /api/data",
            transactionName: "/api/data",
          },
          evidenceDisplay: [
            {
              name: "Source Location",
              value: "DataService.fetch",
              important: true,
            },
          ],
        })
        .build();

      const output = formatEventOutput(event);

      expect(output).toMatchInlineSnapshot(`
        "**Parent Operation:**
        GET /api/data

        **Transaction:**
        /api/data

        **Source Location:**
        DataService.fetch

        "
      `);
    });
  });
});

```

--------------------------------------------------------------------------------
/packages/mcp-core/src/internal/formatting.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * LLM response formatting utilities for Sentry data.
 *
 * Converts Sentry API responses into structured markdown format optimized
 * for LLM consumption. Handles stacktraces, event details, issue summaries,
 * and contextual information with consistent formatting patterns.
 */
import type { z } from "zod";
import type {
  Event,
  Issue,
  AutofixRunState,
  Trace,
  TraceSpan,
  GenericEvent,
} from "../api-client/types";
import type {
  ErrorEntrySchema,
  ErrorEventSchema,
  DefaultEventSchema,
  GenericEventSchema,
  EventSchema,
  FrameInterface,
  RequestEntrySchema,
  MessageEntrySchema,
  ThreadsEntrySchema,
  SentryApiService,
  AutofixRunStepRootCauseAnalysisSchema,
} from "../api-client";
import {
  getOutputForAutofixStep,
  isTerminalStatus,
  getStatusDisplayName,
} from "./tool-helpers/seer";
import { logIssue } from "../telem/logging";

// Language detection mappings
const LANGUAGE_EXTENSIONS: Record<string, string> = {
  ".java": "java",
  ".py": "python",
  ".js": "javascript",
  ".jsx": "javascript",
  ".ts": "javascript",
  ".tsx": "javascript",
  ".rb": "ruby",
  ".php": "php",
};

const LANGUAGE_MODULE_PATTERNS: Array<[RegExp, string]> = [
  [/^(java\.|com\.|org\.)/, "java"],
];

/**
 * Detects the programming language of a stack frame based on the file extension.
 * Falls back to the platform parameter if no filename is available or extension is unrecognized.
 *
 * @param frame - The stack frame containing file and location information
 * @param platform - Optional platform hint to use as fallback
 * @returns The detected language or platform fallback or "unknown"
 */
function detectLanguage(
  frame: z.infer<typeof FrameInterface>,
  platform?: string | null,
): string {
  // Check filename extensions
  if (frame.filename) {
    const ext = frame.filename.toLowerCase().match(/\.[^.]+$/)?.[0];
    if (ext && LANGUAGE_EXTENSIONS[ext]) {
      return LANGUAGE_EXTENSIONS[ext];
    }
  }

  // Check module patterns
  if (frame.module) {
    for (const [pattern, language] of LANGUAGE_MODULE_PATTERNS) {
      if (pattern.test(frame.module)) {
        return language;
      }
    }
  }

  // Fallback to platform or unknown
  return platform || "unknown";
}

/**
 * Formats a stack frame into a language-specific string representation.
 * Different languages have different conventions for displaying stack traces.
 *
 * @param frame - The stack frame to format
 * @param frameIndex - Optional frame index for languages that display frame numbers
 * @param platform - Optional platform hint for language detection fallback
 * @returns Formatted stack frame string
 */
export function formatFrameHeader(
  frame: z.infer<typeof FrameInterface>,
  frameIndex?: number,
  platform?: string | null,
) {
  const language = detectLanguage(frame, platform);

  switch (language) {
    case "java": {
      // at com.example.ClassName.methodName(FileName.java:123)
      const className = frame.module || "UnknownClass";
      const method = frame.function || "<unknown>";
      const source = frame.filename || "Unknown Source";
      const location = frame.lineNo ? `:${frame.lineNo}` : "";
      return `at ${className}.${method}(${source}${location})`;
    }

    case "python": {
      // File "/path/to/file.py", line 42, in function_name
      const file =
        frame.filename || frame.absPath || frame.module || "<unknown>";
      const func = frame.function || "<module>";
      const line = frame.lineNo ? `, line ${frame.lineNo}` : "";
      return `  File "${file}"${line}, in ${func}`;
    }

    case "javascript": {
      // Original compact format: filename:line:col (function)
      // This preserves backward compatibility
      return `${[frame.filename, frame.lineNo, frame.colNo]
        .filter((i) => !!i)
        .join(":")}${frame.function ? ` (${frame.function})` : ""}`;
    }

    case "ruby": {
      // from /path/to/file.rb:42:in `method_name'
      const file = frame.filename || frame.module || "<unknown>";
      const func = frame.function ? ` \`${frame.function}\`` : "";
      const line = frame.lineNo ? `:${frame.lineNo}:in` : "";
      return `    from ${file}${line}${func}`;
    }

    case "php": {
      // #0 /path/to/file.php(42): functionName()
      const file = frame.filename || "<unknown>";
      const line = frame.lineNo ? `(${frame.lineNo})` : "";
      const func = frame.function || "<unknown>";
      const prefix = frameIndex !== undefined ? `#${frameIndex} ` : "";
      return `${prefix}${file}${line}: ${func}()`;
    }

    default: {
      // Generic format for unknown languages
      const func = frame.function || "<unknown>";
      const location = frame.filename || frame.module || "<unknown>";
      const line = frame.lineNo ? `:${frame.lineNo}` : "";
      const col = frame.colNo != null ? `:${frame.colNo}` : "";
      return `    at ${func} (${location}${line}${col})`;
    }
  }
}

/**
 * Formats a Sentry event into a structured markdown output.
 * Includes error messages, stack traces, request info, and contextual data.
 *
 * @param event - The Sentry event to format
 * @param options - Additional formatting context
 * @returns Formatted markdown string
 */
export function formatEventOutput(
  event: Event,
  options?: {
    performanceTrace?: Trace;
  },
) {
  let output = "";

  // Check if entries exist (may be undefined for unsupported event types)
  if (!event.entries || !Array.isArray(event.entries)) {
    // For unsupported event types, just show tags and contexts
    output += formatTags(event.tags);
    output += formatContext(event.context);
    output += formatContexts(event.contexts);
    return output;
  }

  // Look for the primary error information
  const messageEntry = event.entries.find((e) => e.type === "message");
  const exceptionEntry = event.entries.find((e) => e.type === "exception");
  const threadsEntry = event.entries.find((e) => e.type === "threads");
  const requestEntry = event.entries.find((e) => e.type === "request");
  const spansEntry = event.entries.find((e) => e.type === "spans");
  const cspEntry = event.entries.find((e) => e.type === "csp");

  // Error message (if present)
  if (messageEntry) {
    output += formatMessageInterfaceOutput(
      event,
      messageEntry.data as z.infer<typeof MessageEntrySchema>,
    );
  }

  // Stack trace (from exception or threads)
  if (exceptionEntry) {
    output += formatExceptionInterfaceOutput(
      event,
      exceptionEntry.data as z.infer<typeof ErrorEntrySchema>,
    );
  } else if (threadsEntry) {
    output += formatThreadsInterfaceOutput(
      event,
      threadsEntry.data as z.infer<typeof ThreadsEntrySchema>,
    );
  }

  // Request info (if HTTP error)
  if (requestEntry) {
    output += formatRequestInterfaceOutput(
      event,
      requestEntry.data as z.infer<typeof RequestEntrySchema>,
    );
  }

  // CSP violation details
  if (cspEntry) {
    output += formatCspInterfaceOutput(event, cspEntry.data);
  }

  // Performance issue details (N+1 queries, etc.)
  // Pass spans data for additional context even if we have evidence
  if (event.type === "transaction") {
    output += formatPerformanceIssueOutput(event, spansEntry?.data, options);
  }

  // Generic events (performance regressions, metric-based issues)
  // These have occurrence data with evidenceDisplay that needs formatting
  if (event.type === "generic") {
    output += formatGenericEventOutput(event);
  }

  output += formatTags(event.tags);
  output += formatContext(event.context);
  output += formatContexts(event.contexts);
  return output;
}

/**
 * Extracts the context line matching the frame's line number for inline display.
 * This is used in the full stacktrace view to show the actual line of code
 * that caused the error inline with the stack frame.
 *
 * @param frame - The stack frame containing context lines
 * @returns The line of code at the frame's line number, or empty string if not available
 */
function renderInlineContext(frame: z.infer<typeof FrameInterface>): string {
  if (!frame.context?.length || !frame.lineNo) {
    return "";
  }

  const contextLine = frame.context.find(([lineNo]) => lineNo === frame.lineNo);
  return contextLine ? `\n${contextLine[1]}` : "";
}

/**
 * Renders an enhanced view of a stack frame with context lines and variables.
 * Used for the "Most Relevant Frame" section to provide detailed information
 * about the most relevant application frame where the error occurred.
 *
 * @param frame - The stack frame to render with enhanced information
 * @param event - The Sentry event containing platform information for language detection
 * @returns Formatted string with frame header, context lines, and variables table
 */
function renderEnhancedFrame(
  frame: z.infer<typeof FrameInterface>,
  event: Event,
): string {
  const parts: string[] = [];

  parts.push("**Most Relevant Frame:**");
  parts.push("─────────────────────");
  parts.push(formatFrameHeader(frame, undefined, event.platform));

  // Add context lines if available
  if (frame.context?.length) {
    const contextLines = renderContextLines(frame);
    if (contextLines) {
      parts.push("");
      parts.push(contextLines);
    }
  }

  // Add variables table if available
  if (frame.vars && Object.keys(frame.vars).length > 0) {
    parts.push("");
    parts.push(renderVariablesTable(frame.vars));
  }

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

function formatExceptionInterfaceOutput(
  event: Event,
  data: z.infer<typeof ErrorEntrySchema>,
) {
  const parts: string[] = [];

  // Handle both single exception (value) and chained exceptions (values)
  const exceptions = data.values || (data.value ? [data.value] : []);

  if (exceptions.length === 0) {
    return "";
  }

  // For chained exceptions, they are typically ordered from innermost to outermost
  // We'll render them in reverse order (outermost first) to match how they occurred
  const isChained = exceptions.length > 1;

  // Create a copy before reversing to avoid mutating the original array
  [...exceptions].reverse().forEach((exception, index) => {
    if (!exception) return;

    // Add language-specific chain indicator for multiple exceptions
    if (isChained && index > 0) {
      parts.push("");
      parts.push(
        getExceptionChainMessage(
          event.platform || null,
          index,
          exceptions.length,
        ),
      );
      parts.push("");
    }

    // Use the actual exception type and value as the heading
    const exceptionTitle = `${exception.type}${exception.value ? `: ${exception.value}` : ""}`;

    parts.push(index === 0 ? "### Error" : `### ${exceptionTitle}`);
    parts.push("");

    // Add the error details in a code block for the first exception
    // to maintain backward compatibility
    if (index === 0) {
      parts.push("```");
      parts.push(exceptionTitle);
      parts.push("```");
      parts.push("");
    }

    if (!exception.stacktrace || !exception.stacktrace.frames) {
      parts.push("**Stacktrace:**");
      parts.push("```");
      parts.push("No stacktrace available");
      parts.push("```");
      return;
    }

    const frames = exception.stacktrace.frames;

    // Only show enhanced frame for the first (outermost) exception to avoid overwhelming output
    if (index === 0) {
      const firstInAppFrame = findFirstInAppFrame(frames);
      if (
        firstInAppFrame &&
        (firstInAppFrame.context?.length || firstInAppFrame.vars)
      ) {
        parts.push(renderEnhancedFrame(firstInAppFrame, event));
        parts.push("");
        parts.push("**Full Stacktrace:**");
        parts.push("────────────────");
      } else {
        parts.push("**Stacktrace:**");
      }
    } else {
      parts.push("**Stacktrace:**");
    }

    parts.push("```");
    parts.push(
      frames
        .map((frame) => {
          const header = formatFrameHeader(frame, undefined, event.platform);
          const context = renderInlineContext(frame);
          return `${header}${context}`;
        })
        .join("\n"),
    );
    parts.push("```");
  });

  parts.push("");
  parts.push("");

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

/**
 * Get the appropriate exception chain message based on the platform
 */
function getExceptionChainMessage(
  platform: string | null,
  index: number,
  totalExceptions: number,
): string {
  // Default message for unknown platforms
  const defaultMessage =
    "**During handling of the above exception, another exception occurred:**";

  if (!platform) {
    return defaultMessage;
  }

  switch (platform.toLowerCase()) {
    case "python":
      // Python has two distinct messages, but without additional metadata
      // we default to the implicit chaining message
      return "**During handling of the above exception, another exception occurred:**";

    case "java":
      return "**Caused by:**";

    case "csharp":
    case "dotnet":
      return "**---> Inner Exception:**";

    case "ruby":
      return "**Caused by:**";

    case "go":
      return "**Wrapped error:**";

    case "rust":
      return `**Caused by (${index}):**`;

    default:
      return defaultMessage;
  }
}

function formatCspInterfaceOutput(event: Event, data: any) {
  if (!data) {
    return "";
  }

  const parts: string[] = [];
  parts.push("### CSP Violation");
  parts.push("");

  if (data.blocked_uri) {
    parts.push(`**Blocked URI**: ${data.blocked_uri}`);
  }

  if (data.violated_directive) {
    parts.push(`**Violated Directive**: ${data.violated_directive}`);
  }

  if (data.effective_directive) {
    parts.push(`**Effective Directive**: ${data.effective_directive}`);
  }

  if (data.document_uri) {
    parts.push(`**Document URI**: ${data.document_uri}`);
  }

  if (data.source_file) {
    parts.push(`**Source File**: ${data.source_file}`);
    if (data.line_number) {
      parts.push(`**Line Number**: ${data.line_number}`);
    }
  }

  if (data.disposition) {
    parts.push(`**Disposition**: ${data.disposition}`);
  }

  if (data.original_policy) {
    parts.push("");
    parts.push("**Original Policy:**");
    parts.push("```");
    parts.push(data.original_policy);
    parts.push("```");
  }

  parts.push("");
  parts.push("");

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

function formatRequestInterfaceOutput(
  event: Event,
  data: z.infer<typeof RequestEntrySchema>,
) {
  if (!data.method || !data.url) {
    return "";
  }
  return `### HTTP Request\n\n**Method:** ${data.method}\n**URL:** ${data.url}\n\n`;
}

function formatMessageInterfaceOutput(
  event: Event,
  data: z.infer<typeof MessageEntrySchema>,
) {
  if (!data.formatted && !data.message) {
    return "";
  }
  const message = data.formatted || data.message || "";
  return `### Error\n\n${"```"}\n${message}\n${"```"}\n\n`;
}

function formatThreadsInterfaceOutput(
  event: Event,
  data: z.infer<typeof ThreadsEntrySchema>,
) {
  if (!data.values || data.values.length === 0) {
    return "";
  }

  // Find the crashed thread only
  const crashedThread = data.values.find((t) => t.crashed);

  if (!crashedThread?.stacktrace?.frames) {
    return "";
  }

  const parts: string[] = [];

  // Include thread name if available
  if (crashedThread.name) {
    parts.push(`**Thread** (${crashedThread.name})`);
    parts.push("");
  }

  const frames = crashedThread.stacktrace.frames;

  // Find and format the first in-app frame with enhanced view
  const firstInAppFrame = findFirstInAppFrame(frames);
  if (
    firstInAppFrame &&
    (firstInAppFrame.context?.length || firstInAppFrame.vars)
  ) {
    parts.push(renderEnhancedFrame(firstInAppFrame, event));
    parts.push("");
    parts.push("**Full Stacktrace:**");
    parts.push("────────────────");
  } else {
    parts.push("**Stacktrace:**");
  }

  parts.push("```");
  parts.push(
    frames
      .map((frame) => {
        const header = formatFrameHeader(frame, undefined, event.platform);
        const context = renderInlineContext(frame);
        return `${header}${context}`;
      })
      .join("\n"),
  );
  parts.push("```");
  parts.push("");

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

/**
 * Renders surrounding source code context for a stack frame.
 * Shows a window of code lines around the error line with visual indicators.
 *
 * @param frame - The stack frame containing context lines
 * @param contextSize - Number of lines to show before and after the error line (default: 3)
 * @returns Formatted context lines with line numbers and arrow indicator for the error line
 */
function renderContextLines(
  frame: z.infer<typeof FrameInterface>,
  contextSize = 3,
): string {
  if (!frame.context || frame.context.length === 0 || !frame.lineNo) {
    return "";
  }

  const lines: string[] = [];
  const errorLine = frame.lineNo;
  const maxLineNoWidth = Math.max(
    ...frame.context.map(([lineNo]) => lineNo.toString().length),
  );

  for (const [lineNo, code] of frame.context) {
    const isErrorLine = lineNo === errorLine;
    const lineNoStr = lineNo.toString().padStart(maxLineNoWidth, " ");

    if (Math.abs(lineNo - errorLine) <= contextSize) {
      if (isErrorLine) {
        lines.push(`  → ${lineNoStr} │ ${code}`);
      } else {
        lines.push(`    ${lineNoStr} │ ${code}`);
      }
    }
  }

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

/**
 * Formats a variable value for display in the variables table.
 * Handles different types appropriately and safely, converting complex objects
 * to readable representations and handling edge cases like circular references.
 *
 * @param value - The variable value to format (can be any type)
 * @param maxLength - Maximum length for stringified objects/arrays (default: 80)
 * @returns Human-readable string representation of the value
 */
function formatVariableValue(value: unknown, maxLength = 80): string {
  try {
    if (typeof value === "string") {
      return `"${value}"`;
    }
    if (value === null) {
      return "null";
    }
    if (value === undefined) {
      return "undefined";
    }
    if (typeof value === "object") {
      const stringified = JSON.stringify(value);
      if (stringified.length > maxLength) {
        // Leave room for ", ...]" or ", ...}"
        const truncateAt = maxLength - 6;
        let truncated = stringified.substring(0, truncateAt);

        // Find the last complete element by looking for the last comma
        const lastComma = truncated.lastIndexOf(",");
        if (lastComma > 0) {
          truncated = truncated.substring(0, lastComma);
        }

        // Add the appropriate ending
        if (Array.isArray(value)) {
          return `${truncated}, ...]`;
        }
        return `${truncated}, ...}`;
      }
      return stringified;
    }
    return String(value);
  } catch {
    // Handle circular references or other stringify errors
    return `<${typeof value}>`;
  }
}

/**
 * Renders a table of local variables in a tree-like format.
 * Uses box-drawing characters to create a visual hierarchy of variables
 * and their values at the point where the error occurred.
 *
 * @param vars - Object containing variable names as keys and their values
 * @returns Formatted variables table with tree-style prefix characters
 */
function renderVariablesTable(vars: Record<string, unknown>): string {
  const entries = Object.entries(vars);
  if (entries.length === 0) {
    return "";
  }

  const lines: string[] = ["Local Variables:"];
  const lastIndex = entries.length - 1;

  entries.forEach(([key, value], index) => {
    const prefix = index === lastIndex ? "└─" : "├─";
    const valueStr = formatVariableValue(value);
    lines.push(`${prefix} ${key}: ${valueStr}`);
  });

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

/**
 * Finds the first application frame (in_app) in a stack trace.
 * Searches from the bottom of the stack (oldest frame) to find the first
 * frame that belongs to the user's application code rather than libraries.
 *
 * @param frames - Array of stack frames, typically in reverse chronological order
 * @returns The first in-app frame found, or undefined if none exist
 */
function findFirstInAppFrame(
  frames: z.infer<typeof FrameInterface>[],
): z.infer<typeof FrameInterface> | undefined {
  // Frames are usually in reverse order (most recent first)
  // We want the first in-app frame from the bottom
  for (let i = frames.length - 1; i >= 0; i--) {
    if (frames[i].inApp === true) {
      return frames[i];
    }
  }
  return undefined;
}

/**
 * Constants for performance issue formatting
 */
const MAX_SPANS_IN_TREE = 10;

/**
 * Safely parse a number from a string, returning a default if invalid
 */
function safeParseInt(value: unknown, defaultValue: number): number {
  if (typeof value === "number") return value;
  if (typeof value === "string") {
    const parsed = Number.parseInt(value, 10);
    return Number.isNaN(parsed) ? defaultValue : parsed;
  }
  return defaultValue;
}

/**
 * Simplified span structure for rendering span trees in performance issues.
 * This is a subset of the full span data focused on visualization needs.
 */
interface PerformanceSpan {
  span_id: string;
  op: string; // Operation type (e.g., "db.query", "http.client")
  description: string; // Human-readable description of what the span did
  duration: number; // Duration in milliseconds
  is_n1_query: boolean; // Whether this span is part of the N+1 pattern
  children: PerformanceSpan[];
  level: number; // Nesting level for tree rendering
}

interface RawSpan {
  span_id?: string;
  id?: string;
  op?: string;
  description?: string;
  timestamp?: number;
  start_timestamp?: number;
  duration?: number;
}

interface N1EvidenceData {
  parentSpan?: string;
  parentSpanIds?: string[];
  repeatingSpansCompact?: string[];
  repeatingSpans?: string[];
  numberRepeatingSpans?: string; // API returns string even though it's a number
  numPatternRepetitions?: number;
  offenderSpanIds?: string[];
  transactionName?: string;
  [key: string]: unknown;
}

interface SlowDbEvidenceData {
  parentSpan?: string;
  [key: string]: unknown;
}

function normalizeSpanId(value: unknown): string | undefined {
  if (typeof value === "string" && value) {
    return value;
  }
  return undefined;
}

function getSpanIdentifier(span: RawSpan): string | undefined {
  if (span.span_id !== undefined) {
    return normalizeSpanId(span.span_id);
  }
  if (span.id !== undefined) {
    return normalizeSpanId(span.id);
  }
  return undefined;
}

function getSpanDurationMs(span: RawSpan): number {
  if (
    typeof span.timestamp === "number" &&
    typeof span.start_timestamp === "number"
  ) {
    const deltaSeconds = span.timestamp - span.start_timestamp;
    if (Number.isFinite(deltaSeconds) && deltaSeconds >= 0) {
      return deltaSeconds * 1000;
    }
  }

  // Trace APIs expose `duration` in milliseconds. Preserve fractional values.
  if (typeof span.duration === "number" && Number.isFinite(span.duration)) {
    return span.duration >= 0 ? span.duration : 0;
  }

  return 0;
}

function normalizeIdArray(values: unknown): string[] {
  if (!Array.isArray(values)) {
    return [];
  }

  return values
    .map((value) => normalizeSpanId(value))
    .filter((value): value is string => value !== undefined);
}

function isValidSpanArray(value: unknown): value is RawSpan[] {
  return Array.isArray(value);
}

/**
 * Get the repeating span descriptions from evidence data.
 * Prefers repeatingSpansCompact (more concise) over repeatingSpans (verbose).
 */
function getRepeatingSpanLines(evidenceData: N1EvidenceData): string[] {
  // Try compact version first (preferred for display)
  if (
    Array.isArray(evidenceData.repeatingSpansCompact) &&
    evidenceData.repeatingSpansCompact.length > 0
  ) {
    return evidenceData.repeatingSpansCompact
      .map((s) => (typeof s === "string" ? s.trim() : ""))
      .filter((s): s is string => s.length > 0);
  }

  // Fall back to full version
  if (
    Array.isArray(evidenceData.repeatingSpans) &&
    evidenceData.repeatingSpans.length > 0
  ) {
    return evidenceData.repeatingSpans
      .map((s) => (typeof s === "string" ? s.trim() : ""))
      .filter((s): s is string => s.length > 0);
  }

  return [];
}

function isTraceSpan(node: unknown): node is TraceSpan {
  if (node === null || typeof node !== "object") {
    return false;
  }
  const candidate = node as { event_type?: unknown; event_id?: unknown };
  // Trace API returns spans with event_type: "span"
  return (
    candidate.event_type === "span" && typeof candidate.event_id === "string"
  );
}

function buildTraceSpanTree(
  trace: Trace,
  parentSpanIds: string[],
  offenderSpanIds: string[],
  maxSpans: number,
): string[] {
  const offenderSet = new Set(offenderSpanIds);
  const spanMap = new Map<string, TraceSpan>();

  function indexSpan(span: TraceSpan): void {
    // Try to get span_id from additional_attributes, fall back to event_id
    const spanId =
      normalizeSpanId(span.additional_attributes?.span_id) || span.event_id;
    if (spanId && spanId.length > 0) {
      spanMap.set(spanId, span);
    }
    for (const child of span.children ?? []) {
      if (isTraceSpan(child)) {
        indexSpan(child);
      }
    }
  }

  for (const node of trace) {
    if (isTraceSpan(node)) {
      indexSpan(node);
    }
  }

  const roots: PerformanceSpan[] = [];
  const budget = { count: 0, limit: maxSpans };

  // First, try to find parent spans
  for (const parentId of parentSpanIds) {
    const span = spanMap.get(parentId);
    if (!span) {
      continue;
    }
    const perfSpan = convertTraceSpanToPerformanceSpan(
      span,
      offenderSet,
      budget,
      0,
    );
    if (perfSpan) {
      roots.push(perfSpan);
    }
    if (budget.count >= budget.limit) {
      break;
    }
  }

  // If no parent spans found, try to find offender spans directly
  if (roots.length === 0 && offenderSpanIds.length > 0) {
    for (const offenderId of offenderSpanIds) {
      const span = spanMap.get(offenderId);
      if (!span) {
        continue;
      }
      const perfSpan = convertTraceSpanToPerformanceSpan(
        span,
        offenderSet,
        budget,
        0,
      );
      if (perfSpan) {
        roots.push(perfSpan);
      }
      if (budget.count >= budget.limit) {
        break;
      }
    }
  }

  if (roots.length === 0) {
    return [];
  }

  return renderPerformanceSpanTree(roots);
}

function convertTraceSpanToPerformanceSpan(
  span: TraceSpan,
  offenderSet: Set<string>,
  budget: { count: number; limit: number },
  level: number,
): PerformanceSpan | null {
  if (budget.count >= budget.limit) {
    return null;
  }

  budget.count += 1;

  // Get span ID from additional_attributes or fall back to event_id
  const spanId =
    normalizeSpanId(span.additional_attributes?.span_id) || span.event_id;

  const performanceSpan: PerformanceSpan = {
    span_id: spanId,
    op: span.op || "unknown",
    description: formatTraceSpanDescription(span),
    duration: getTraceSpanDurationMs(span),
    is_n1_query: offenderSet.has(spanId),
    children: [],
    level,
  };

  for (const child of span.children ?? []) {
    if (!isTraceSpan(child)) {
      continue;
    }
    if (budget.count >= budget.limit) {
      break;
    }
    const childSpan = convertTraceSpanToPerformanceSpan(
      child,
      offenderSet,
      budget,
      level + 1,
    );
    if (childSpan) {
      performanceSpan.children.push(childSpan);
    }
    if (budget.count >= budget.limit) {
      break;
    }
  }

  return performanceSpan;
}

function formatTraceSpanDescription(span: TraceSpan): string {
  if (span.name && span.name.trim().length > 0) {
    return span.name.trim();
  }
  if (span.description && span.description.trim().length > 0) {
    return span.description.trim();
  }
  if (span.op && span.op.trim().length > 0) {
    return span.op.trim();
  }
  return "unnamed";
}

function getTraceSpanDurationMs(span: TraceSpan): number {
  if (typeof span.duration === "number" && span.duration >= 0) {
    return span.duration;
  }
  if (
    typeof (span as { end_timestamp?: number }).end_timestamp === "number" &&
    typeof span.start_timestamp === "number"
  ) {
    const deltaSeconds =
      (span as { end_timestamp: number }).end_timestamp - span.start_timestamp;
    if (Number.isFinite(deltaSeconds) && deltaSeconds >= 0) {
      return deltaSeconds * 1000;
    }
  }
  return 0;
}

function buildOffenderSummaries(
  spans: RawSpan[],
  offenderSpanIds: string[],
): string[] {
  if (offenderSpanIds.length === 0) {
    return [];
  }

  const spanMap = new Map<string, RawSpan>();
  for (const span of spans) {
    const identifier = getSpanIdentifier(span);
    if (identifier) {
      spanMap.set(identifier, span);
    }
  }

  const summaries: string[] = [];
  for (const offenderId of offenderSpanIds) {
    const span = spanMap.get(offenderId);
    if (span) {
      const description = span.description || span.op || `Span ${offenderId}`;
      const duration = getSpanDurationMs(span);
      const durationLabel = duration > 0 ? ` (${Math.round(duration)}ms)` : "";
      summaries.push(`${description}${durationLabel} [${offenderId}] [N+1]`);
    } else {
      summaries.push(`Span ${offenderId} [N+1]`);
    }
  }

  return summaries;
}

/**
 * Renders a hierarchical tree of performance spans using box-drawing characters.
 * Highlights N+1 queries with a special indicator.
 *
 * @param spans - Array of selected performance spans
 * @returns Array of formatted strings representing the tree
 */
function renderPerformanceSpanTree(spans: PerformanceSpan[]): string[] {
  const lines: string[] = [];

  function renderSpan(span: PerformanceSpan, prefix = "", isLast = true): void {
    const connector = prefix === "" ? "" : isLast ? "└─ " : "├─ ";

    const displayName = span.description?.trim() || span.op || "unnamed";
    const shortId = span.span_id ? span.span_id.substring(0, 8) : "unknown";
    const durationDisplay =
      span.duration > 0 ? `${Math.round(span.duration)}ms` : "unknown";

    const metadataParts: string[] = [shortId];
    if (span.op && span.op !== "default") {
      metadataParts.push(span.op);
    }
    metadataParts.push(durationDisplay);

    const line = `${prefix}${connector}${displayName} [${metadataParts.join(
      " · ",
    )}]${span.is_n1_query ? " [N+1]" : ""}`;
    lines.push(line);

    // Render children
    for (let i = 0; i < span.children.length; i++) {
      const child = span.children[i];
      const isLastChild = i === span.children.length - 1;
      const childPrefix = prefix + (isLast ? "   " : "│  ");
      renderSpan(child, childPrefix, isLastChild);
    }
  }

  for (let i = 0; i < spans.length; i++) {
    const span = spans[i];
    const isLastRoot = i === spans.length - 1;
    renderSpan(span, "", isLastRoot);
  }

  return lines;
}

function selectN1QuerySpans(
  spans: RawSpan[],
  evidence: N1EvidenceData,
  maxSpans = MAX_SPANS_IN_TREE,
): PerformanceSpan[] {
  const selected: PerformanceSpan[] = [];
  let spanCount = 0;

  const offenderSpanIds = normalizeIdArray(evidence.offenderSpanIds);
  const parentSpanIds = normalizeIdArray(evidence.parentSpanIds);

  let parentSpan: PerformanceSpan | null = null;
  if (parentSpanIds.length > 0) {
    const parent = spans.find((span) => {
      const identifier = getSpanIdentifier(span);
      return identifier ? parentSpanIds.includes(identifier) : false;
    });

    if (parent) {
      parentSpan = {
        span_id: getSpanIdentifier(parent) ?? "unknown",
        op: parent.op || "unknown",
        description:
          parent.description || evidence.parentSpan || "Parent Operation",
        duration: getSpanDurationMs(parent),
        is_n1_query: false,
        children: [],
        level: 0,
      };
      selected.push(parentSpan);
      spanCount += 1;
    }
  }

  if (offenderSpanIds.length > 0) {
    const offenderSet = new Set(offenderSpanIds);
    const offenderSpans = spans
      .filter((span) => {
        const identifier = getSpanIdentifier(span);
        return identifier ? offenderSet.has(identifier) : false;
      })
      .slice(0, Math.max(0, maxSpans - spanCount));

    for (const span of offenderSpans) {
      const perfSpan: PerformanceSpan = {
        span_id: getSpanIdentifier(span) ?? "unknown",
        op: span.op || "db.query",
        description: span.description || "Database Query",
        duration: getSpanDurationMs(span),
        is_n1_query: true,
        children: [],
        level: parentSpan ? 1 : 0,
      };

      if (parentSpan) {
        parentSpan.children.push(perfSpan);
      } else {
        selected.push(perfSpan);
      }

      spanCount += 1;
      if (spanCount >= maxSpans) {
        break;
      }
    }
  }

  return selected;
}

/**
 * Known Sentry performance issue types that we handle.
 *
 * NOTE: We intentionally only implement formatters for high-value performance issues
 * that provide complex insights. Not all issue types need custom formatting - many
 * can rely on the generic evidenceDisplay fields that Sentry provides.
 *
 * Currently fully implemented:
 * - N+1 query detection (DB and API)
 *
 * Partially implemented:
 * - Slow DB queries (shows parent span only)
 *
 * Not implemented (lower priority):
 * - Asset-related issues (render blocking, uncompressed, large payloads)
 * - File I/O issues
 * - Consecutive queries
 */
const KNOWN_PERFORMANCE_ISSUE_TYPES = {
  N_PLUS_ONE_DB_QUERIES: "performance_n_plus_one_db_queries",
  N_PLUS_ONE_API_CALLS: "performance_n_plus_one_api_calls",
  SLOW_DB_QUERY: "performance_slow_db_query",
  RENDER_BLOCKING_ASSET: "performance_render_blocking_asset",
  CONSECUTIVE_DB_QUERIES: "performance_consecutive_db_queries",
  FILE_IO_MAIN_THREAD: "performance_file_io_main_thread",
  M_N_PLUS_ONE_DB_QUERIES: "performance_m_n_plus_one_db_queries",
  UNCOMPRESSED_ASSET: "performance_uncompressed_asset",
  LARGE_HTTP_PAYLOAD: "performance_large_http_payload",
} as const;

/**
 * Map numeric occurrence types to issue types (from Sentry's codebase).
 *
 * Sentry uses numeric type IDs internally in the occurrence data structure,
 * but string issue types in the UI and other APIs. This mapping converts
 * between them.
 *
 * Source: sentry/static/app/types/group.tsx in Sentry's codebase
 * Range: 1xxx = transaction-based performance issues
 *        2xxx = profile-based performance issues
 */
const OCCURRENCE_TYPE_TO_ISSUE_TYPE: Record<number, string> = {
  1001: KNOWN_PERFORMANCE_ISSUE_TYPES.SLOW_DB_QUERY,
  1004: KNOWN_PERFORMANCE_ISSUE_TYPES.RENDER_BLOCKING_ASSET,
  1006: KNOWN_PERFORMANCE_ISSUE_TYPES.N_PLUS_ONE_DB_QUERIES,
  1906: KNOWN_PERFORMANCE_ISSUE_TYPES.N_PLUS_ONE_DB_QUERIES, // Alternative ID for N+1 DB
  1007: KNOWN_PERFORMANCE_ISSUE_TYPES.CONSECUTIVE_DB_QUERIES,
  1008: KNOWN_PERFORMANCE_ISSUE_TYPES.FILE_IO_MAIN_THREAD,
  1009: "performance_consecutive_http",
  1010: KNOWN_PERFORMANCE_ISSUE_TYPES.N_PLUS_ONE_API_CALLS,
  1910: KNOWN_PERFORMANCE_ISSUE_TYPES.N_PLUS_ONE_API_CALLS, // Alternative ID for N+1 API
  1012: KNOWN_PERFORMANCE_ISSUE_TYPES.UNCOMPRESSED_ASSET,
  1013: "performance_db_main_thread",
  1015: KNOWN_PERFORMANCE_ISSUE_TYPES.LARGE_HTTP_PAYLOAD,
  1016: "performance_http_overhead",
};

// Type alias currently unused but kept for potential future type safety
// type PerformanceIssueType = typeof KNOWN_PERFORMANCE_ISSUE_TYPES[keyof typeof KNOWN_PERFORMANCE_ISSUE_TYPES];

/**
 * Formats N+1 query issue evidence data.
 *
 * N+1 queries are a common performance anti-pattern where code executes
 * 1 query to get a list of items, then N additional queries (one per item)
 * instead of using a single JOIN or batch query.
 *
 * Evidence fields we use:
 * - parentSpan: The operation that triggered the N+1 queries
 * - repeatingSpansCompact/repeatingSpans: The query pattern being repeated
 * - numberRepeatingSpans: How many times the query was executed
 * - offenderSpanIds: IDs of the actual span instances
 * - parentSpanIds: IDs of parent spans for tree visualization
 */
function formatN1QueryEvidence(
  evidenceData: N1EvidenceData,
  spansData: unknown,
  performanceTrace?: Trace,
): string {
  const parts: string[] = [];

  // Format parent span info if available
  if (evidenceData.parentSpan) {
    parts.push("**Parent Operation:**");
    parts.push(`${evidenceData.parentSpan}`);
    parts.push("");
  }

  // Format repeating spans (the N+1 queries)
  const repeatingLines = getRepeatingSpanLines(evidenceData);
  if (repeatingLines.length > 0) {
    parts.push("### Repeated Database Queries");
    parts.push("");

    const queryCount = evidenceData.numberRepeatingSpans
      ? safeParseInt(evidenceData.numberRepeatingSpans, 0)
      : evidenceData.numPatternRepetitions ||
        evidenceData.offenderSpanIds?.length ||
        0;

    if (queryCount > 0) {
      parts.push(`**Query executed ${queryCount} times:**`);
    }

    // Show the query pattern - if single line, render as SQL block; if multiple, as list
    if (repeatingLines.length === 1) {
      parts.push("```sql");
      parts.push(repeatingLines[0]);
      parts.push("```");
      parts.push("");
    } else {
      parts.push("**Repeated operations:**");
      for (const line of repeatingLines) {
        parts.push(`- ${line}`);
      }
      parts.push("");
    }
  }

  const parentSpanIds = normalizeIdArray(evidenceData.parentSpanIds);
  const offenderSpanIds = normalizeIdArray(evidenceData.offenderSpanIds);

  const traceLines = performanceTrace
    ? buildTraceSpanTree(
        performanceTrace,
        parentSpanIds,
        offenderSpanIds,
        MAX_SPANS_IN_TREE,
      )
    : [];

  if (traceLines.length > 0) {
    parts.push(`### Span Tree (Limited to ${MAX_SPANS_IN_TREE} spans)`);
    parts.push("");
    parts.push("```");
    parts.push(...traceLines);
    parts.push("```");
    parts.push("");
  } else {
    const spanTree = isValidSpanArray(spansData)
      ? selectN1QuerySpans(spansData, evidenceData, MAX_SPANS_IN_TREE)
      : [];

    if (spanTree.length > 0) {
      parts.push(`### Span Tree (Limited to ${MAX_SPANS_IN_TREE} spans)`);
      parts.push("");
      parts.push("```");
      parts.push(...renderPerformanceSpanTree(spanTree));
      parts.push("```");
      parts.push("");
    } else if (isValidSpanArray(spansData)) {
      // Only show offender summaries if we have spans data but couldn't build a tree
      const offenderSummaries = buildOffenderSummaries(
        spansData as RawSpan[],
        offenderSpanIds,
      );

      if (offenderSummaries.length > 0) {
        parts.push("### Offending Spans");
        parts.push("");
        for (const summary of offenderSummaries) {
          parts.push(`- ${summary}`);
        }
      }
    }
  }

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

/**
 * Formats slow DB query issue evidence data.
 *
 * Currently only partially implemented - shows parent span information.
 * Full implementation would show query duration, explain plan, etc.
 *
 * This is lower priority as the generic evidenceDisplay fields usually
 * provide sufficient information for slow query issues.
 */
function formatSlowDbQueryEvidence(
  evidenceData: SlowDbEvidenceData,
  spansData: unknown,
): string {
  const parts: string[] = [];

  // Show parent span if available (generic field that applies to slow queries)
  if (evidenceData.parentSpan) {
    parts.push("**Parent Operation:**");
    parts.push(`${evidenceData.parentSpan}`);
    parts.push("");
  }

  // TODO: Implement slow query specific fields when we know the structure
  // Potential fields: query duration, database name, query plan
  console.warn(
    "[formatSlowDbQueryEvidence] Evidence data rendering not yet fully implemented",
  );

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

/**
 * Formats performance issue details from transaction events based on the issue type.
 *
 * This is the main dispatcher for performance issue formatting. It:
 * 1. Detects the issue type from occurrence data (numeric or string)
 * 2. Calls the appropriate type-specific formatter if implemented
 * 3. Falls back to generic evidenceDisplay fields for unimplemented types
 * 4. Provides span analysis fallback for events without occurrence data
 *
 * The occurrence data structure comes from Sentry's performance issue detection
 * and contains evidence about what triggered the issue.
 *
 * @param event - The transaction event containing performance issue data
 * @param spansData - The spans data from the event entries
 * @returns Formatted markdown string with performance issue details
 */
function formatPerformanceIssueOutput(
  event: Event,
  spansData: unknown,
  options?: {
    performanceTrace?: Trace;
  },
): string {
  const parts: string[] = [];

  // Check if we have occurrence data
  const occurrence = (event as any).occurrence;
  if (!occurrence) {
    return "";
  }

  // Get issue type - occurrence.type is numeric, issueType may be a string
  let issueType: string | undefined;
  if (typeof occurrence.type === "number") {
    issueType = OCCURRENCE_TYPE_TO_ISSUE_TYPE[occurrence.type];
  } else {
    issueType = occurrence.issueType || occurrence.type;
  }

  const evidenceData = occurrence.evidenceData;

  // Process evidence data based on known performance issue types
  if (evidenceData) {
    switch (issueType) {
      case KNOWN_PERFORMANCE_ISSUE_TYPES.N_PLUS_ONE_DB_QUERIES:
      case KNOWN_PERFORMANCE_ISSUE_TYPES.N_PLUS_ONE_API_CALLS:
      case KNOWN_PERFORMANCE_ISSUE_TYPES.M_N_PLUS_ONE_DB_QUERIES: {
        const result = formatN1QueryEvidence(
          evidenceData,
          spansData,
          options?.performanceTrace,
        );
        if (result) parts.push(result);
        break;
      }

      case KNOWN_PERFORMANCE_ISSUE_TYPES.SLOW_DB_QUERY: {
        const result = formatSlowDbQueryEvidence(evidenceData, spansData);
        if (result) parts.push(result);
        break;
      }

      default:
        // We don't implement formatters for all performance issue types.
        // Many lower-priority issues (consecutive queries, asset issues, file I/O)
        // work fine with just the generic evidenceDisplay fields below.
        // Only high-value, complex issues like N+1 queries need custom formatting.
        if (issueType) {
          console.warn(
            `[formatPerformanceIssueOutput] No custom formatter for issue type: ${issueType}`,
          );
        }
      // Fall through to show generic evidence display below
    }
  }

  // Show transaction name if available for any performance issue (generic field)
  if (evidenceData?.transactionName) {
    parts.push("**Transaction:**");
    parts.push(`${evidenceData.transactionName}`);
    parts.push("");
  }

  // Always show evidence display if available (this is generic and doesn't require type knowledge)
  if (occurrence.evidenceDisplay?.length > 0) {
    for (const display of occurrence.evidenceDisplay) {
      if (display.important) {
        parts.push(`**${display.name}:**`);
        parts.push(`${display.value}`);
        parts.push("");
      }
    }
  }

  return parts.length > 0 ? `${parts.join("\n")}\n` : "";
}

/**
 * Formats generic event output (performance regressions, metric-based issues).
 * Generic events don't have traditional error entries, but have occurrence data
 * with evidenceDisplay showing regression details, metric changes, etc.
 */
function formatGenericEventOutput(event: Event): string {
  const parts: string[] = [];

  // Only generic events have occurrence data
  if (event.type !== "generic") {
    return "";
  }

  // Type assertion after guard - we know it's a GenericEvent
  const genericEvent = event as GenericEvent;
  const occurrence = genericEvent.occurrence;
  if (!occurrence) {
    return "";
  }

  // Add a section header for performance regression details
  const evidenceData = occurrence.evidenceData;
  if (evidenceData) {
    parts.push("### Performance Regression Details");
    parts.push("");
  }

  // Show evidence display items (regression details, metric changes, etc.)
  if (occurrence.evidenceDisplay && occurrence.evidenceDisplay.length > 0) {
    for (const display of occurrence.evidenceDisplay) {
      if (display.important) {
        parts.push(`**${display.name}:**`);
        parts.push(`${display.value}`);
        parts.push("");
      }
    }
  }

  return parts.length > 0 ? `${parts.join("\n")}\n` : "";
}

function formatTags(tags: z.infer<typeof EventSchema>["tags"]) {
  if (!tags || tags.length === 0) {
    return "";
  }
  return `### Tags\n\n${tags
    .map((tag) => `**${tag.key}**: ${tag.value}`)
    .join("\n")}\n\n`;
}

function formatContext(context: z.infer<typeof EventSchema>["context"]) {
  if (!context || Object.keys(context).length === 0) {
    return "";
  }
  return `### Extra Data\n\nAdditional data attached to this event.\n\n${Object.entries(
    context,
  )
    .map(([key, value]) => {
      return `**${key}**: ${JSON.stringify(value, undefined, 2)}`;
    })
    .join("\n")}\n\n`;
}

function formatContexts(contexts: z.infer<typeof EventSchema>["contexts"]) {
  if (!contexts || Object.keys(contexts).length === 0) {
    return "";
  }
  return `### Additional Context\n\nThese are additional context provided by the user when they're instrumenting their application.\n\n${Object.entries(
    contexts,
  )
    .map(
      ([name, data]) =>
        `**${name}**\n${Object.entries(data)
          .filter(([key, _]) => key !== "type")
          .map(([key, value]) => {
            return `${key}: ${JSON.stringify(value, undefined, 2)}`;
          })
          .join("\n")}`,
    )
    .join("\n\n")}\n\n`;
}

/**
 * Formats a brief Seer analysis summary for inclusion in issue details.
 * Shows current status and high-level insights, prompting to use analyze_issue_with_seer for full details.
 *
 * @param autofixState - The autofix state containing Seer analysis data
 * @param organizationSlug - The organization slug for the issue
 * @param issueId - The issue ID (shortId)
 * @returns Formatted markdown string with Seer summary, or empty string if no analysis exists
 */
function formatSeerSummary(
  autofixState: AutofixRunState | undefined,
  organizationSlug: string,
  issueId: string,
): string {
  if (!autofixState || !autofixState.autofix) {
    return "";
  }

  const { autofix } = autofixState;
  const parts: string[] = [];

  parts.push("## Seer Analysis");
  parts.push("");

  // Show status first
  const statusDisplay = getStatusDisplayName(autofix.status);
  if (!isTerminalStatus(autofix.status)) {
    parts.push(`**Status:** ${statusDisplay}`);
    parts.push("");
  }

  // Show summary of what we have so far
  if (autofix.steps.length > 0) {
    const completedSteps = autofix.steps.filter(
      (step) => step.status === "COMPLETED",
    );

    // Find the solution step if available
    const solutionStep = completedSteps.find(
      (step) => step.type === "solution",
    );

    if (solutionStep) {
      // For solution steps, use the description directly
      const solutionDescription = solutionStep.description;
      if (
        solutionDescription &&
        typeof solutionDescription === "string" &&
        solutionDescription.trim()
      ) {
        parts.push("**Summary:**");
        parts.push(solutionDescription.trim());
      } else {
        // Fallback to extracting from output if no description
        const solutionOutput = getOutputForAutofixStep(solutionStep);
        const lines = solutionOutput.split("\n");
        const firstParagraph = lines.find(
          (line) =>
            line.trim().length > 50 &&
            !line.startsWith("#") &&
            !line.startsWith("*"),
        );
        if (firstParagraph) {
          parts.push("**Summary:**");
          parts.push(firstParagraph.trim());
        }
      }
    } else if (completedSteps.length > 0) {
      // Show what steps have been completed so far
      const rootCauseStep = completedSteps.find(
        (step) => step.type === "root_cause_analysis",
      );

      if (rootCauseStep) {
        const typedStep = rootCauseStep as z.infer<
          typeof AutofixRunStepRootCauseAnalysisSchema
        >;
        if (
          typedStep.causes &&
          typedStep.causes.length > 0 &&
          typedStep.causes[0].description
        ) {
          parts.push("**Root Cause Identified:**");
          parts.push(typedStep.causes[0].description.trim());
        }
      } else {
        // Show generic progress
        parts.push(
          `**Progress:** ${completedSteps.length} of ${autofix.steps.length} steps completed`,
        );
      }
    }
  } else {
    // No steps yet - check for terminal states first
    if (isTerminalStatus(autofix.status)) {
      if (autofix.status === "FAILED" || autofix.status === "ERROR") {
        parts.push("**Status:** Analysis failed.");
      } else if (autofix.status === "CANCELLED") {
        parts.push("**Status:** Analysis was cancelled.");
      } else if (
        autofix.status === "NEED_MORE_INFORMATION" ||
        autofix.status === "WAITING_FOR_USER_RESPONSE"
      ) {
        parts.push(
          "**Status:** Analysis paused - additional information needed.",
        );
      }
    } else {
      parts.push("Analysis has started but no results yet.");
    }
  }

  // Add specific messages for terminal states when steps exist
  if (autofix.steps.length > 0 && isTerminalStatus(autofix.status)) {
    if (autofix.status === "FAILED" || autofix.status === "ERROR") {
      parts.push("");
      parts.push("**Status:** Analysis failed.");
    } else if (autofix.status === "CANCELLED") {
      parts.push("");
      parts.push("**Status:** Analysis was cancelled.");
    } else if (
      autofix.status === "NEED_MORE_INFORMATION" ||
      autofix.status === "WAITING_FOR_USER_RESPONSE"
    ) {
      parts.push("");
      parts.push(
        "**Status:** Analysis paused - additional information needed.",
      );
    }
  }

  // Always suggest using analyze_issue_with_seer for more details
  parts.push("");
  parts.push(
    `**Note:** For detailed root cause analysis and solutions, call \`analyze_issue_with_seer(organizationSlug='${organizationSlug}', issueId='${issueId}')\``,
  );

  return `${parts.join("\n")}\n\n`;
}

/**
 * Formats a Sentry issue with its latest event into comprehensive markdown output.
 * Includes issue metadata, event details, and usage instructions.
 *
 * @param params - Object containing organization slug, issue, event, and API service
 * @returns Formatted markdown string with complete issue information
 */
export function formatIssueOutput({
  organizationSlug,
  issue,
  event,
  apiService,
  autofixState,
  performanceTrace,
}: {
  organizationSlug: string;
  issue: Issue;
  event: Event;
  apiService: SentryApiService;
  autofixState?: AutofixRunState;
  performanceTrace?: Trace;
}) {
  let output = `# Issue ${issue.shortId} in **${organizationSlug}**\n\n`;

  // Check if this is a performance issue based on issueCategory or issueType
  // Performance issues can have various categories like 'db_query' but issueType starts with 'performance_'
  const isPerformanceIssue =
    issue.issueType?.startsWith("performance_") ||
    issue.issueCategory === "performance";

  if (isPerformanceIssue && issue.metadata) {
    // For performance issues, use metadata for better context
    const issueTitle = issue.metadata.title || issue.title;
    output += `**Description**: ${issueTitle}\n`;

    if (issue.metadata.location) {
      output += `**Location**: ${issue.metadata.location}\n`;
    }
    if (issue.metadata.value) {
      output += `**Query Pattern**: \`${issue.metadata.value}\`\n`;
    }
  } else {
    // For regular errors and other issues
    output += `**Description**: ${issue.title}\n`;
    output += `**Culprit**: ${issue.culprit}\n`;
  }

  output += `**First Seen**: ${new Date(issue.firstSeen).toISOString()}\n`;
  output += `**Last Seen**: ${new Date(issue.lastSeen).toISOString()}\n`;
  output += `**Occurrences**: ${issue.count}\n`;
  output += `**Users Impacted**: ${issue.userCount}\n`;
  output += `**Status**: ${issue.status}\n`;

  // Add substatus if present (e.g., "regressed" for performance regressions)
  if (issue.substatus) {
    output += `**Substatus**: ${issue.substatus}\n`;
  }

  // Add assignee information if assigned
  if (issue.assignedTo) {
    if (typeof issue.assignedTo === "string") {
      output += `**Assigned To**: ${issue.assignedTo}\n`;
    } else {
      const assignee = issue.assignedTo;
      const type = assignee.type === "team" ? "Team" : "User";
      output += `**Assigned To**: ${assignee.name} (${type})\n`;
    }
  }

  // Add issue type and category for performance/metric issues
  if (issue.issueType) {
    output += `**Issue Type**: ${issue.issueType}\n`;
  }
  if (issue.issueCategory) {
    output += `**Issue Category**: ${issue.issueCategory}\n`;
  }

  output += `**Platform**: ${issue.platform}\n`;
  output += `**Project**: ${issue.project.name}\n`;
  output += `**URL**: ${apiService.getIssueUrl(organizationSlug, issue.shortId)}\n`;
  output += "\n";
  output += "## Event Details\n\n";

  // Check if this is an unsupported event type
  // Event type union is: ErrorEvent | DefaultEvent | TransactionEvent | GenericEvent | CspEvent
  // But in practice we may have other types returned as UnknownEvent
  const eventType = event.type;
  const isUnsupportedType =
    eventType !== "error" &&
    eventType !== "default" &&
    eventType !== "transaction" &&
    eventType !== "generic" &&
    eventType !== "csp";

  if (isUnsupportedType) {
    // Log to Sentry for tracking new/unknown event types
    const sentryEventId = logIssue(
      `Unsupported event type encountered: ${String(eventType)}`,
      {
        contexts: {
          event: {
            event_id: event.id,
            event_type: eventType,
            issue_id: issue.id,
            issue_short_id: issue.shortId,
            organization_slug: organizationSlug,
            project_slug: issue.project.slug,
          },
        },
      },
    );

    output += `⚠️  **Warning**: Unsupported event type "${String(eventType)}"\n\n`;
    output += "This event type is not yet fully supported by the MCP server. ";
    output += "Only basic issue information is shown above.\n\n";
    output +=
      "**Please report this**: Open a GitHub issue at https://github.com/getsentry/sentry-mcp/issues/new ";
    output += `and include Event ID **${event.id}**`;
    if (sentryEventId) {
      output += ` and Sentry Event ID **${sentryEventId}**`;
    }
    output += " to help us add support for this event type.\n";

    // For unsupported event types, return early without trying to render event details
    return output;
  }

  output += `**Event ID**: ${event.id}\n`;
  output += `**Type**: ${event.type}\n`;
  // "default" type represents error events without exception data
  // "generic" type represents performance regressions and metric-based issues
  // "csp" type represents Content Security Policy violations
  if (
    event.type === "error" ||
    event.type === "default" ||
    event.type === "generic" ||
    event.type === "csp"
  ) {
    const typedEvent = event as
      | z.infer<typeof ErrorEventSchema>
      | z.infer<typeof DefaultEventSchema>
      | z.infer<typeof GenericEventSchema>
      | any; // CSP events don't have a schema yet
    if (typedEvent.dateCreated) {
      output += `**Occurred At**: ${new Date(typedEvent.dateCreated).toISOString()}\n`;
    }
  }
  if (event.message) {
    output += `**Message**:\n${event.message}\n`;
  }
  output += "\n";
  output += formatEventOutput(event, { performanceTrace });

  // Add Seer context if available
  if (autofixState) {
    output += formatSeerSummary(autofixState, organizationSlug, issue.shortId);
  }

  output += "# Using this information\n\n";
  output += `- You can reference the IssueID in commit messages (e.g. \`Fixes ${issue.shortId}\`) to automatically close the issue when the commit is merged.\n`;
  output +=
    "- The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.\n";
  return output;
}

```

--------------------------------------------------------------------------------
/packages/mcp-core/src/toolDefinitions.json:
--------------------------------------------------------------------------------

```json
[
  {
    "name": "analyze_issue_with_seer",
    "description": "Use Seer to analyze production errors and get detailed root cause analysis with specific code fixes.\n\nUse this tool when you need:\n- Detailed AI-powered root cause analysis\n- Specific code fixes and implementation guidance\n- Step-by-step troubleshooting for complex issues\n- Understanding why an error is happening in production\n\nWhat this tool provides:\n- Root cause analysis with code-level explanations\n- Specific file locations and line numbers where errors occur\n- Concrete code fixes you can apply\n- Step-by-step implementation guidance\n\nThis tool automatically:\n1. Checks if analysis already exists (instant results)\n2. Starts new AI analysis if needed (~2-5 minutes)\n3. Returns complete fix recommendations\n\n<examples>\n### User: \"What's causing this error? https://my-org.sentry.io/issues/PROJECT-1Z43\"\n\n```\nanalyze_issue_with_seer(issueUrl='https://my-org.sentry.io/issues/PROJECT-1Z43')\n```\n\n### User: \"Can you help me understand why this is failing in production?\"\n\n```\nanalyze_issue_with_seer(organizationSlug='my-organization', issueId='ERROR-456')\n```\n</examples>\n\n<hints>\n- Use this tool when you need deeper analysis beyond basic issue details\n- If the user provides an issueUrl, extract it and use that parameter alone\n- The analysis includes actual code snippets and fixes, not just error descriptions\n- Results are cached - subsequent calls return instantly\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "issueId": {
          "type": "string",
          "description": "The Issue ID. e.g. `PROJECT-1Z43`"
        },
        "issueUrl": {
          "type": "string",
          "format": "uri",
          "description": "The URL of the issue. e.g. https://my-organization.sentry.io/issues/PROJECT-1Z43"
        },
        "instruction": {
          "type": "string",
          "description": "Optional custom instruction for the AI analysis"
        }
      },
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": []
  },
  {
    "name": "create_dsn",
    "description": "Create an additional DSN for an EXISTING project.\n\nUSE THIS TOOL WHEN:\n- Project already exists and needs additional DSN\n- 'Create another DSN for project X'\n- 'I need a production DSN for existing project'\n\nDO NOT USE for new projects (use create_project instead)\n\nBe careful when using this tool!\n\n<examples>\n### Create additional DSN for existing project\n```\ncreate_dsn(organizationSlug='my-organization', projectSlug='my-project', name='Production')\n```\n</examples>\n\n<hints>\n- If the user passes a parameter in the form of name/otherName, its likely in the format of <organizationSlug>/<projectSlug>.\n- If any parameter is ambiguous, you should clarify with the user what they meant.\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "projectSlug": {
          "type": "string",
          "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool."
        },
        "name": {
          "type": "string",
          "description": "The name of the DSN to create, for example 'Production'."
        }
      },
      "required": ["organizationSlug", "projectSlug", "name"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["project:write"]
  },
  {
    "name": "create_project",
    "description": "Create a new project in Sentry (includes DSN automatically).\n\nUSE THIS TOOL WHEN USERS WANT TO:\n- 'Create a new project'\n- 'Set up a project for [app/service] with team [X]'\n- 'I need a new Sentry project'\n- Create project AND need DSN in one step\n\nDO NOT USE create_dsn after this - DSN is included in output.\n\nBe careful when using this tool!\n\n<examples>\n### Create new project with team\n```\ncreate_project(organizationSlug='my-organization', teamSlug='my-team', name='my-project', platform='javascript')\n```\n</examples>\n\n<hints>\n- If the user passes a parameter in the form of name/otherName, its likely in the format of <organizationSlug>/<teamSlug>.\n- If any parameter is ambiguous, you should clarify with the user what they meant.\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "teamSlug": {
          "type": "string",
          "description": "The team's slug. You can find a list of existing teams in an organization using the `find_teams()` tool."
        },
        "name": {
          "type": "string",
          "description": "The name of the project to create. Typically this is commonly the name of the repository or service. It is only used as a visual label in Sentry."
        },
        "platform": {
          "anyOf": [
            {
              "type": "string",
              "description": "The platform for the project. e.g., python, javascript, react, etc."
            },
            {
              "type": "null"
            }
          ],
          "description": "The platform for the project. e.g., python, javascript, react, etc.",
          "default": null
        }
      },
      "required": ["organizationSlug", "teamSlug", "name"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["project:write", "team:read"]
  },
  {
    "name": "create_team",
    "description": "Create a new team in Sentry.\n\nUSE THIS TOOL WHEN USERS WANT TO:\n- 'Create a new team'\n- 'Set up a team called [X]'\n- 'I need a team for my project'\n\nBe careful when using this tool!\n\n<examples>\n### Create a new team\n```\ncreate_team(organizationSlug='my-organization', name='the-goats')\n```\n</examples>\n\n<hints>\n- If any parameter is ambiguous, you should clarify with the user what they meant.\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "name": {
          "type": "string",
          "description": "The name of the team to create."
        }
      },
      "required": ["organizationSlug", "name"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["team:write"]
  },
  {
    "name": "find_dsns",
    "description": "List all Sentry DSNs for a specific project.\n\nUse this tool when you need to:\n- Retrieve a SENTRY_DSN for a specific project\n\n<hints>\n- If the user passes a parameter in the form of name/otherName, its likely in the format of <organizationSlug>/<projectSlug>.\n- If only one parameter is provided, and it could be either `organizationSlug` or `projectSlug`, its probably `organizationSlug`, but if you're really uncertain you might want to call `find_organizations()` first.\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "projectSlug": {
          "type": "string",
          "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool."
        }
      },
      "required": ["organizationSlug", "projectSlug"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["project:read"]
  },
  {
    "name": "find_organizations",
    "description": "Find organizations that the user has access to in Sentry.\n\nUse this tool when you need to:\n- View organizations in Sentry\n- Find an organization's slug to aid other tool requests\n- Search for specific organizations by name or slug\n\nReturns up to 25 results. If you hit this limit, use the query parameter to narrow down results.",
    "inputSchema": {
      "type": "object",
      "properties": {
        "query": {
          "anyOf": [
            {
              "type": "string",
              "description": "Search query to filter results by name or slug. Use this to narrow down results when there are many items."
            },
            {
              "type": "null"
            }
          ],
          "description": "Search query to filter results by name or slug. Use this to narrow down results when there are many items.",
          "default": null
        }
      },
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["org:read"]
  },
  {
    "name": "find_projects",
    "description": "Find projects in Sentry.\n\nUse this tool when you need to:\n- View projects in a Sentry organization\n- Find a project's slug to aid other tool requests\n- Search for specific projects by name or slug\n\nReturns up to 25 results. If you hit this limit, use the query parameter to narrow down results.",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "query": {
          "anyOf": [
            {
              "type": "string",
              "description": "Search query to filter results by name or slug. Use this to narrow down results when there are many items."
            },
            {
              "type": "null"
            }
          ],
          "description": "Search query to filter results by name or slug. Use this to narrow down results when there are many items.",
          "default": null
        }
      },
      "required": ["organizationSlug"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["project:read"]
  },
  {
    "name": "find_releases",
    "description": "Find releases in Sentry.\n\nUse this tool when you need to:\n- Find recent releases in a Sentry organization\n- Find the most recent version released of a specific project\n- Determine when a release was deployed to an environment\n\n<examples>\n### Find the most recent releases in the 'my-organization' organization\n\n```\nfind_releases(organizationSlug='my-organization')\n```\n\n### Find releases matching '2ce6a27' in the 'my-organization' organization\n\n```\nfind_releases(organizationSlug='my-organization', query='2ce6a27')\n```\n</examples>\n\n<hints>\n- If the user passes a parameter in the form of name/otherName, its likely in the format of <organizationSlug>/<projectSlug>.\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "projectSlug": {
          "anyOf": [
            {
              "type": "string",
              "description": "The project's slug. This will default to all projects you have access to. It is encouraged to specify this when possible."
            },
            {
              "type": "null"
            }
          ],
          "description": "The project's slug. This will default to all projects you have access to. It is encouraged to specify this when possible.",
          "default": null
        },
        "query": {
          "anyOf": [
            {
              "type": "string",
              "description": "Search for versions which contain the provided string."
            },
            {
              "type": "null"
            }
          ],
          "description": "Search for versions which contain the provided string.",
          "default": null
        }
      },
      "required": ["organizationSlug"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["project:read"]
  },
  {
    "name": "find_teams",
    "description": "Find teams in an organization in Sentry.\n\nUse this tool when you need to:\n- View teams in a Sentry organization\n- Find a team's slug to aid other tool requests\n- Search for specific teams by name or slug\n\nReturns up to 25 results. If you hit this limit, use the query parameter to narrow down results.",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "query": {
          "anyOf": [
            {
              "type": "string",
              "description": "Search query to filter results by name or slug. Use this to narrow down results when there are many items."
            },
            {
              "type": "null"
            }
          ],
          "description": "Search query to filter results by name or slug. Use this to narrow down results when there are many items.",
          "default": null
        }
      },
      "required": ["organizationSlug"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["team:read"]
  },
  {
    "name": "get_doc",
    "description": "Fetch the full markdown content of a Sentry documentation page.\n\nUse this tool when you need to:\n- Read the complete documentation for a specific topic\n- Get detailed implementation examples or code snippets\n- Access the full context of a documentation page\n- Extract specific sections from documentation\n\n<examples>\n### Get the Next.js integration guide\n\n```\nget_doc(path='/platforms/javascript/guides/nextjs.md')\n```\n</examples>\n\n<hints>\n- Use the path from search_docs results for accurate fetching\n- Paths should end with .md extension\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "path": {
          "type": "string",
          "description": "The documentation path (e.g., '/platforms/javascript/guides/nextjs.md'). Get this from search_docs results."
        }
      },
      "required": ["path"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": []
  },
  {
    "name": "get_event_attachment",
    "description": "Download attachments from a Sentry event.\n\nUse this tool when you need to:\n- Download files attached to a specific event\n- Access screenshots, log files, or other attachments uploaded with an error report\n- Retrieve attachment metadata and download URLs\n\n<examples>\n### Download a specific attachment by ID\n\n```\nget_event_attachment(organizationSlug='my-organization', projectSlug='my-project', eventId='c49541c747cb4d8aa3efb70ca5aba243', attachmentId='12345')\n```\n\n### List all attachments for an event\n\n```\nget_event_attachment(organizationSlug='my-organization', projectSlug='my-project', eventId='c49541c747cb4d8aa3efb70ca5aba243')\n```\n\n</examples>\n\n<hints>\n- If `attachmentId` is provided, the specific attachment will be downloaded as an embedded resource\n- If `attachmentId` is omitted, all attachments for the event will be listed with download information\n- The `projectSlug` is required to identify which project the event belongs to\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "projectSlug": {
          "type": "string",
          "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool."
        },
        "eventId": {
          "type": "string",
          "description": "The ID of the event."
        },
        "attachmentId": {
          "anyOf": [
            {
              "type": "string",
              "description": "The ID of the attachment to download."
            },
            {
              "type": "null"
            }
          ],
          "description": "The ID of the attachment to download.",
          "default": null
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        }
      },
      "required": ["organizationSlug", "projectSlug", "eventId"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["event:read"]
  },
  {
    "name": "get_issue_details",
    "description": "Get detailed information about a specific Sentry issue by ID.\n\nUSE THIS TOOL WHEN USERS:\n- Provide a specific issue ID (e.g., 'CLOUDFLARE-MCP-41', 'PROJECT-123')\n- Ask to 'explain [ISSUE-ID]', 'tell me about [ISSUE-ID]'\n- Want details/stacktrace/analysis for a known issue\n- Provide a Sentry issue URL\n\nDO NOT USE for:\n- General searching or listing issues (use search_issues)\n- Root cause analysis (use analyze_issue_with_seer)\n\nTRIGGER PATTERNS:\n- 'Explain ISSUE-123' → use get_issue_details\n- 'Tell me about PROJECT-456' → use get_issue_details\n- 'What happened in [issue URL]' → use get_issue_details\n\n<examples>\n### With Sentry URL (recommended - simplest approach)\n```\nget_issue_details(issueUrl='https://sentry.sentry.io/issues/6916805731/?project=4509062593708032&query=is%3Aunresolved')\n```\n\n### With issue ID and organization\n```\nget_issue_details(organizationSlug='my-organization', issueId='CLOUDFLARE-MCP-41')\n```\n\n### With event ID and organization\n```\nget_issue_details(organizationSlug='my-organization', eventId='c49541c747cb4d8aa3efb70ca5aba243')\n```\n</examples>\n\n<hints>\n- **IMPORTANT**: If user provides a Sentry URL, pass the ENTIRE URL to issueUrl parameter unchanged\n- When using issueUrl, all other parameters are automatically extracted - don't provide them separately\n- If using issueId (not URL), then organizationSlug is required\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "issueId": {
          "type": "string",
          "description": "The Issue ID. e.g. `PROJECT-1Z43`"
        },
        "eventId": {
          "type": "string",
          "description": "The ID of the event."
        },
        "issueUrl": {
          "type": "string",
          "format": "uri",
          "description": "The URL of the issue. e.g. https://my-organization.sentry.io/issues/PROJECT-1Z43"
        }
      },
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["event:read"]
  },
  {
    "name": "get_trace_details",
    "description": "Get detailed information about a specific Sentry trace by ID.\n\nUSE THIS TOOL WHEN USERS:\n- Provide a specific trace ID (e.g., 'a4d1aae7216b47ff8117cf4e09ce9d0a')\n- Ask to 'show me trace [TRACE-ID]', 'explain trace [TRACE-ID]'\n- Want high-level overview and link to view trace details in Sentry\n- Need trace statistics and span breakdown\n\nDO NOT USE for:\n- General searching for traces (use search_events with trace queries)\n- Individual span details (this shows trace overview)\n\nTRIGGER PATTERNS:\n- 'Show me trace abc123' → use get_trace_details\n- 'Explain trace a4d1aae7216b47ff8117cf4e09ce9d0a' → use get_trace_details\n- 'What is trace [trace-id]' → use get_trace_details\n\n<examples>\n### Get trace overview\n```\nget_trace_details(organizationSlug='my-organization', traceId='a4d1aae7216b47ff8117cf4e09ce9d0a')\n```\n</examples>\n\n<hints>\n- Trace IDs are 32-character hexadecimal strings\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "traceId": {
          "type": "string",
          "pattern": "^[0-9a-fA-F]{32}$",
          "description": "The trace ID. e.g. `a4d1aae7216b47ff8117cf4e09ce9d0a`"
        }
      },
      "required": ["organizationSlug", "traceId"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["event:read"]
  },
  {
    "name": "search_docs",
    "description": "Search Sentry documentation for SDK setup, instrumentation, and configuration guidance.\n\nUse this tool when you need to:\n- Set up Sentry SDK or framework integrations (Django, Flask, Express, Next.js, etc.)\n- Configure features like performance monitoring, error sampling, or release tracking\n- Implement custom instrumentation (spans, transactions, breadcrumbs)\n- Configure data scrubbing, filtering, or sampling rules\n\nReturns snippets only. Use `get_doc(path='...')` to fetch full documentation content.\n\n<examples>\n```\nsearch_docs(query='Django setup configuration SENTRY_DSN', guide='python/django')\nsearch_docs(query='source maps webpack upload', guide='javascript/nextjs')\n```\n</examples>\n\n<hints>\n- Use guide parameter to filter to specific technologies (e.g., 'javascript/nextjs')\n- Include specific feature names like 'beforeSend', 'tracesSampleRate', 'SENTRY_DSN'\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "query": {
          "type": "string",
          "minLength": 2,
          "maxLength": 200,
          "description": "The search query in natural language. Be specific about what you're looking for."
        },
        "maxResults": {
          "type": "integer",
          "minimum": 1,
          "maximum": 10,
          "default": 3,
          "description": "Maximum number of results to return (1-10)"
        },
        "guide": {
          "anyOf": [
            {
              "type": "string",
              "enum": [
                "javascript",
                "python",
                "java",
                "dotnet",
                "go",
                "php",
                "ruby",
                "android",
                "apple",
                "unity",
                "unreal",
                "rust",
                "elixir",
                "kotlin",
                "native",
                "dart",
                "godot",
                "nintendo-switch",
                "playstation",
                "powershell",
                "react-native",
                "xbox",
                "javascript/nextjs",
                "javascript/react",
                "javascript/gatsby",
                "javascript/remix",
                "javascript/vue",
                "javascript/angular",
                "javascript/hono",
                "javascript/svelte",
                "javascript/express",
                "javascript/fastify",
                "javascript/astro",
                "javascript/bun",
                "javascript/capacitor",
                "javascript/cloudflare",
                "javascript/connect",
                "javascript/cordova",
                "javascript/deno",
                "javascript/electron",
                "javascript/ember",
                "javascript/nuxt",
                "javascript/solid",
                "javascript/solidstart",
                "javascript/sveltekit",
                "javascript/tanstack-react",
                "javascript/wasm",
                "javascript/node",
                "javascript/koa",
                "javascript/nestjs",
                "javascript/hapi",
                "python/django",
                "python/flask",
                "python/fastapi",
                "python/celery",
                "python/tornado",
                "python/pyramid",
                "python/aiohttp",
                "python/anthropic",
                "python/airflow",
                "python/aws-lambda",
                "python/boto3",
                "python/bottle",
                "python/chalice",
                "python/dramatiq",
                "python/falcon",
                "python/langchain",
                "python/litestar",
                "python/logging",
                "python/loguru",
                "python/openai",
                "python/quart",
                "python/ray",
                "python/redis",
                "python/rq",
                "python/sanic",
                "python/sqlalchemy",
                "python/starlette",
                "dart/flutter",
                "dotnet/aspnetcore",
                "dotnet/maui",
                "dotnet/wpf",
                "dotnet/winforms",
                "dotnet/aspnet",
                "dotnet/aws-lambda",
                "dotnet/azure-functions",
                "dotnet/blazor-webassembly",
                "dotnet/entityframework",
                "dotnet/google-cloud-functions",
                "dotnet/extensions-logging",
                "dotnet/log4net",
                "dotnet/nlog",
                "dotnet/serilog",
                "dotnet/uwp",
                "dotnet/xamarin",
                "java/spring",
                "java/spring-boot",
                "java/android",
                "java/jul",
                "java/log4j2",
                "java/logback",
                "java/servlet",
                "go/echo",
                "go/fasthttp",
                "go/fiber",
                "go/gin",
                "go/http",
                "go/iris",
                "go/logrus",
                "go/negroni",
                "go/slog",
                "go/zerolog",
                "php/laravel",
                "php/symfony",
                "ruby/delayed_job",
                "ruby/rack",
                "ruby/rails",
                "ruby/resque",
                "ruby/sidekiq",
                "android/kotlin",
                "apple/ios",
                "apple/macos",
                "apple/watchos",
                "apple/tvos",
                "apple/visionos",
                "kotlin/multiplatform"
              ],
              "description": "Optional guide filter to limit search results to specific documentation sections. Use either a platform (e.g., 'javascript', 'python') or platform/guide combination (e.g., 'javascript/nextjs', 'python/django')."
            },
            {
              "type": "null"
            }
          ],
          "description": "Optional guide filter to limit search results to specific documentation sections. Use either a platform (e.g., 'javascript', 'python') or platform/guide combination (e.g., 'javascript/nextjs', 'python/django').",
          "default": null
        }
      },
      "required": ["query"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": []
  },
  {
    "name": "search_events",
    "description": "Search for events AND perform counts/aggregations - the ONLY tool for statistics and counts.\n\nSupports TWO query types:\n1. AGGREGATIONS (counts, sums, averages): 'how many errors', 'count of issues', 'total tokens'\n2. Individual events with timestamps: 'show me error logs from last hour'\n\nUSE THIS FOR ALL COUNTS/STATISTICS:\n- 'how many errors today' → returns count\n- 'count of database failures' → returns count\n- 'total number of issues' → returns count\n- 'average response time' → returns avg()\n- 'sum of tokens used' → returns sum()\n\nALSO USE FOR INDIVIDUAL EVENTS:\n- 'error logs from last hour' → returns event list\n- 'database errors with timestamps' → returns event list\n- 'trace spans for slow API calls' → returns span list\n\nDataset Selection (AI automatically chooses):\n- errors: Exception/crash events\n- logs: Log entries\n- spans: Performance data, AI/LLM calls, token usage\n\nDO NOT USE for grouped issue lists → use search_issues\n\n<examples>\nsearch_events(organizationSlug='my-org', naturalLanguageQuery='how many errors today')\nsearch_events(organizationSlug='my-org', naturalLanguageQuery='count of database failures this week')\nsearch_events(organizationSlug='my-org', naturalLanguageQuery='total tokens used by model')\nsearch_events(organizationSlug='my-org', naturalLanguageQuery='error logs from the last hour')\n</examples>\n\n<hints>\n- If the user passes a parameter in the form of name/otherName, it's likely in the format of <organizationSlug>/<projectSlug>.\n- Parse org/project notation directly without calling find_organizations or find_projects.\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "naturalLanguageQuery": {
          "type": "string",
          "minLength": 1,
          "description": "Natural language description of what you want to search for"
        },
        "projectSlug": {
          "anyOf": [
            {
              "type": "string",
              "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool.",
          "default": null
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "limit": {
          "type": "number",
          "minimum": 1,
          "maximum": 100,
          "default": 10,
          "description": "Maximum number of results to return"
        },
        "includeExplanation": {
          "type": "boolean",
          "default": false,
          "description": "Include explanation of how the query was translated"
        }
      },
      "required": ["organizationSlug", "naturalLanguageQuery"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["event:read"]
  },
  {
    "name": "search_issues",
    "description": "Search for grouped issues/problems in Sentry - returns a LIST of issues, NOT counts or aggregations.\n\nUses AI to translate natural language queries into Sentry issue search syntax.\nReturns grouped issues with metadata like title, status, and user count.\n\nUSE THIS TOOL WHEN USERS WANT:\n- A LIST of issues: 'show me issues', 'what problems do we have'\n- Filtered issue lists: 'unresolved issues', 'critical bugs'\n- Issues by impact: 'errors affecting more than 100 users'\n- Issues by assignment: 'issues assigned to me'\n\nDO NOT USE FOR COUNTS/AGGREGATIONS:\n- 'how many errors' → use search_events\n- 'count of issues' → use search_events\n- 'total number of errors today' → use search_events\n- 'sum/average/statistics' → use search_events\n\nALSO DO NOT USE FOR:\n- Individual error events with timestamps → use search_events\n- Details about a specific issue ID → use get_issue_details\n\nREMEMBER: This tool returns a LIST of issues, not counts or statistics!\n\n<examples>\nsearch_issues(organizationSlug='my-org', naturalLanguageQuery='critical bugs from last week')\nsearch_issues(organizationSlug='my-org', naturalLanguageQuery='unhandled errors affecting 100+ users')\nsearch_issues(organizationSlug='my-org', naturalLanguageQuery='issues assigned to me')\n</examples>\n\n<hints>\n- If the user passes a parameter in the form of name/otherName, it's likely in the format of <organizationSlug>/<projectSlugOrId>.\n- Parse org/project notation directly without calling find_organizations or find_projects.\n- The projectSlugOrId parameter accepts both project slugs (e.g., 'my-project') and numeric IDs (e.g., '123456').\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "naturalLanguageQuery": {
          "type": "string",
          "minLength": 1,
          "description": "Natural language description of issues to search for"
        },
        "projectSlugOrId": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "The project's slug or numeric ID (optional)"
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "limit": {
          "type": "number",
          "minimum": 1,
          "maximum": 100,
          "default": 10,
          "description": "Maximum number of issues to return"
        },
        "includeExplanation": {
          "type": "boolean",
          "default": false,
          "description": "Include explanation of how the query was translated"
        }
      },
      "required": ["organizationSlug", "naturalLanguageQuery"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["event:read"]
  },
  {
    "name": "update_issue",
    "description": "Update an issue's status or assignment in Sentry. This allows you to resolve, ignore, or reassign issues.\n\nUse this tool when you need to:\n- Resolve an issue that has been fixed\n- Assign an issue to a team member or team for investigation\n- Mark an issue as ignored to reduce noise\n- Reopen a resolved issue by setting status to 'unresolved'\n\n<examples>\n### Resolve an issue\n\n```\nupdate_issue(organizationSlug='my-organization', issueId='PROJECT-123', status='resolved')\n```\n\n### Assign an issue to a user (use whoami to get your user ID)\n\n```\nupdate_issue(organizationSlug='my-organization', issueId='PROJECT-123', assignedTo='user:123456')\n```\n\n### Assign an issue to a team\n\n```\nupdate_issue(organizationSlug='my-organization', issueId='PROJECT-123', assignedTo='team:789')\n```\n\n### Mark an issue as ignored\n\n```\nupdate_issue(organizationSlug='my-organization', issueId='PROJECT-123', status='ignored')\n```\n\n</examples>\n\n<hints>\n- If the user provides the `issueUrl`, you can ignore the other required parameters and extract them from the URL.\n- At least one of `status` or `assignedTo` must be provided to update the issue.\n- assignedTo format: Use 'user:ID' for users (e.g., 'user:123456') or 'team:ID' for teams (e.g., 'team:789')\n- To find your user ID, first use the whoami tool which returns your numeric user ID\n- Valid status values are: 'resolved', 'resolvedInNextRelease', 'unresolved', 'ignored'.\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "issueId": {
          "type": "string",
          "description": "The Issue ID. e.g. `PROJECT-1Z43`"
        },
        "issueUrl": {
          "type": "string",
          "format": "uri",
          "description": "The URL of the issue. e.g. https://my-organization.sentry.io/issues/PROJECT-1Z43"
        },
        "status": {
          "type": "string",
          "enum": [
            "resolved",
            "resolvedInNextRelease",
            "unresolved",
            "ignored"
          ],
          "description": "The new status for the issue. Valid values are 'resolved', 'resolvedInNextRelease', 'unresolved', and 'ignored'."
        },
        "assignedTo": {
          "type": "string",
          "description": "The assignee in format 'user:ID' or 'team:ID' where ID is numeric. Example: 'user:123456' or 'team:789'. Use the whoami tool to find your user ID."
        }
      },
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["event:write"]
  },
  {
    "name": "update_project",
    "description": "Update project settings in Sentry, such as name, slug, platform, and team assignment.\n\nBe careful when using this tool!\n\nUse this tool when you need to:\n- Update a project's name or slug to fix onboarding mistakes\n- Change the platform assigned to a project\n- Update team assignment for a project\n\n<examples>\n### Update a project's name and slug\n\n```\nupdate_project(organizationSlug='my-organization', projectSlug='old-project', name='New Project Name', slug='new-project-slug')\n```\n\n### Assign a project to a different team\n\n```\nupdate_project(organizationSlug='my-organization', projectSlug='my-project', teamSlug='backend-team')\n```\n\n### Update platform\n\n```\nupdate_project(organizationSlug='my-organization', projectSlug='my-project', platform='python')\n```\n\n</examples>\n\n<hints>\n- If the user passes a parameter in the form of name/otherName, it's likely in the format of <organizationSlug>/<projectSlug>.\n- Team assignment is handled separately from other project settings\n- If any parameter is ambiguous, you should clarify with the user what they meant.\n- When updating the slug, the project will be accessible at the new slug after the update\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "organizationSlug": {
          "type": "string",
          "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
        },
        "regionUrl": {
          "anyOf": [
            {
              "type": "string",
              "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
          "default": null
        },
        "projectSlug": {
          "type": "string",
          "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool."
        },
        "name": {
          "anyOf": [
            {
              "type": "string",
              "description": "The new name for the project"
            },
            {
              "type": "null"
            }
          ],
          "description": "The new name for the project",
          "default": null
        },
        "slug": {
          "anyOf": [
            {
              "type": "string",
              "description": "The new slug for the project (must be unique)"
            },
            {
              "type": "null"
            }
          ],
          "description": "The new slug for the project (must be unique)",
          "default": null
        },
        "platform": {
          "anyOf": [
            {
              "type": "string",
              "description": "The platform for the project. e.g., python, javascript, react, etc."
            },
            {
              "type": "null"
            }
          ],
          "description": "The platform for the project. e.g., python, javascript, react, etc.",
          "default": null
        },
        "teamSlug": {
          "anyOf": [
            {
              "type": "string",
              "description": "The team's slug. You can find a list of existing teams in an organization using the `find_teams()` tool."
            },
            {
              "type": "null"
            }
          ],
          "description": "The team to assign this project to. Note: this will replace the current team assignment.",
          "default": null
        }
      },
      "required": ["organizationSlug", "projectSlug"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": ["project:write"]
  },
  {
    "name": "use_sentry",
    "description": "Natural language interface to Sentry via an embedded AI agent.\n\nUse this tool when you need to:\n- Perform complex multi-step operations\n- Explore and analyze Sentry data with natural language\n- Chain multiple operations automatically\n\nCapabilities depend on granted skills:\n- inspect: Search errors/events, analyze traces, explore issues and projects\n- seer: Get AI-powered debugging insights and root cause analysis\n- docs: Search and retrieve Sentry documentation\n- triage: Resolve, assign, comment on, and update issues\n- project-management: Create/modify teams, projects, and configure DSNs\n\n<examples>\nuse_sentry(request='find unresolved errors from yesterday')\nuse_sentry(request='analyze the top 3 performance issues')\nuse_sentry(request='create a backend team and assign them to API project')\n</examples>\n\n<hints>\n- If user asks to 'use Sentry' for something, they always mean to call this tool\n- Pass the user's request verbatim - do not interpret or rephrase\n- The agent can chain multiple tool calls automatically\n- Use trace=true parameter to see which tools were called\n- For simple single-tool operations, consider calling tools directly instead\n</hints>",
    "inputSchema": {
      "type": "object",
      "properties": {
        "request": {
          "type": "string",
          "minLength": 1,
          "description": "The user's raw input. Do not interpret the prompt in any way. Do not add any additional information to the prompt."
        },
        "trace": {
          "type": ["boolean", "null"],
          "default": null,
          "description": "Enable tracing to see all tool calls made by the agent. Useful for debugging."
        }
      },
      "required": ["request"],
      "additionalProperties": false,
      "$schema": "http://json-schema.org/draft-07/schema#"
    },
    "requiredScopes": []
  },
  {
    "name": "whoami",
    "description": "Identify the authenticated user in Sentry.\n\nUse this tool when you need to:\n- Get the user's name and email address.",
    "inputSchema": {},
    "requiredScopes": []
  }
]

```
Page 14/16FirstPrevNextLast