Sentry's DSN explainer still says the quiet part plainly: "DSNs are safe to keep public because they only allow submission of new events and related event data; they do not allow read access to any information." That was sound advice for the threat model it was written in, where the only consumer of an error stream was a human squinting at a dashboard (docs.sentry.io). Point an issue-reading MCP server at the same project and the sentence stops covering the case. Tenet Security's Threat Labs found 2,388 organisations with injectable DSNs, 71 of them in the Tranco top 1M, and reported an 85% success rate getting coding agents to execute a command planted inside a fabricated error.

Short version. Agentjacking works because the Model Context Protocol draws its trust boundary around the server, not the third-party data a trusted server faithfully relays. Sentry's MCP server is trusted, behaves correctly, and hands your agent a payload an unauthenticated stranger composed. Four months after disclosure, the untrusted-telemetry boundary in Sentry's own MCP code is still an unmerged draft, and the one security change that did merge hardens the hop the spec actually names.

What a public DSN actually authorises

The DSN is a write-only credential, documented as safe to embed in frontend JavaScript, where anyone with devtools can read it. Nothing about that changed. What changed is the second consumer. On a project whose issues an MCP server reads, that same string is an unauthenticated write into an engineer's context window, on a path that terminates at a shell.

Compare the neighbouring product. PagerDuty's Events API v2 docs take the opposite line on their routing key: treat it like a password, keep it server-side, never ship it to a browser. Same shape of ingest endpoint, opposite documented policy. Write-reachability of a telemetry store is a per-vendor choice, and the Sentry MCP server inherits the most open one in the category. Public GitHub issues are more open still, since they need no secret at all.

If you have ever audited who writes to your own telemetry, this will feel familiar. When I pulled apart my analytics and found 70% of it was a bot that executed my JavaScript, the lesson was that an ingest endpoint collects whatever the internet feels like sending. That was a measurement problem. Now the same rows get read aloud to something holding a terminal.

Where the MCP spec draws its line

The current revision is 2026-07-28, built on schema/2026-07-28/schema.ts. Its Security and Trust & Safety material tells clients to distrust servers: "descriptions of tool behavior such as annotations should be considered untrusted, unless obtained from a trusted server" (modelcontextprotocol.io). Consent before tool invocation, untrusted annotations from untrusted servers, both covered.

There is no provenance field on a tool result. No way for a server to mark "characters 400 through 900 of this blob were typed by an anonymous HTTP client," and therefore nothing for a client to enforce. The omission is the finding, and it explains the patch asymmetry below better than any story about prioritisation.

It also finishes an argument I started when I wrote that readOnlyHint is a self-claim, not a permission. Read versus write on the tool was never the useful axis. The question is who can write to the store a read tool reads.

Unauthenticated posterholding a public DSNSentry ingest endpointSentry issue storesentry-mcpget_issue_detailsAgent context windowShell commandapproved?npx runs with your rightsspec covers this hopjustified by event textno provenance on results

Four months of patches, and the one that merged

The attack has been covered since Tenet's June disclosure, including in The New Stack. The patch history has not been read out, and it is more instructive than the attack.

What merged is origin validation. getsentry/toolkit PR #1444, "Share strict Sentry origin checks across CLI and MCP," landed on 7 October 2026. It moves HTTPS and default-port SaaS trust validation into a shared @sentry/toolkit-core/sentry-origin module, rejects URL credentials ahead of product-specific checks, and applies a region allowlist for MCP. Good change, correctly scoped, and it answers the question the spec asks: is this server the real Sentry? The question the spec does not ask went untouched.

The structured-output route was closed, not shipped. PR #1045 added experimental structuredContent payloads for get_issue_details, search_events and search_issue_events behind one broad security note; reviewers flagged raw API object exposure and thin adversarial coverage, and it was closed on 28 July 2026 as superseded by per-tool migrations in #1193.

The direction with the most design thought behind it is still an open issue. Issue #1093, "Expose event provenance and action gates in MCP tool results," has been open since 15 June 2026. In it, Sentry's dcramer cautions that existing test cases show markers are insufficient for model safety, and the thread moves toward eval fixtures containing injected commands as regression tests. The proposal that survives is action gates: a recommendation sourced from untrusted event text must be corroborated against the codebase before any shell command, package install or URL fetch. That is a deterministic client-side constraint and the only item in the thread that does not depend on a model choosing well.

Watching four patches take four different views of the same class of bug is a pattern I have written up before, when four agent patches for the same core.fsmonitor RCE shipped four different key lists. When the specification does not name the boundary, every implementer draws their own, and you inherit the union of the gaps.

The boundary marker that already leaks

PR #1056, open as a draft since 5 June 2026, is the untrusted-data boundary. It wraps user-controlled telemetry from get_issue_details in a single <untrusted_event_telemetry> section, entity-encodes literal closing tags to stop boundary breakout, and ships an LLM evaluation canary. Thoughtful work.

Its reviewer bot then found that trace_id, read from event.contexts.trace, is interpolated into the trusted "Response Notes" section after the boundary closes, with no format validation. The trace context arrives from the client. A newline inside that value writes attacker text into trusted output, outside the wrapper built to contain it.

That generalises into a review rule. An XML-boundary scheme is worth exactly the proof that no field rendered outside the boundary is client-influenced, and trace_id passes the eyeball test because it reads like server metadata. It is the same mistake as assuming retrieved context inherits the permissions of its source, which is how RAG chunks end up carrying no ACL at all.

Why CI is the expensive case

Tenet's validation window ran 2 to 17 June 2026 across Claude Code (version 2.1.161 documented), Cursor and OpenAI Codex, with more than 100 confirmed executions and sandboxed CI variants included. The payload in the published write-up was a single npx invocation inside a fabricated "Resolution" section, running with the developer's own privileges.

Two details set the blast radius. Tenet reported that agents ran the payload even where the system prompt explicitly told them to disregard untrusted external data, which disqualifies every mitigation currently circulating as prompt text. And an agent triaging Sentry issues inside CI is holding a registry token, a cloud OIDC credential and write access to a release pipeline, which is a worse set of keys than any laptop in the org.

No Sentry field proves provenance

sdk.name and sdk.version are documented searchable event properties, described as the "Name of the Sentry SDK that sent the event," alongside release, dist, http.url, platform and user.ip (docs.sentry.io). Every one of them travels inside the client envelope. A search for !has:sdk.name catches a lazy forgery and says nothing about a careful one.

The control that survives contact is partitioning rather than detection, because it changes who can write at all.

What to change before your next agent triage run

  1. Reclassify the DSN on every project an MCP server reads. Mint a fresh client key, serve ingest only through your own server-side tunnel endpoint so the browser never carries a DSN, then rotate or revoke the old key in the project's Client Keys (DSN) settings. Leave the legacy key live for a measurement window and treat residual volume on it as your injection surface. IP blocking is available in the same settings for the loudest sources.
  2. Gate the action in config, not in the prompt. Deny npx, curl | sh, package installs and network fetches whose justification traces to event text. In Claude Code that is a deny rule such as Bash(npx:*) in settings; in CI it is the allowlist on the runner. Tenet's finding that system-prompt warnings did not stop execution is the reason this cannot live in instructions.
  3. Re-review any marker-based mitigation you already shipped. List every field your tool renders outside the untrusted wrapper and prove each one is server-generated. trace_id in PR #1056 is the template: it looks like infrastructure and arrives from the client.
  4. Add an eval fixture with an injected command. That is the direction issue #1093 landed on, and it is the only regression test that fails when someone widens the boundary later.
  5. Drop read-versus-write from your MCP review checklist. Replace it with one question per tool: who can write a row this tool will later read? Public issue trackers, shared support inboxes and any ingest endpoint documented as safe to publish all answer "anyone."

Do the first two this week. The ingest tunnel takes an afternoon and the deny rules take ten minutes, and together they remove both the anonymous write and the unattended execution. The rest is review discipline, which matters most for whoever ships the merged version of PR #1056.