Skip to content

perf(cli): Give __complete its own light entry point - #1762

Merged
BYK merged 2 commits into
mainfrom
perf/cli-complete-fast-entry
Oct 31, 2026
Merged

BYK merged 2 commits into
mainfrom
perf/cli-complete-fast-entry

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Shell completion runs sentry __complete on every TAB press, and since #1760 also for every flag name in bash and fish. Each call was paying for most of the CLI before it printed anything. This addresses the review thread on #1751 (complete.ts).

Where the time went

  • The npm bin required the whole 7.6 MB dist/index.cjs just to reach the completion branch in cli.ts.
  • The completion engine's eager import graph still reached @sentry/node, even with telemetry off. db/auth.ts and db/user.ts imported withDbSpan from telemetry.ts, the cache modules imported recordCacheHit, and logger.ts imported createConsolaReporter. That graph was 659 modules: Sentry SDK, OpenTelemetry, and the API client. The completers themselves cost under 10 ms.

What changed

  • src/bin.ts and dist/bin.cjs check for __complete first. They call runCompletion in src/lib/complete.ts without loading cli.ts. The npm package ships that path as a separate dist/complete.cjs (322 KB). The SEA binary takes it as a lazily evaluated chunk of its single bundle.
  • The database modules now get withDbSpan and recordCacheHit from db/index.ts. Like the existing tracing proxy there, these require telemetry lazily and skip it when telemetry is off, which runCompletion sets. initSentry now passes the Sentry reporter to attachSentryReporter, so logger.ts no longer imports the SDK.
  • The eager graph is down to 40 modules. Completion output and the protocol are unchanged: I diffed the output of several cases between the old and new npm and SEA builds.

Numbers. Median of 20 runs on Node 24 (Linux x64), against a seeded config DB:

bare node issue list "" issue list -- init ./ (directory)
npm before 26 ms 356 ms 359 ms 346 ms
npm after 26 ms 67 ms 72 ms 76 ms
SEA before 28 ms 145 ms 148 ms 145 ms
SEA after 27 ms 97 ms 97 ms 99 ms

In dev (tsx), the same calls went from about 650 ms to about 315 ms; most of what's left is tsx's own startup.

Guard. test/script/cli-startup.test.ts bundles lib/complete.ts without its deferred imports. It fails on any package outside COMPLETION_PACKAGES, naming the importer, or when there are more than 50 source modules. It also now covers bin.ts. The npm e2e suite checks that __complete loads complete.cjs and never index.cjs. The "Shell completion" section of AGENTS.md describes the rule.

What's left. consola is about 15 ms of the remaining npm time, and Stricli (through errors.ts) about 3 ms. In the SEA binary, the rest is mostly the cost of loading the single 7.6 MB embedded script. Each of these would need a larger change (a lazy logger, a second SEA script), so they are left for follow-ups.

via BYK.

--

View Session in Sentry

Shell completion runs on every TAB press, but `sentry __complete` paid
for most of the CLI before printing anything. The npm bin required the
whole 7.6 MB `dist/index.cjs` (~340 ms per call), and the completion
engine's static imports reached `@sentry/node` anyway: `db/auth.ts` and
the cache modules imported telemetry, and `logger.ts` imported
`createConsolaReporter`. Its eager graph was 659 modules.

`src/bin.ts` and `dist/bin.cjs` now check for `__complete` first and
call `runCompletion` in `lib/complete.ts` without loading `cli.ts`; the
npm package ships it as a separate `dist/complete.cjs` (322 KB). The
database modules get `withDbSpan` and `recordCacheHit` from
`db/index.ts`, which requires telemetry lazily like the existing
tracing proxy and skips it with telemetry off, and `initSentry` hands
the logger its Sentry reporter. The eager graph is now 40 modules.

Median on Node 24 (`issue list ""`): npm 356 -> 67 ms, SEA binary
145 -> 97 ms, bare node 26 ms. Completion output is unchanged.

`test/script/cli-startup.test.ts` bundles `lib/complete.ts` and fails on
any package outside an allowlist or more than 50 source modules, and
the npm e2e test checks `__complete` never loads `index.cjs`.

Co-Authored-By: BYK <burak.kaya@sentry.io>
@vercel

vercel Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
sentry-local Skipped Skipped Oct 31, 2026 2:05pm UTC

Request Review

@github-actions github-actions Bot added the risk: medium PR risk score: medium label Oct 30, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f410427. Configure here.

Comment thread packages/cli/test/e2e/bundle.test.ts Resolved
Comment thread packages/cli/src/bin.ts Resolved
Comment thread packages/cli/script/bundle.ts Resolved
Comment thread packages/cli/src/lib/logger.ts Resolved
Comment thread packages/cli/src/bin.ts Resolved
BYK added a commit that referenced this pull request Oct 31, 2026
Part of #1761.

Zod 4's `core`, `classic`, and `mini` entry points all re-export
`locales/index.js`, which statically imports every locale. Nothing we
bundle calls `z.config(z.locales.xx())`; Zod sets its English default by
importing `locales/en.js` directly. A new esbuild plugin,
`script/lib/zod-locales-plugin.ts`, resolves Zod's locales index to a
module that exports only `en`. Error messages stay the same, and
`z.locales.en` still works.

The npm bundle (`script/bundle.ts`) and the standalone binary
(`script/build.ts`) now use the plugin. Both inline `@sentry/mcp-core`
for `sentry mcp`. The plugin lives in `script/lib/` so the
`@sentry/mcp-server` bundle from #1731 can import it next to
`require-alias-plugin.ts`.

| | before | after |
| --- | --- | --- |
| CLI `dist/index.cjs` | 7,869,567 B | 7,605,958 B (−263.6 KB) |
| Zod in the CLI bundle | 547,614 B | 284,024 B |
| `sentry --version` max RSS | 210.4 MB | 207.6 MB |
| `sentry mcp --version` max RSS | 178.0 MB | 175.0 MB |
| `sentry mcp` idle max RSS (initialize + tools/list) | 180.5 MB | 177.3
MB |
| `@sentry/mcp-server` bundle (#1731 branch, plugin applied locally) |
3,663,084 B | 3,399,473 B |
| `sentry-mcp --version` / idle max RSS (#1731) | 137 / 143 MB | 134 /
141 MB |

Byte counts come from the esbuild metafile. RSS is
`process.resourceUsage().maxRSS` from a `-r` preload, on Node 22, 3 runs
each.

The new test bundles real Zod, ESM and CJS, with and without the plugin.
It checks that the messages match and that `z.locales` has only `en`.

This touches `script/bundle.ts` in two lines, an import and one entry at
the top of the `plugins` array. #1762 adds code just below that array,
so the two may need a trivial rebase.

<!-- junior-request-attribution:start -->
via **BYK**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3A97f516d754ebba138e04ff1efc71c075 -->

--

[View Session in
Sentry](https://sentry.sentry.io/explore/conversations/agent%3A97f516d754ebba138e04ff1efc71c075/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <burak.kaya@sentry.io>
…fine

Replace the esbuild resolve plugin that externalized the lazy
telemetry require with a SENTRY_CLI_COMPLETION_BUILD define. The
complete.cjs build sets it to true, so esbuild drops the require and
@sentry/node from that bundle. Source runs keep the
SENTRY_CLI_NO_TELEMETRY check.
@BYK
BYK merged commit 4b12058 into main Oct 31, 2026
26 checks passed
@BYK
BYK deleted the perf/cli-complete-fast-entry branch October 31, 2026 14:32
BYK added a commit that referenced this pull request Oct 31, 2026
This is groundwork for #1731. That PR builds `@sentry/mcp-server` from
the real CLI runner with a smaller route map (`{ mcp, auth, help }`,
`mcp` as the default command). Today that can't work: `lib/help.ts` and
`lib/completions.ts` import `routes` from `app.ts`, and
`lib/list-command.ts` loads it lazily with `require()`. Every command
imports `help.ts` through `buildCommand`, so bundling any one command
pulls in the whole CLI: every other command, `sentry init`, and Mastra.

`app.ts` now registers its route map with `setRoutes()` in the new
`lib/route-registry.ts`. `help`, `completions`, `list-command`, and the
`schema` command read it from there. `getRoutes()` throws a clear error
when nothing has been registered. `list-command` uses `findRoutes()`
instead, so a command's `func` called on its own, as unit tests do,
still runs without subcommand hints, as it did before. Another entry
point can register a smaller map.

Output doesn't change: `sentry --help`, `sentry help --json`, `sentry
cli completion bash`, `sentry schema issues --json`, and `sentry
projects list --help` are byte-identical before and after, from source
and from the npm bundle. The plural-alias hint (`sentry projects list` →
`Tip: "list" is a subcommand`) still works in the bundle.

Bundle size with an esbuild metafile (minified ESM, the same plugins as
`script/bundle.ts`):

| Entry | Before | After |
|---|---|---|
| `commands/auth/whoami.ts` | 1822 modules, 194 command files, 5.62 MB |
1156 modules, 1 command file, 2.89 MB |
| `lib/command.ts` | 1822 modules, 5.62 MB | 1107 modules, 2.55 MB |
| `commands/auth/index.ts` | 1822 modules, 5.62 MB | 1171 modules, 2.93
MB |
| `commands/mcp.ts` | 1822 modules, 5.62 MB | 1108 modules, 2.55 MB |

After the change, none of these include `app.ts` or `@mastra/*`.

Guard: `test/lib/route-registry.test.ts` bundles every module in
`src/lib/` and fails, naming the importer, if any of them reaches
`app.ts`. It follows lazy `_require()` loads too. `sdk-invoke.ts` is the
one exception: it's the library-mode runner, reached only from
`src/index.ts`, and it executes the whole app. The three
`interceptSubcommand` tests that were skipped because that `require()`
failed under vitest now run. `list-command` no longer needs the
`require-alias` rewrite, so it's removed from the filters in
`script/build.ts` and `script/bundle.ts`.

Not included, left for #1731 or later PRs: `cli.ts` still imports
`./app.js` for `run()` and unknown-command reporting. Making the
application injectable is the next step. I left `cli.ts` alone here to
stay clear of #1762. This branch merges cleanly with #1762. `whoami`
also still reaches `lib/init/ui/ink-ui.ts` through `resolve-target.ts`
(the Ink prompt UI, not the init wizard).

<!-- junior-request-attribution:start -->
via **BYK**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3A9214c867676f0f724d594cfe3d630133 -->

--

[View Session in
Sentry](https://sentry.sentry.io/explore/conversations/agent%3A9214c867676f0f724d594cfe3d630133/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <burak.kaya@sentry.io>
sentry-junior Bot added a commit that referenced this pull request Oct 31, 2026
Brings in #1762 (light `__complete` entry point). Completion now runs from
the bin entry points through `lib/complete.ts`'s `runCompletion`, so the
branch's `CliEntry.complete` hook and `startCli()`'s completion branch are
removed; `bin.ts` keeps main's dispatch and passes `SENTRY_CLI` to
`startCli()`. #1762's comment cleanup in `preloadProjectContext` is applied
to `lib/project-context.ts`, where this branch moved it.
sentry-junior Bot added a commit that referenced this pull request Oct 31, 2026
Adds `mcp-server.ts` to #1762's startup guard. The entry's eager graph is
14 source modules with no `@sentry/*` SDK import, so `--version` and a lone
`-v` return before `@sentry/node` or OpenTelemetry load; the guard keeps it
that way.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Nov 2, 2026
…s CLI auth (#1731)

> **Base:** #1763 (Zod English locale), #1764 (lazy server stack), #1765
(lazy syntax highlighting), #1762 (light `__complete` entry) and #1766
(route registry) are now on `main` and merged into this branch with
merge commits (`cc2ae2e7`; `0a864625` for #1762; `5a72e8a3` for #1766,
which leaves the tree unchanged because this branch already carried
#1766; `b7a34abd` for #1770 and `23a92f9e` for #1771). With #1766,
`help.ts`, `completions.ts`, `list-command.ts`, and `schema` read routes
from `lib/route-registry.ts`, and `buildCliApplication()` calls
`setRoutes()` for both `app.ts` and `mcp-server-app.ts`; those files and
the `REQUIRE_ALIAS_FILTER` (no `list-command`) match `main`, so no
interim workaround is left. With #1762, shell completion runs from the
bin entry points, so `CliEntry` only carries `loadApp`; the branch's own
completion hook is gone. #1764 only touched
`packages/mcp-server/src/index.ts`, which this PR deletes; its
lazy-loading idea lives in the unified entry here. This PR's own changes
are `261e371f` (MCP SDK v2), `ffeaf9ba` (unified entry), `7a2f0a2a`
(stdio-auth docs), `69e65252` (lone `-v`), `9977bb0c` (no mcp-core
import in the command tree), `c0243b5a` (MCP SDK v1 removed from
mcp-core), `07b8d4c8` (`zod/v3` stub in both bundles), `ccdd87ab`
(`--access-token` redacted from telemetry), `a98cb249` (`--access-token`
as a deprecated alias), `0e227f71` (help's kitty/sixel banner out of the
bundle), `ed4eddf6` (start the server when its cwd was deleted),
`3392ea03` (startup guard for the bundle's `--version` entry), and
`88c11b37` (bundle size note).

`@sentry/mcp-server` stays the recommended stdio install. Its server
code moves into the CLI so it shares the CLI's auth, and the package
becomes a slim, dependency-free bundle of that code (see the
`@sentry/mcp-server` section below). The CLI exposes the same server as
`sentry mcp`. This also locks in a one-way package graph, so MCP tools
can later be built on the CLI's portable operations without a cycle.

## What moved where

- **stdio transport**: `packages/mcp-server/src/transports/stdio.ts` →
`packages/mcp-core/src/transports/stdio.ts`, exported as
`@sentry/mcp-core/transports/stdio`. It now uses `@sentry/core` tracing,
so mcp-core takes no Node SDK dependency.
- **Server host**: `mcp` is a real Stricli command
(`packages/cli/src/commands/mcp.ts`) with its flags declared, so help is
generated like every other command. Env merging and config validation
live in `packages/cli/src/lib/mcp/{args,config}.ts`, agent provider and
telemetry setup in `lib/mcp/server.ts`. mcp-core stays free of CLI
concepts. The command loads `lib/mcp.ts`, and that loads the server
stack, only when it serves. The command module itself imports nothing
from mcp-core (its help copies the skill names, and a test checks them
against `ALL_SKILLS`), so docs generation and `pnpm run cli` still work
without building mcp-core.
- **Standalone auth** (device code flow, bundled client ID,
`~/.sentry/mcp.json` cache, `auth` subcommands): deleted. CLI auth
replaces it.
- **Consumers**: evals import `@sentry/mcp-core/transports/stdio`. The
test client launches `sentry mcp` from CLI source. Smoke tests only
target the hosted worker, so they needed no change.
`ensure-generated.mjs` now sends generator output to stderr, so a first
`pnpm run cli mcp` cannot write junk into the JSON-RPC stream.
- **Refresh after a 401**: mcp-core copies the handler context per call,
so a token returned from `onUpstreamUnauthorized` was dropped before.
The refreshed token now reaches the context, and a regression test
checks that later calls use it.
- **Install snippets and docs** lead with `npx
@sentry/mcp-server@latest`; the web install instructions are unchanged.
User-facing docs mention the shared CLI login but do not steer installs
to `sentry mcp`; the `mcp-qa` skill drives the server from CLI source.
`docs/operations/stdio-auth.md` and `docs/releases/stdio.md` are
rewritten package-first, and `stdio-auth.md` documents that each server
process uses one account.

## Parity

| Old `@sentry/mcp-server` | New `@sentry/mcp-server` (and `sentry mcp`)
|
| --- | --- |
| `--access-token` / `SENTRY_ACCESS_TOKEN` | **Deprecated alias.** Runs
as if `SENTRY_AUTH_TOKEN=<token>` and `SENTRY_FORCE_ENV_TOKEN=1` were
set, through the same env-token path, so it wins over a stored login. It
also replaces an existing `SENTRY_AUTH_TOKEN`/`SENTRY_TOKEN`, since the
flag is the explicit choice. The flag wins over the env var. The token
is sent to the `--host`/`--url` target as before; an org token
(`sntrys_`) issued for another host is refused with `HostScopeError`.
Prints one deprecation warning on stderr, without the token; the flag
value is redacted from telemetry. Docs, help, and `server.json` now lead
with `SENTRY_AUTH_TOKEN` (+ `SENTRY_FORCE_ENV_TOKEN=1`, and
`SENTRY_HOST`/`SENTRY_URL` for self-hosted). |
| No token: device code flow plus `~/.sentry/mcp.json` cache | CLI
session (`sentry auth login`, or `SENTRY_AUTH_TOKEN`/`SENTRY_TOKEN`),
refreshed after an upstream 401. In a TTY it starts the CLI login. Over
pipes it fails with an error naming the login command: `npx -y
@sentry/mcp-server auth login` from the package, `sentry auth login`
from `sentry mcp`. The old cache is not read. |
| `--host` / `SENTRY_HOST`, `--url` / `SENTRY_URL` | Same flags. The env
values come from the CLI's configured URL, which is translated into
`--host`/`--url`. One difference: an `http://` `SENTRY_URL` now maps to
`--host` plus `--insecure-http` (the CLI's self-hosted behavior) instead
of being rejected. |
| `--insecure-http` | Same. It is still rejected with `--url` and for
sentry.io hosts. |
| `--mcp-url` / `MCP_URL`, `--sentry-dsn` / `SENTRY_DSN` /
`DEFAULT_SENTRY_DSN` | Same |
| `--skills` / `MCP_SKILLS`, `--all-skills`, `--disable-skills` /
`MCP_DISABLE_SKILLS` | Same, including dropping `seer` from the defaults
on self-hosted hosts |
| `--agent-provider` / `EMBEDDED_AGENT_PROVIDER`, `--openai-base-url`,
`--openai-model` / `OPENAI_MODEL`, `--anthropic-base-url`,
`--anthropic-model` / `ANTHROPIC_MODEL`, provider API keys | Same, with
the same provider mismatch and auto-detection warnings |
| `--organization-slug`, `--project-slug`, `--experimental` | Same |
| `--help` / `-h`, `--version` / `-v` | Stricli's generated help.
`sentry-mcp --help` lists `mcp`, `auth`, and `help`; `sentry-mcp mcp
--help` lists the flags and env vars. A lone `-v` still prints the
version; anywhere else `-v` is the CLI's `--verbose`. **Changed:** the
output is the bare version (`0.48.0`), like the CLI, instead of
`sentry-mcp 0.48.0`. |
| `--flag=` (for example `--sentry-dsn=` to turn off telemetry), values
with a line break | Same. Stricli does not read these joined forms, so
the runner splits them into `--flag value` first
(`splitInlineFlagValues`). |
| Unknown flags or positionals | Rejected by Stricli (exit 252) |
| `SENTRY_CLIENT_ID`, `SENTRY_MCP_AUTH_CACHE` | Ignored, because the
device flow is gone |
| `sentry-mcp auth login\|logout\|status` | The CLI's real `auth`
commands: `login` (with all its flags, including `--url`), `logout`,
`status`, `list`, `switch`, `refresh`, `token`, `whoami`. They use the
CLI session. Account pickers take the account as an argument, since the
Ink picker is not bundled. |

## Dependency direction guard

`sentry` → `@sentry/mcp-core` → `@sentry/toolkit-core`, and
`@sentry/mcp-cloudflare` → `@sentry/mcp-core`. No edge runs the other
way. `pnpm run lint` enforces this with two checks:

- An oxlint `no-restricted-imports` override bans `sentry`, `sentry/**`,
and relative CLI paths inside `packages/mcp-core` and
`packages/toolkit-core`.
- `scripts/check-workspace-deps.mjs` (`lint:deps`) reads every workspace
`package.json`. It fails on any cycle, on mcp-core reaching `sentry` or
mcp-cloudflare, and on toolkit-core reaching `sentry` or mcp-core.

The rule is documented in `AGENTS.md` and
`docs/architecture/overview.md#package-dependency-direction`.
Capabilities that only the CLI host has (SQLite cache, DSN detection
from local files, file uploads) are injected into mcp-core through
interfaces defined in mcp-core or toolkit-core. `sentry mcp` already
works this way for the access token and refresh callback in
`ServerContext`.

## Release and MCP registry

Every CLI release now publishes `@sentry/mcp-server` at the CLI's
version, so the next one goes from 0.42.0 to the CLI's next version
(0.48.0). The private `mcp-*` packages keep their own versions.

- `packages/cli/script/bump-version.ts` sets
`packages/mcp-server/package.json` to the CLI version on `--pre` and to
the next `-dev` version on `--post`. The bundle script fails if the two
versions differ.
- `cli-build.yml` bundles, tests, and packs the package and uploads it
as `mcp-server-npm-package`. The `packages/cli` Craft workspace
publishes it through a second npm target and attaches it to the
`cli@<version>` GitHub release.
- The separate `packages/mcp-server` Craft workspace, the Release
workflow's `mcp` option, and the packing steps in `merge-jobs.yml` are
removed.
- `mcp-registry.yml` now runs on stable `cli@` releases and strips the
prefix. A manual run with `-f version=X` uses `cli@X`, or the bare tags
used up to 0.42.0. `mcpName: io.github.getsentry/sentry-mcp` is
unchanged; `server.json`'s environment variables now declare
`SENTRY_AUTH_TOKEN` (required, secret) and `SENTRY_FORCE_ENV_TOKEN`
(default `1`) instead of `SENTRY_ACCESS_TOKEN`.

## `@sentry/mcp-server` bundle

`@sentry/mcp-server` stays the recommended, supported package. It ships
one dependency-free file built from the CLI's own runner, so the package
and `sentry mcp` run the same code, including self-hosted Sentry.

- **Entry**: `packages/cli/src/mcp-server.ts` runs `cli.ts`'s
`startCli()` with `mcp-server-app.ts`, a route map of the CLI's `mcp`
(the default command), `auth`, and `help`. The runner takes a `CliEntry`
that loads the application, so `sentry` (`sentry-cli.ts`) and
`sentry-mcp` differ only in their route map. `lib/application.ts` holds
the Stricli application setup that used to sit in `app.ts`.
`packages/mcp-server/script/bundle.ts` bundles the entry into
`packages/mcp-server/dist/sentry-mcp.cjs`.
- **Running `mcp`**: `isMcpInvocation()` finds `mcp` after leading
global flags, or no command at all where `mcp` is the default. The
runner then runs Stricli without the CLI's telemetry, recovery
middleware, and update check (the server owns stdout and has its own
telemetry). The command resolves when the client closes stdin, and only
then does the runner close the dispatcher and arm the force-exit
backstop.
- **Names**: `lib/app-identity.ts` replaces `login-command.ts` and
`usage.ts`. Each entry registers its name, and hints (`AuthError`,
refresh and device-flow failures, host-scope errors) and help examples
use it: `sentry auth login`, `sentry-mcp auth login`, or `npx -y
@sentry/mcp-server auth login` when npx launched the process
(`npm_command=exec` or an `_npx` path). MCP telemetry keeps
`app.server.distribution` at `sentry mcp` or `@sentry/mcp-server`.
- **Lazy loading** (from #1764): `--help`, `--version`, and `auth …` do
not load mcp-core's server, the provider factory, the MCP telemetry, or
the stdio transport; `commands/mcp.ts` imports `lib/mcp.ts` only when it
serves, and `lib/mcp.ts` imports `lib/mcp/server.ts` only after setup
checks pass. A bare `--version` (or lone `-v`) returns before the
application loads. `--help` and `auth` still load `@sentry/node`,
because the CLI's command builder and error reporting import it, same as
`sentry --help`.
- **MCP SDK v2** (`261e371f`, separate commit): stdio uses
`@modelcontextprotocol/server` and its stdio transport, like the
Cloudflare worker. `startStdio()` returns a `closed` promise; v2's
transport closes when stdin ends, which is what lets the command return.
`c0243b5a` then removes SDK v1 from mcp-core: `buildServer()` always
builds a v2 `McpServer`, the `sdkVersion` option is gone, and
`@modelcontextprotocol/sdk` is only a dev dependency of mcp-core (its
tests use the in-memory client). The Cloudflare worker keeps its own SDK
dependency for `StreamableHTTPClientTransport`. Both `sentry mcp` and
the bundle serve 14 tools over v2 and exit 0 within ~13–16 ms of stdin
closing.
- **Size work**: #1763's Zod English-locale plugin;
`__SENTRY_CLI_SYNTAX_HIGHLIGHT__=false` (#1765) and a new
`__SENTRY_CLI_INK_UI__=false` (`lib/select-prompt.ts`) compile out
highlight.js/parse5 and Ink; `zod/v3` resolves to a shared stub
(`packages/cli/script/lib/zod-v3-stub-plugin.ts`) in both this bundle
and the CLI's (`script/bundle.ts`, `script/build.ts`):
`@ai-sdk/provider-utils` and `@mastra/schema-compat` only reach it for
Zod 3 inputs, and nothing we bundle builds Zod 3 schemas. The stub keeps
`ZodType` a class so Mastra's `instanceof` check is false, and throws a
clear error if a Zod 3 path ever runs. A test bundles the real Mastra
client and provider-utils, runs a workflow create/start, and converts
Zod 4 and JSON schemas. With SDK v1 gone from mcp-core, the bundle no
longer needs a v1 stub and fails if any `@modelcontextprotocol/sdk`
module reaches it.
- **Left out**: the other CLI commands, agent skills, shell completions,
the sixel dashboard and help's kitty/sixel image banner (`pngjs`; help
falls back to block art), syntax highlighting, the Ink UI, `sentry
init`, and build/sourcemap/debug-file code. The SQLite WASM fallback is
also left out, so the engine moves from `>=22.13` to `>=22.15` (native
`node:sqlite`), and the bin exits early with a clear error on older
Node.js.
- **Guards**: the bundle script fails if any of those modules (or any
`@modelcontextprotocol/sdk` module) contributes code to the output, or
if the file grows past 3.75 MiB (`MAX_BUNDLE_BYTES`, ~300 KB over the
measured size; left as is, since removing SDK v1 saved only ~1 KB here,
where it was already stubbed).
- **Help banner** (`0e227f71`): `help.ts` imported `sixel.ts`, which
pulled `kitty-image.ts`, `sixel-image.ts`, the generated banner image,
`pngjs`, and `jpeg-js` into this bundle through `help`. `help.ts` now
takes a `GraphicsBanner` through `setGraphicsBanner()`; the full CLI's
`app.ts` registers the sixel one, and `mcp-server-app.ts` doesn't. The
bundle check now rejects those modules and `pngjs`. `jpeg-js` stays
allowed, because mcp-core's snapshot and attachment tools use it
(`internal/blob-utils.ts`). −45,219 B.
`test/script/help-graphics-bundle.test.ts` bundles `help.ts` and fails
against the old import.
- **Deleted cwd** (`ed4eddf6`, Warden): `serveMcp()` called
`process.cwd()` for the `.sentryclirc` trust gate added in this PR, so
the server failed to start from a deleted worktree. `mcpCommand` now
passes `this.cwd` (`resolveCwd()` with the `PWD` fallback).
- **`--version` guard** (`3392ea03`, #1762):
`test/script/cli-startup.test.ts` now also checks that `mcp-server.ts`
loads no `@sentry/*` module eagerly. The entry graph is 14 source
modules and lazily loads `mcp-server-app.ts` (197). Most of the
`--version` cost is parsing the 3.5 MB file (read + compile alone: 57 MB
/ 89 ms; bare Node 42 MB / 19 ms), so a separate version entry is a
possible follow-up, not done here.
- **Tests and CI**: `packages/mcp-server/test/bundle.test.mjs` runs the
built file: `initialize` plus `tools/list` over stdio with
`--sentry-dsn=` (with `SENTRY_AUTH_TOKEN`, and with the deprecated
`--access-token`, checking the warning is on stderr once and never
echoes the token), exit on stdin close, no runtime dependencies, bare
`--version` and `-v`, `--help` listing exactly `mcp`/`auth`/`help`, `mcp
--help` examples naming `sentry-mcp`, the login hint with and without
npx, `auth status`, and rejecting other CLI commands. `cli-build.yml`
bundles, tests, and packs the package next to the CLI's npm package.

The old `@sentry/mcp-server/transports/stdio` export is gone (now
`@sentry/mcp-core/transports/stdio`), and the standalone clean-consumer
workflow (`mcp-server-package.yml`) is removed; the bundle test replaces
it.

Release note: do not run `npm deprecate`. Users on Node.js 22.13 or
22.14 need to upgrade Node.js.

### Measurements (Node 22, this sandbox; startup rows are the median of
7 runs, idle RSS is 5 runs)

| | Before restack (`53fc92fe`) | `9977bb0c` |
| --- | --- | --- |
| Bundle | 3,663,084 B | 3,690,124 B (3,689,429 B at `a98cb249`;
3,689,513 B at `0a864625`; 3,688,917 B after #1770; 3,643,698 B with the
banner left out; 3,624,475 B after #1771) |
| `sentry-mcp --version` | 137 MB / ~335 ms (sibling scratch) | 86 MB /
133–138 ms |
| `sentry-mcp --help` | – | 153 MB / 352 ms |
| `sentry-mcp auth status` (logged out) | – | 158 MB / 444 ms |
| Idle server after `initialize` + `tools/list` | – | 152–153 MB RSS |

After #1771 (`88c11b37`, median of 9): `--version` 85 MB / 129 ms, `-v`
85 MB / 130 ms, `--help` 158 MB / 341 ms, `auth status` 164 MB / 505 ms,
the same as the pre-banner-fix bundle within noise (85 / 126, 85 / 125,
158 / 331, 165 / 490). `--help`, `auth status`, and idle RSS in the
table were measured at `7a2f0a2a`; the two later commits only add the
`-v` fast path and move a string list, and the bundle grew 89 bytes.

CLI `dist/index.cjs`: 7,426,174 B before `c0243b5a` → 7,203,971 B
without SDK v1 (−222 KB; ajv and most of zod-to-json-schema go with it)
→ 7,142,739 B with the `zod/v3` stub (−61 KB) → 7,143,333 B at
`a98cb249` → 7,021,265 B at `0a864625` (#1762 moves the `__complete`
path into a separate 331,140 B `dist/complete.cjs`) → 7,001,805 B plus a
259,731 B `complete.cjs` after #1771.

The bundle ends up about the same size as before even with ~550 KB of
savings (Zod locales −264 KB, SDK v1 and Zod 3 −~280 KB), because it now
carries the CLI runner, the full `auth` commands, and generated help.
Largest inputs: `@sentry/api` 482 KB, `@modelcontextprotocol/server` 291
KB (156 KB of it ajv), `@sentry/server-utils` 242 KB, `zod` 193 KB,
mcp-core's OTel semantics 187 KB, `@sentry/core` 162 KB.

## mcp-core CJS output

Nothing builds or consumes mcp-core CJS anymore. #1732, now merged into
this branch from main, switched `packages/mcp-core/tsdown.config.ts` to
`format: ["esm"]`. mcp-core is `"type": "module"`, and every `exports`
entry maps only `types` and `default` (`.js`) with no `require`
condition. The CLI's esbuild bundle (CJS output) resolves `default` and
converts the ESM itself, and `scripts/check-build-imports.ts` now checks
`dist/server.js`. This PR does not touch the build format.

## Verification

- `pnpm run tsc`, `pnpm run lint` (includes `lint:deps`), and CLI
`ci:policy` pass.
- `pnpm run test`: toolkit-core, mcp-core, mcp-cloudflare,
mcp-test-client, docs-mcp, local, and cli-docs pass. CLI: 12,807 pass;
the 6 failures are chmod-based tests (`completions` EACCES/EPERM,
read-only DB repair, upgrade lock permissions, unreadable DSN file,
file-changes I/O) that fail in this sandbox's filesystem and do not
touch this diff. The MCP, auth, app-identity, startup, help, and
completion suites pass.
- `pnpm --filter sentry run generate:docs` passes with
`packages/mcp-core/dist` removed, which is the `sentry-cli-docs` CI
condition.
- New or rewritten tests: `test/lib/mcp/args.test.ts` parses with the
command's real Stricli parameters (`test/lib/mcp/parse-flags.ts`);
`mcp-start.test.ts` and `mcp-auth.test.ts` drive `serveMcp()` and
`withConfiguredTarget()` through it; `mcp-command.test.ts` covers
`isMcpInvocation`, the `{mcp, auth, help}` app, no CLI telemetry for
`mcp`, teardown only after the server returns, setup failures, `--help`,
the help's skill list against `ALL_SKILLS`, and unknown flags;
`app-identity.test.ts` covers names and npx hints. `usage*.test.ts`,
`mcp/auth.test.ts`, and `mcp-run.test.ts` are deleted with their
modules.
- `pnpm --filter @sentry/mcp-server run bundle` (with
`SENTRY_AUTH_TOKEN` unset) builds 3,689,513 B at `0a864625` (242,647 B
under the 3.75 MiB limit); `test:bundle` passes 13/13.
- The CLI bundle builds; `node dist/bin.cjs mcp` answers `tools/list`
with 14 tools over SDK v2 and exits 0 when stdin closes. With
`--access-token`, stdout carries only JSON-RPC frames and stderr has the
one deprecation line, without the token. CLI `test/e2e/bundle.test.ts` +
`library.test.ts` pass 26/26.
- At `a98cb249`: `pnpm run lint` passes; `tsc` passes for cli, mcp-core,
mcp-cloudflare, mcp-server-evals, and mcp-test-client; mcp-core (1,890),
mcp-cloudflare (443), and mcp-test-client (82) tests pass. CLI `test/lib
test/commands test/types test/script`: 12,816 pass, 11 fail, and all 11
fail the same way without this change (6 chmod-based tests, plus 5
login-hint wording tests in `errors`, `oauth`, and
`auto-login-host-guard` that see `npx -y sentry` because the sandbox
runs vitest through npx).
- After merging `main` (`0a864625`; `5a72e8a3` has the same tree): `pnpm
run lint` and `pnpm run tsc` pass. CLI `test/script`, completion, db,
logger, telemetry, MCP, app-identity, and startup suites: 1,237 pass;
the 3 failures are the sandbox-only chmod/read-only tests above
(`completions` EACCES/EPERM, read-only DB repair). Both bundles build
with `SENTRY_AUTH_TOKEN` unset: `sentry-mcp.cjs` 3,689,513 B with no
highlight.js in the output, CLI `dist/index.cjs` 7,021,265 B plus
`dist/complete.cjs` 331,140 B. CLI `test/e2e/bundle.test.ts` +
`library.test.ts` pass 27/27, including #1762's check that `__complete`
never loads `index.cjs`.
- After merging #1770 and #1771 (`88c11b37`): `pnpm run lint` and `pnpm
run tsc` pass. Help/sixel, MCP command/start/auth, `lib/mcp/*`, startup,
completions, list-command, route-registry, schema, app-identity, and
bundle-plugin suites: 258 pass; the 2 failures are the sandbox chmod
tests in `completions` (EACCES/EPERM; `completions.ts` and its test
match `main`). With `SENTRY_AUTH_TOKEN` unset, `sentry-mcp.cjs` builds
at 3,624,475 B (307 KB under the limit) and `test:bundle` passes 13/13;
the CLI bundle builds (`index.cjs` 7,001,805 B, `index.mjs` 6,991,272 B,
`complete.cjs` 259,731 B). CLI e2e `bundle`, `completion`, `auth`, and
`telemetry-exit`: 27/28 in one parallel run, where the exit-timing test
took 2.36 s against its 2 s limit; it passed twice on its own.
- `--access-token` tests (`mcp-start.test.ts`): it serves the same token
and refresh result as `SENTRY_AUTH_TOKEN` + `SENTRY_FORCE_ENV_TOKEN=1`,
overrides a stored login before and after startup, overrides
`SENTRY_AUTH_TOKEN`/`SENTRY_TOKEN`, leaves `process.env` untouched,
prints the hint once to stderr and nothing to stdout, and keeps the
token out of the hint and of `MalformedAuthTokenError`/`HostScopeError`.
The existing self-hosted, org-token, and `.sentryclirc` cases still
pass.
- `craft config --workspace packages/cli` and the release checks above
are unchanged from the earlier verification.

Noticed, not changed: `serverInfo.version` in `initialize` reports
mcp-core's package version (`0.42.0`), as it did before this PR, not the
CLI/package version.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3A66fb44be021d2c0219302a43af471fb3 -->

--

[View Session in
Sentry](https://sentry.sentry.io/explore/conversations/agent%3A66fb44be021d2c0219302a43af471fb3/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <burak.kaya@sentry.io>
Co-authored-by: BYK <126780+BYK@users.noreply.github.com>

This branch was previously deployed

1 inactive deployment
Preview — fd2a64a7 Deployed Oct 31, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant