Skip to content

ref(toolkit-core): Start the shared API client with errors and response reading - #1732

Merged
BYK merged 10 commits into
mainfrom
feat/shared-api-client
Oct 29, 2026
Merged

BYK merged 10 commits into
mainfrom
feat/shared-api-client

Conversation

@sentry-junior

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

Copy link
Copy Markdown
Contributor

Starts the shared Sentry API client in toolkit-core/src/api-client/ (step 1 of #1669, task group 8). The MCP and the CLI both use it for errors, API paths, and response reading.

It is the first of a stack of PRs by layer (see "Next" below).

What changes

  • Portability guard (8.1). Lint bans node:* imports and Node globals such as Buffer and process in toolkit-core. A second typecheck (tsconfig.browser.json) checks it against the DOM library with no Node types, and pnpm run tsc runs both. The node:zlib cache codec is the one exception. Its types move to response-cache/body.ts, so the policy and store modules stay portable.

  • Standard Schema validation. validateResponse takes any Standard Schema validator: Zod from the MCP, valibot from the CLI. toolkit-core depends only on @standard-schema/spec types. Lint bans zod and valibot imports in its source; they are dev dependencies for the tests only.

  • Shared errors and apiPath. ApiError, its status subclasses, createApiError, UserInputError, ConfigurationError, and apiPath move from mcp-core to @sentry/toolkit-core/api-client/*. mcp-core/src/errors.ts re-exports the two general errors, so its importers don't change.

  • Shared response reading. readErrorResponse reads a failed response once: its text, parsed JSON, string detail, whether it is an HTML page, and the rate-limit wait. readJsonResponse is the MCP's Content-Type-checked JSON parse.

    • SentryApiService uses both.
    • The CLI's throwRawApiError uses readErrorResponse.
    • Each product still maps the result to its own errors (MCP ApiError subclasses, CLI exit-coded ApiError).
  • mcp-core builds ESM only. Once the errors moved, the CJS build hit a rolldown "canonical name not found" panic. Nothing loads that CJS output:

    • @sentry/mcp-core is private and "type": "module".
    • All 29 of its exports point at .js, with no require condition.
    • Every consumer is in the workspace: mcp-server bundles it as a devDependency through those .js exports, mcp-cloudflare bundles it with Vite, and mcp-test-client and mcp-server-evals are private ESM packages.
    • No source requires it or reads its .cjs files.

    @sentry/mcp-server's own CJS and ESM output is unchanged. I tried upgrading tsdown first, to 0.15.12, 0.18.4, 0.20.3, 0.22.14, and 0.23.4. Every version broke the mcp-server-mocks build: 0.15.12 failed declaration output (members is not iterable), and 0.18.4 and later hit a rolldown JSON parse "recursion limit exceeded" on src/fixtures/trace.json. 0.18.4 and 0.20.3 also failed the mcp-core and mcp-server builds. Bundling toolkit-core into the dist (noExternal) would have duplicated the error classes across dists and broken instanceof between packages.

  • Docs. The toolkit-core README now has an "API client" section. api-patterns.md, error-handling.md, and the logging skill point at the new error module.

Behavior changes

  • MCP, unreadable error body. If the body of a failed response cannot be read, the caller now gets an ApiError for the status instead of the raw stream error.
  • MCP, "no string detail" warning. The validationErrorMessage on the "Failed to parse validated API error response" warning is now a fixed string instead of Zod's message. ApiErrorSchema is deleted; nothing else used it.

Messages, details, retryAfterMs, and the CLI's error detail are otherwise unchanged.

Next

This is the bottom of a stack of PRs by layer for the rest of #1669 group 8. Each one is based on the PR below it:

  1. Move pagination, cache policy, retry, and transport under api-client/.
  2. Shared endpoint wrappers on top of @sentry/api, with SentryApiService switched onto them.
  3. The CLI host seam, with the CLI's lib/api/ wrappers switched onto the shared client.
  4. Merge the wrappers both products share.

This PR does not touch packages/mcp-server, packages/cli/src/lib/mcp.ts, or the evals stdio entry; the @sentry/mcp-server retirement PR owns those.

Commits

Each codemod run is a generated commit whose message gives the command, and each hand fix is a separate commit:

  1. 13053adab Portability guard (hand)
  2. f13df0d24 Standard Schema validation (hand)
  3. bea21d648 mcp-core ESM only (hand)
  4. 0d083bdf3 Move errors and apiPath (generated, move-shared-errors.mjs)
  5. 4ddc54813 Move the client.ts request-path test back next to client.ts (hand)
  6. 143da6e39 Shared response reading (hand)
  7. b776eb55b MCP JSON parsing on the shared reader (generated, use-shared-json-response.mjs)
  8. 44976334d MCP error responses on the shared reader (hand)
  9. 7027a482d CLI raw error responses on the shared reader (hand)
  10. bf0b248ac Docs (hand)

Codemods

The scripts are not in the tree. To re-run one, check out the parent of its generated commit, run the script from a scratch directory with ts-morph@28.0.0 installed, then run oxfmt on the changed files. The output should match the commit.

move-shared-errors.mjs (commit 0d083bd)
node move-shared-errors.mjs <repo-root>
oxfmt <added and modified files>
// Codemod: move the MCP's API error classes and API path helper into
// toolkit-core's shared api-client, and rewrite every importer.
//
// Usage: node move-shared-errors.mjs <toolkit-repo-root>
// (run from a directory where `ts-morph@28.0.0` is installed)
import { realpathSync } from "node:fs";
import path from "node:path";
import { Project } from "ts-morph";

const root = path.resolve(process.argv[2] ?? ".");
const mcpSrc = path.join(root, "packages/mcp-core/src");
const coreSrc = path.join(root, "packages/toolkit-core/src");

// Classes the shared client throws, moved out of mcp-core's general errors.
const MOVED_CLASSES = ["UserInputError", "ConfigurationError"];
// mcp-core files that move into toolkit-core/src/api-client/.
const MOVED_FILES = [
  "api-client/errors.ts",
  "api-client/api-path.ts",
  "api-client/api-path.test.ts",
];

const project = new Project({
  tsConfigFilePath: path.join(root, "packages/mcp-core/tsconfig.json"),
});
project.addSourceFilesAtPaths(path.join(coreSrc, "**/*.ts"));

// 1. Move the classes into the API errors module; mcp-core/src/errors.ts
//    re-exports them so its existing importers keep working.
const generalErrors = project.getSourceFileOrThrow(
  path.join(mcpSrc, "errors.ts"),
);
const apiErrors = project.getSourceFileOrThrow(
  path.join(mcpSrc, "api-client/errors.ts"),
);
const classTexts = MOVED_CLASSES.map((name) => {
  const declaration = generalErrors.getClassOrThrow(name);
  const text = declaration.getFullText().trim();
  declaration.remove();
  return text;
});
apiErrors.insertText(0, `${classTexts.join("\n\n")}\n\n`);
generalErrors.insertExportDeclaration(0, {
  namedExports: [...MOVED_CLASSES].sort(),
  moduleSpecifier: "./api-client/errors",
});

// 2. Move the files. ts-morph rewrites relative imports in and to them.
const movedPaths = new Set();
for (const file of MOVED_FILES) {
  const sourceFile = project.getSourceFileOrThrow(path.join(mcpSrc, file));
  sourceFile.move(path.join(coreSrc, "api-client", path.basename(file)));
  movedPaths.add(sourceFile.getFilePath());
}

// Moved files take the moved classes from the API errors module directly,
// not through mcp-core's re-export.
for (const movedPath of movedPaths) {
  const sourceFile = project.getSourceFileOrThrow(movedPath);
  for (const declaration of sourceFile.getImportDeclarations()) {
    if (declaration.getModuleSpecifierSourceFile() !== generalErrors) continue;
    const names = declaration.getNamedImports().map((n) => n.getName());
    if (names.every((name) => MOVED_CLASSES.includes(name))) {
      declaration.setModuleSpecifier(apiErrors);
    }
  }
}

// 3. Normalize specifiers that point into toolkit-core: relative with `.js`
//    inside toolkit-core, `@sentry/toolkit-core/<path>` everywhere else.
function realPath(file) {
  try {
    return realpathSync(file);
  } catch {
    return file;
  }
}

for (const sourceFile of project.getSourceFiles()) {
  const importer = sourceFile.getFilePath();
  const declarations = [
    ...sourceFile.getImportDeclarations(),
    ...sourceFile.getExportDeclarations(),
  ];
  for (const declaration of declarations) {
    const target = declaration.getModuleSpecifierSourceFile();
    if (!target) continue;
    const targetPath = realPath(target.getFilePath());
    if (!targetPath.startsWith(`${coreSrc}/`)) continue;
    // Leave specifiers the codemod did not affect as they are.
    if (!movedPaths.has(importer) && !movedPaths.has(targetPath)) continue;
    const withoutExt = targetPath.replace(/\.ts$/, "");
    let specifier;
    if (importer.startsWith(`${coreSrc}/`)) {
      specifier = path.relative(path.dirname(importer), withoutExt);
      if (!specifier.startsWith(".")) specifier = `./${specifier}`;
      specifier = `${specifier}.js`;
    } else {
      specifier = `@sentry/toolkit-core/${path
        .relative(coreSrc, withoutExt)
        .replace(/\/index$/, "")}`;
    }
    if (declaration.getModuleSpecifierValue() !== specifier) {
      declaration.setModuleSpecifier(specifier);
    }
  }
}

await project.save();
use-shared-json-response.mjs (commit b776eb5)
node use-shared-json-response.mjs <repo-root>
oxfmt packages/mcp-core/src/api-client/client.ts
// Codemod: replace SentryApiService's private parseJsonResponse with the
// shared readJsonResponse from toolkit-core, and delete the private copy.
//
// Usage: node use-shared-json-response.mjs <toolkit-repo-root>
// (run from a directory where `ts-morph@28.0.0` is installed)
import path from "node:path";
import { Project, SyntaxKind } from "ts-morph";

const root = path.resolve(process.argv[2] ?? ".");
const clientPath = path.join(root, "packages/mcp-core/src/api-client/client.ts");

const project = new Project({ skipAddingFilesFromTsConfig: true });
const client = project.addSourceFileAtPath(clientPath);
const service = client.getClassOrThrow("SentryApiService");

// 1. `this.parseJsonResponse(x)` -> `readJsonResponse(x)`.
const calls = client
  .getDescendantsOfKind(SyntaxKind.CallExpression)
  .filter((call) => call.getExpression().getText() === "this.parseJsonResponse")
  .reverse();
for (const call of calls) {
  call.getExpression().replaceWithText("readJsonResponse");
}

// 2. Delete the private method it replaces.
service.getMethodOrThrow("parseJsonResponse").remove();

// 3. Import the shared helper.
client.addImportDeclaration({
  namedImports: ["readJsonResponse"],
  moduleSpecifier: "@sentry/toolkit-core/api-client/response",
});

await project.save();
console.log(`rewrote ${calls.length} call sites`);

Verification

At bf0b248ac, rebased on main after #1722 merged:

  • pnpm run tsc passes. It includes toolkit-core's browser-target typecheck and the CLI typecheck.
  • pnpm run lint passes (oxlint, oxfmt, ast-grep rules), and so does pnpm run docs:check.
  • Tests: toolkit-core 306, mcp-core 1881 (6 skipped), and mcp-cloudflare 443 pass.
  • Before the rebase, the full pnpm run test also passed mcp-server (87), mcp-test-client (80), docs-mcp (49), local (322), and cli-docs.
  • CLI (test/lib, test/commands): 11718 pass and 6 fail in 5 files (completions, dsn/code-scanner, init/tools/file-changes, telemetry, upgrade).
    • All 6 test permission errors (EACCES/EPERM/read-only). They fail only because the sandbox runs with CAP_DAC_OVERRIDE, so a chmod 000 file stays readable.
    • This PR touches none of that code; CI is the check for them.
  • Both generated commits reproduce exactly when their codemod is re-run on the rebased parent.
  • The diff has no codemod scripts, ast-grep or ts-morph config, or ts-morph dependency.

via BYK.

--

View Junior Session in Sentry

Lint bans node:* imports and Node globals in toolkit-core, and a second
typecheck runs it against the DOM library with no Node types. The cache
codec keeps node:zlib; its types move to body.ts so the policy and stores
stay portable.
validateResponse takes any Standard Schema validator, so the MCP keeps Zod
and the CLI keeps valibot. toolkit-core depends only on the interface's
types, and lint now bans zod and valibot imports in shared modules.
Nothing loads mcp-core's CJS output. The package is private, "type":
"module", and all 29 exports point at .js with no "require" condition,
so require() would resolve to the ESM files anyway. Its consumers are
workspace-only: mcp-server bundles it as a devDependency through those
.js exports, mcp-cloudflare bundles it with Vite, and mcp-test-client
and mcp-server-evals are private and ESM. No source in the repo requires
it or reads its .cjs files.

Dropping the CJS build also avoids the rolldown "canonical name not
found" panic hit by the CJS pass once the API errors move to
toolkit-core. mcp-server's own CJS+ESM output is unchanged.
Generated by the ts-morph codemod move-shared-errors.mjs (script in the PR
description), run from a scratch directory with ts-morph@28.0.0 installed:

  node move-shared-errors.mjs <repo-root>
  oxfmt <added and modified files>

It moves mcp-core's api-client/errors.ts, api-path.ts, and api-path.test.ts
into toolkit-core/src/api-client/, moves UserInputError and
ConfigurationError into the shared errors module (mcp-core/src/errors.ts
re-exports them), and rewrites every importer.
The codemod moved api-path.test.ts into toolkit-core, but its second block
reads mcp-core's client.ts source to check every request path uses apiPath.
That block moves back to mcp-core until client.ts moves too.
readErrorResponse reads a failed response once: its text, parsed JSON,
the server's string `detail`, whether it is an HTML page, and the
rate-limit wait. Each host maps that to its own errors. readJsonResponse
is the MCP's Content-Type-checked JSON parse, moved here for both.
Generated by the ts-morph codemod use-shared-json-response.mjs (script in
the PR description), run from a scratch directory with ts-morph@28.0.0
installed:

  node use-shared-json-response.mjs <repo-root>
  oxfmt packages/mcp-core/src/api-client/client.ts

It rewrites the 18 `this.parseJsonResponse(...)` calls in
SentryApiService to toolkit-core's readJsonResponse, deletes the private
method, and imports the shared one.
SentryApiService.request reads a failed response with toolkit-core's
readErrorResponse and maps it to ApiError subclasses as before: the
same messages, details, retryAfterMs, and warnings. The Zod
ApiErrorSchema it used for the `detail` check is gone.

Two small changes: if the error body cannot be read, the caller now gets
an ApiError for the status instead of the stream error, and the
"no string detail" warning's validationErrorMessage is a fixed string
instead of Zod's message.
throwRawApiError takes the body, parsed JSON, and `detail` from
toolkit-core's readErrorResponse instead of parsing them itself. The
detail it puts on ApiError is unchanged: the server's string detail,
the JSON body for statuses other than 401 and 403, the text for
non-JSON bodies, and the status text when the body cannot be read.
The toolkit-core README covers the api-client modules and their
portability guard, api-patterns.md says where ApiError and error reading
live now, and the error-handling doc and logging skill point at the
moved errors module.
@vercel

vercel Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
sentry-local Ready Ready Preview Oct 29, 2026 7:35pm UTC

Request Review

@github-actions github-actions Bot added the risk: medium PR risk score: medium label Oct 29, 2026
sentry-junior Bot added a commit that referenced this pull request Oct 29, 2026
Group 8 now ships as a stack split by layer: the foundation (#1732),
the transport-layer move, endpoint wrappers with the MCP switch, the
CLI host seam and switch, then merged shared wrappers. 6.1 is done in
#1722.

Co-Authored-By: BYK <burak.kaya@sentry.io>
@BYK
BYK enabled auto-merge (squash) October 29, 2026 19:43
@BYK
BYK merged commit 9409280 into main Oct 29, 2026
43 checks passed
@BYK
BYK deleted the feat/shared-api-client branch October 29, 2026 19:51
sentry-junior Bot added a commit that referenced this pull request Oct 29, 2026
… 8.3 and 8.4

Co-Authored-By: BYK <burak.kaya@sentry.io>
BYK added a commit that referenced this pull request Oct 29, 2026
… api-client (#1736)

Moves pagination, the response-cache policy, retry, and transport under
`toolkit-core/src/api-client/`, the layout they will have inside
`@sentry/api` (#1669, task 8.3). Every importer is rewritten. Nothing
else changes.

It's the second PR in the group 8 stack. #1732, the foundation, has
merged, so this one targets `main`.

## What changes

- **Moves.** Only the relative imports inside the moved files change:
  - `pagination.ts` → `api-client/pagination.ts`
  - `api-retry.ts` → `api-client/retry.ts`
  - `api-transport.ts` → `api-client/transport.ts`
-
`response-cache/{body,index,invalidation,keys,memory-store,policy,store,tiers}.ts`
→ `api-client/response-cache/`
  - Their tests move with them.
- **Stays put:** `response-cache/codec.ts`. It's the `node:zlib` codec,
host storage that won't move into `@sentry/api`. It's still excluded
from the browser typecheck, and the `./response-cache/codec` export
doesn't change.
- **Exports:** `./pagination`, `./api-retry`, `./api-transport`, and
`./response-cache` are replaced by the existing `./api-client/*`
wildcard plus one `./api-client/response-cache` entry for the directory
index. `@sentry/toolkit-core` is `private`, and every consumer is in
this workspace and rewritten here.
- **Importers.** The codemod rewrites 11 import specifiers across the
CLI (including `script/generate-api-schema.ts`),
`mcp-core/src/api-client/client.ts`, and toolkit-core.
- **Docs:** `api-patterns.md`, the toolkit-core README, and code
comments name the new subpaths.

## Behavior changes

None.

This PR does not touch `packages/mcp-server`,
`packages/cli/src/lib/mcp.ts`, or the evals stdio entry. The
`@sentry/mcp-server` retirement PR owns those.

## Commits

1. `85fae9151` Move the modules and rewrite importers (**generated**,
`move-transport-layer.mjs`)
2. `85df504be` Point comments and docs at the new paths (hand)

## Verification

- The generated commit reproduces exactly: check out `origin/main`
(`9409280c1`), run the command below, then oxfmt on the changed files.
The tree matches `85fae9151`.
- `pnpm run tsc` (including toolkit-core's browser typecheck), `pnpm run
lint`, and `node scripts/check-doc-links.mjs` pass.
- Tests: toolkit-core 306 passed; mcp-core 1881 passed, 6 skipped;
mcp-cloudflare 443 passed; mcp-server 87 passed; CLI `test/lib
test/commands test/script` 11895 passed, 46 skipped, **6 failed**.
- The 6 CLI failures are sandbox-only, and they fail the same way
without this change. The sandbox runs with `CAP_DAC_OVERRIDE`, so these
permission tests can't make a file unreadable: `completions` (EACCES,
EPERM), `dsn/code-scanner` (unreadable file), `init/tools/file-changes`
(I/O failure), `telemetry` (read-only DB), and `upgrade` (`acquireLock`
permission error). CI runs them normally.

## Codemod

The script isn't in the tree. To re-run it, check out the parent of the
generated commit and run it from a scratch directory with
`ts-morph@28.0.0` installed. Then run oxfmt on the added, modified, and
renamed package files.

<details>
<summary><code>move-transport-layer.mjs</code> (commit
85fae91)</summary>

```sh
node move-transport-layer.mjs <repo-root>
git add -A -- packages
oxfmt $(git diff --cached --name-only --diff-filter=AMR)
```

```js
// Codemod: move toolkit-core's pagination, response-cache policy, api-retry,
// and api-transport modules under src/api-client/, and rewrite every
// importer and the package's exports. The node:zlib cache codec stays at
// src/response-cache/codec.ts as a host-side module.
//
// Usage: node move-transport-layer.mjs <toolkit-repo-root>
// (run from a directory where `ts-morph@28.0.0` is installed)
import { readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { Node, Project } from "ts-morph";

const root = path.resolve(process.argv[2] ?? ".");
const coreDir = path.join(root, "packages/toolkit-core");
const coreSrc = path.join(coreDir, "src");

// Old path -> new path, relative to toolkit-core/src.
const MOVES = new Map([
  ["pagination.ts", "api-client/pagination.ts"],
  ["pagination.test.ts", "api-client/pagination.test.ts"],
  ["api-retry.ts", "api-client/retry.ts"],
  ["api-retry.test.ts", "api-client/retry.test.ts"],
  ["api-transport.ts", "api-client/transport.ts"],
  ["api-transport.test.ts", "api-client/transport.test.ts"],
]);
const CACHE_FILES = [
  "body.ts",
  "index.ts",
  "invalidation.ts",
  "invalidation.test.ts",
  "keys.ts",
  "keys.test.ts",
  "memory-store.ts",
  "memory-store.test.ts",
  "policy.ts",
  "policy.test.ts",
  "policy.property.test.ts",
  "store.ts",
  "tiers.ts",
  "tiers.test.ts",
];
for (const file of CACHE_FILES) {
  MOVES.set(`response-cache/${file}`, `api-client/response-cache/${file}`);
}

// Package subpath -> new subpath. `response-cache/codec` does not move.
const SPECIFIERS = new Map([
  ["@sentry/toolkit-core/api-retry", "@sentry/toolkit-core/api-client/retry"],
  [
    "@sentry/toolkit-core/api-transport",
    "@sentry/toolkit-core/api-client/transport",
  ],
  [
    "@sentry/toolkit-core/pagination",
    "@sentry/toolkit-core/api-client/pagination",
  ],
  [
    "@sentry/toolkit-core/response-cache",
    "@sentry/toolkit-core/api-client/response-cache",
  ],
]);

// 1. Move the files inside toolkit-core. ts-morph rewrites relative imports
//    in and to them; then give every rewritten specifier the `.js` suffix
//    the package uses.
const core = new Project({
  tsConfigFilePath: path.join(coreDir, "tsconfig.json"),
});
const moved = new Set();
for (const [from, to] of MOVES) {
  const sourceFile = core.getSourceFileOrThrow(path.join(coreSrc, from));
  sourceFile.move(path.join(coreSrc, to));
  moved.add(sourceFile.getFilePath());
}
for (const sourceFile of core.getSourceFiles()) {
  const importer = sourceFile.getFilePath();
  for (const declaration of [
    ...sourceFile.getImportDeclarations(),
    ...sourceFile.getExportDeclarations(),
  ]) {
    const target = declaration.getModuleSpecifierSourceFile();
    if (!target || !declaration.getModuleSpecifierValue()?.startsWith(".")) {
      continue;
    }
    const targetPath = target.getFilePath();
    if (!moved.has(importer) && !moved.has(targetPath)) continue;
    let specifier = path.relative(
      path.dirname(importer),
      targetPath.replace(/\.ts$/, ""),
    );
    if (!specifier.startsWith(".")) specifier = `./${specifier}`;
    declaration.setModuleSpecifier(`${specifier}.js`);
  }
}
await core.save();

// 2. Rewrite package specifiers everywhere else, including dynamic imports
//    and vi.mock() calls: any string literal equal to an old subpath.
const others = new Project({ skipAddingFilesFromTsConfig: true });
others.addSourceFilesAtPaths([
  path.join(root, "{packages,apps}/*/{src,test,script,scripts}/**/*.{ts,tsx,mts}"),
  `!${path.join(root, "**/node_modules/**")}`,
  `!${path.join(root, "**/dist/**")}`,
  `!${path.join(coreDir, "**")}`,
]);
let rewritten = 0;
for (const sourceFile of others.getSourceFiles()) {
  let changed = false;
  sourceFile.forEachDescendant((node) => {
    if (!Node.isStringLiteral(node)) return;
    const next = SPECIFIERS.get(node.getLiteralValue());
    if (next) {
      node.setLiteralValue(next);
      changed = true;
      rewritten += 1;
    }
  });
  if (changed) await sourceFile.save();
}

// 3. Point the package exports at the new paths. The `./api-client/*`
//    wildcard covers pagination, retry, and transport; the cache needs its
//    index.
const manifestPath = path.join(coreDir, "package.json");
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
const exports = {};
for (const [key, value] of Object.entries(manifest.exports)) {
  if (["./api-retry", "./api-transport", "./pagination", "./response-cache"].includes(key)) {
    continue;
  }
  if (key === "./api-client/*") {
    exports["./api-client/response-cache"] = {
      types: "./src/api-client/response-cache/index.ts",
      default: "./src/api-client/response-cache/index.ts",
    };
  }
  exports[key] = value;
}
manifest.exports = exports;
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);

console.log(`moved ${moved.size} files, rewrote ${rewritten} specifiers`);
```

</details>

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

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

--

[View Junior Session in
Sentry](https://sentry.sentry.io/explore/conversations/agent%3Aa6eddc0161cff61914454da9f74fcdf1/?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>
BYK added a commit that referenced this pull request Oct 29, 2026
…mar (#1730)

Part of #1564. Dashboard commands parsed their positionals by hand
inside `func`, and issue commands used their own grammar (#1729). This
moves both onto the shared ID grammar (`grammar/id-arguments.pegjs`,
#1714), parsed before `func` runs. Event view's issue checks (#1733,
#1738) use the new issue kind too.

**Shared grammar: kinds with names.** `IdKind.names` adds one idea to
`id-arguments.pegjs`: a kind may name an item without its ID, and
anything that is not, and may not be, an ID is a name. Kinds without
names keep their grammar paths exactly as before. Kinds with names keep
each line whole (no splitting at spaces), so `Top 10` stays one title
and never becomes title `Top` + ID `10`.
- `ID_KINDS.dashboard`: a numeric ID or a title, which may hold spaces
and slashes (`Latency/p95`). `<org>/[<project>/]<digits>` reads as a
path only when slugs come before the digits, so `Errors.v2/123` and
`/123` are titles. After a target, the title takes the rest of the
arguments (`my-org/ My Dashboard`). A bare first word names an org
(`orgScoped`).
- `ID_KINDS.issue`: an identifier (numeric ID, short ID, selector, issue
URL) is one whole token (`wholeIds`). `src/lib/issue-identifier.ts`
takes it apart with the #1729 grammar, which is unchanged. Text with
spaces is a name, meaning a search.
- `dashboard-arguments.pegjs` is gone. `dashboard list` keeps its thin
`parseDashboardListArgs`, because a bare word there is a title filter
(see #1711).

**Bare first word: org or title?** For `dashboard view My Dashboard`,
the parser makes no decision. It returns the structural reading (org
`My`, title `Dashboard`) and the whole input as one title
(`nameWithTarget`, surfaced as `orgOrTitle`). `resolveDashboardInput`
decides after auth:
- First it checks whether `My` is an org: the cached org list and region
cache, then `GET /organizations/My/` only on a cache miss.
- If the org exists, the title lookup in that org is final, even when it
misses.
- On a 404 or 403 it warns ``No org 'My'; treating "My Dashboard" as the
dashboard title. Use my-org/ to name an org.`` and looks up "My
Dashboard" in the auto-detected org.
- If that misses too, one error names both readings (no org `My` to find
`Dashboard` in, and no `My Dashboard` in `<detected-org>`) and hints at
`<org>/ Dashboard` and at quoting.
- A 5xx or network error from the org check propagates, with no
fallback.
- It never falls back for `my-org/ …` (explicit org), a numeric ref, or
a URL.

**Issue search text.** `issue view "login timeout"` resolves through
`GET /organizations/{org}/issues/?query=…` in the detected org. One hit
is the issue. Zero or several hits raise a `ResolutionError` that lists
up to 5 candidates as runnable commands and points at `sentry issue list
<org>/ --query '…'`. I checked the endpoint against getsentry/sentry
(`organization_group_index.py`, `group_index/index.py`): `query` takes
free text, an exact event ID or short ID gets a fast lookup, and an
explicit query replaces the default `is:unresolved`.

**Search hints.** Text where a trace, span, log, or replay ID belongs
(`trace view login`) now fails with ``Not a trace ID: "login". To
search, try `sentry trace list --query 'login'`.``. The hint comes from
`validate()`, so commands that recover malformed IDs (`trace view`,
`span view`, `log view`) recover first and use the hint only as their
final error. Event IDs get no hint, because `event list` lists one
issue's events.

**event view.** `ID_KINDS.issue.distinctForm` (uppercase short ID or
numeric) replaces `looksLikeIssueShortId` + `isAllDigits` and keeps
#1738's order: `<org>/<short-id>`, then `<short-id>/<event-id>` (even an
all-digit event ID), then `<org>/<numeric-id>`. I added a regression
test for the all-digit event ID and one for that order.

**Behavior changes:**
- `dashboard view my-org My Dashboard` (unquoted title) looks up "My
Dashboard". Before, it used `My` and dropped `Dashboard`. The same goes
for `my-org/ My Dashboard` and `https://acme.sentry.io/ My Dashboard`.
- `dashboard view My Dashboard`: when there is no org `My` (404/403), it
falls back to the title "My Dashboard" in the detected org, with the
warning above. If neither reading finds a dashboard, one error names
both. Dashboard commands with one dashboard ref get this (`view`,
`restore`, `revisions`, `widget edit/delete`, and `widget add` after it
splits off its widget title).
- A numeric ref first is the dashboard, as with every ID kind:
`dashboard view 42 X`, `my-org/42 X`. Before, the first of two arguments
was always the target. After a target, a numeric ID keeps the rest as
extra (`my-org 123 extra` → ID 123, ignored `extra`) instead of joining
them into a title. Arguments after the ID go through the shared rules:
extra, reversed (`42 my-org/`), or the shared "reference different
projects" error when they name another target.
- A lone non-Sentry URL is no longer taken as a dashboard title.
- `issue view "login timeout"` (text with spaces) searches instead of
failing as a malformed identifier. Control characters and URLs are still
rejected.
- Trace/span/log/replay IDs that are plain text get a search hint
instead of "Invalid trace ID".
- Parse errors (no dashboard, an org URL without a dashboard, a
dashboard URL passed to `list`, an invalid target slug) fail before the
auth guard, as in the earlier commits on this branch. They print `Failed
to parse "…" for <placeholder>: …` and exit 252.
- `widget add/edit/delete`: an argument error comes before flag errors
like "Specify --index or --title". `widget add`'s positional completes
cached dashboards. `parseDashboardTarget` had no callers left and is
removed.

**Lint ratchet (#1739):** the dashboard entries are off both
allow-lists, since these commands now parse before `func`. The issue
entries (`issue/utils.ts`, `issue/merge.ts`, which need an array issue
positional) stay.

**Syncing:** this branch merges `main` (no rebase). 54ebfa2 and 7f52f19
merge #1729, #1732–#1734, #1736, #1738 and #1739.

Verification:
- `tsc`, `pnpm run lint` (oxlint, format, ast-grep) and
`scripts/cli-lint-rules.test.mjs` (70/70) pass.
- vitest on `test/lib`, `test/grammar` and the dashboard, issue, event,
trace, span, log, replay and feedback command tests: 10,158 passed. The
6 failures are the sandbox-only permission tests (completions
EACCES/EPERM, code-scanner, file-changes, telemetry readonly, upgrade
lock), which also fail on `main`. The full CLI suite shows the same 6,
plus e2e/eval files that need a build or credentials.
- New tests:
- The bare-org fallback: the org from cache and from the API (no
fallback), org missing then fallback hit with the warning, a 403, org
missing then fallback miss with the combined error, a 5xx (propagates,
no fallback), `my-org/ My Dashboard`, and a numeric ref.
  - Issue search with 0, 1 and several hits.
  - Search hints, including `validate: false` deferral.
  - #1738's event view order.
- Differential runs against this PR's previous parsers:
- Issue arguments: 8,575 cases. 1,388 former errors are now searches
(text with spaces), with 0 other differences.
- Dashboard view: 32,552 cases over 0–3 arguments. Every difference is
in one of the categories above: a numeric ID first (2,600), arguments
after a dashboard URL that name another target (688), a numeric ID after
a target with more words (475), and a lone non-Sentry URL (1).

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

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

--

[View Junior Session in
Sentry](https://sentry.sentry.io/explore/conversations/agent%3A5a5e7e4543124325a44d731c08dd6089/?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 30, 2026
main moved response parsing into toolkit-core's readJsonResponse
(#1732, #1737), which still throws a plain Error for an empty or
non-JSON body. validateEvents is the only caller that lets a 400 reach
that parser, so classify its non-JSON 400s there with createApiError.
Successful non-JSON responses stay reportable.
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 successfully deployed

1 active and 1 inactive deployments
Preview — bf0b248a Deployed Oct 29, 2026 by vercel[bot]
Actions — bf0b248a Deployed Oct 29, 2026 by sentry-junior[bot] via eval #1405
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