Skip to content

ref(mcp): Send SentryApiService requests through a shared request core - #1737

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

BYK merged 2 commits into
mainfrom
feat/shared-api-client-core

Conversation

@sentry-junior

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

Copy link
Copy Markdown
Contributor

Adds the shared request core in toolkit-core/src/api-client/core.ts and switches SentryApiService onto it. This is the first slice of #1669 task 8.4. #1736 has merged, so this PR now targets main.

8.4 as written covers the request core and every domain's endpoint wrappers. SentryApiService is about 6,000 lines with about 100 endpoint methods, so I'm splitting 8.4 into the core first, then domain PRs on top. This PR is the core.

What changes

  • createApiClientCore (toolkit-core, browser-portable). The host injects its transport, per-request headers, and logger. request(origin, path, init, { allowStatuses }):

    • sends to <origin>/api/0<path> with Content-Type: application/json plus the host's headers (they replace any the caller passes, as before);
    • turns network failures into Unable to connect to <url> - …, as a ConfigurationError for ENOTFOUND, EAI_AGAIN, ECONNREFUSED, ETIMEDOUT, and connect timeouts;
    • rethrows the caller's abort unchanged;
    • maps an error response to the status's ApiError through readErrorResponse, with the same three warnings as before.

    requestJson also parses the body with readJsonResponse.

  • SentryApiService. request and requestJSON keep their signatures and delegate to the core, so no endpoint method changes. SentryApiService supplies User-Agent, Authorization (still a ConfigurationError for a malformed token, before anything is sent), the X-Sentry-MCP-* attribution headers, the existing shared transport, and logWarn under the api.client scope. Its copies of the network error table and the error-response mapping are deleted.

  • Still in mcp-core: sdkClient, which now goes through the core via request, and sdkListPage.

    • Moving sdkClient means toolkit-core depends on @sentry/api. Today that resolves a second @sentry/api copy, because toolkit-core pins TypeScript through the ^5.8.3 catalog while mcp-core resolves 5.9.3. The domain-wrapper PRs will fix that skew first.
    • sdkListPage stays because its rules differ from the shared paginate: an exact per_page, and an error on extra rows or a repeated cursor. Merging them would change behavior, so that waits for the merge step.
  • Docs: the toolkit-core README's API client section and api-patterns.md describe the core.

Behavior changes

  • Warning host field. The three error-response warnings (HTML page, unparseable JSON, no string detail) now log the host the request actually went to. Before, they logged the service's default host even when a call overrode it, e.g. a regional host. Messages, error classes, detail, responseBody, and retryAfterMs don't change.

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

Both commits are hand-written. No codemod.

  1. 5710b8e8d Add createApiClientCore with unit tests
  2. fc6aa0875 Switch SentryApiService onto it, plus docs

Verification

  • pnpm run tsc (including toolkit-core's browser typecheck), pnpm run lint, and node scripts/check-doc-links.mjs pass.
  • toolkit-core: 319 passed, including 13 new core.test.ts cases covering headers, header errors, detail mapping, allowed statuses, rate-limit wait, HTML and unreadable bodies, network errors, abort, and requestJson.
  • mcp-core: 1881 passed, 6 skipped. The existing client.test.ts MSW suites cover the delegated path unchanged.
  • mcp-server: 87 passed. mcp-cloudflare: 443 passed.
  • The CLI is untouched in this PR; its turn is 8.5.

--

View Junior Session in Sentry

@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 9:30pm UTC

Request Review

@github-actions github-actions Bot added the risk: medium PR risk score: medium label Oct 29, 2026
Base automatically changed from feat/shared-api-client-layout to main October 29, 2026 21:23
createApiClientCore sends a request through the host's transport with
the host's headers, turns network failures into readable errors
(ConfigurationError for an unreachable host), maps error responses to
the status's ApiError through readErrorResponse, and parses JSON bodies.
The transport, headers, and logger are injected, so it stays
browser-portable. It is the MCP's request path, moved as is; the next
commit switches SentryApiService onto it.
request and requestJSON keep their signatures and delegate to
createApiClientCore, so no endpoint method changes. SentryApiService
supplies its headers (User-Agent, Authorization, MCP attribution), the
shared transport, and logWarn under the api.client scope. Its copies of
the network error messages and the error-response mapping are deleted.

The one difference: the error-response warnings now log the host the
request went to, rather than the service's default host, when a call
overrides the host.
@sentry-junior
sentry-junior Bot force-pushed the feat/shared-api-client-core branch from 31cc0f4 to fc6aa08 Compare October 29, 2026 21:27
@BYK
BYK merged commit 6a267d9 into main Oct 29, 2026
32 of 36 checks passed
@BYK
BYK deleted the feat/shared-api-client-core branch October 29, 2026 21:44
sentry-junior Bot added a commit that referenced this pull request Oct 29, 2026
main gained #1737 as a squash commit whose tree matches this branch's
copy of the request core (fc6aa08), so every conflict resolves to this
branch's side and the merged tree is unchanged.
BYK pushed a commit that referenced this pull request Oct 29, 2026
…1741)

Task 8.4, part 2 of the shared API client stack
([#1669](#1669)). It used to be
stacked on #1737, which has merged, so it now targets `main`.

This adds the first shared endpoint wrappers to `toolkit-core`'s
`api-client/`, and the MCP's `SentryApiService` now calls them. The code
moves as is, so it can go to `@sentry/api` later without changes.

**Changes**
- `endpoints/organizations` adds `getAuthenticatedUser`,
`listOrganizations` and `getOrganization`. Each one takes an `ApiTarget`
(the request core plus an origin) and a Standard Schema from the host,
and returns that schema's output. `toolkit-core` doesn't depend on Zod
or valibot when the code runs.
- The request core gains `sdkClient`, which sends one GET to an expected
path and rejects any other path or method, and `listPage`, which walks a
list endpoint up to a limit and validates each page. These are the MCP's
`sdkClient` and `sdkListPage`, moved as is. The only change is that
validation now uses Standard Schema.
- `SentryApiService` still decides which host to call: the control host
for the current user and for the organization list on public SaaS. Its
own `sdkClient` and `sdkListPage` copies are deleted, and the teams and
projects lists use the shared ones.
- `toolkit-core` now depends on `@sentry/api`. To keep a single
TypeScript copy, the pnpm catalog's TypeScript moves from `^5.8.3` to
`^5.9.3`, and `toolkit-core` and the CLI use `catalog:` instead of
pinning their own version. Every workspace package now resolves
TypeScript 5.9.3, and 5.8.3 drops out of the lockfile. The cache policy
changes one type to match TypeScript 5.9's DOM `BodyInit`. Nothing
changes at runtime.

**One change in behaviour:** if a response fails its schema in these
calls, the error is now the shared `ApiResponseValidationError` instead
of a `ZodError`. Both still reach the tool error handler as unexpected
errors.

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3A0ab4fc2bd5a2e23f2c25efcf -->

--

[View Junior Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A0ab4fc2bd5a2e23f2c25efcf/?project=4510944073809921)

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

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
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.
sentry-junior Bot added a commit that referenced this pull request Nov 3, 2026
…1820

Co-Authored-By: Claude <noreply@anthropic.com>
BYK added a commit that referenced this pull request Nov 3, 2026
…1836)

When a tool call leaves out `organizationSlug` and the login can reach
exactly one organization, the call now uses that organization. When the
login can reach several, a tool that requires the slug fails with a
retry the model can run as is, plus the other organizations. This is the
MCP version of the CLI recovery in #1634, and it finishes the part #1678
held back. The server never chooses between organizations.

## Why now
Over the last 7 days (`sentry/mcp-server` spans, extrapolated counts):
- `organizationSlug: Required` failures: about 4.3K, from search_issues,
find_projects, search_errors, search_logs, and calls through
execute_sentry_tool. `get_sentry_resource` adds about 1.9K more with
"`organizationSlug` is required when not using a URL".
- The script that sent about 89% of the earlier ~31K/week has stopped.
The rest is spread out: no user accounts for more than 2 of the 40
sampled failures.

## How it works
- **No schema or handler edits.** `getFilteredInputSchema` makes a
required `organizationSlug` optional at registration time, so the MCP
SDK no longer rejects the call before our code runs. Then
`prepareToolCallParams` runs `applyDefaultOrganization`. Both top-level
calls and `execute_sentry_tool` go through it, before
`withRoutedCredential`, so stdio account routing (#1802) gets the
defaulted slug.
- It runs only when the call has no slug after constraint injection and
no Sentry URL parameter (it reuses `getTargetOrganizationSlug`).
Org-scoped sessions never reach it.
- **One organization:** the call uses its slug. If the tool takes
`regionUrl` and the caller didn't set one, the call also gets the
organization's `regionUrl`. On self-hosted that value is empty, so
`regionUrl` stays unset.
- **Several organizations:** for a required slug, the error looks like
``Retry example: `search_issues(organizationSlug='org-a', period='1h')`
`` and lists the other organizations. Catalog tools get the
`execute_sentry_tool(name=…, arguments=…)` form, and values equal to a
schema default are left out. For an optional slug (URL-or-ID tools such
as `get_sentry_resource`), the call runs as before.
- **Listing fails** (for example, a token without `org:read`): the error
falls back to the find_organizations hint from #1678.
- **Multiple logins:** organizations from every account are merged.

## Fit with #1669
- The organization list comes from `SentryApiService.listOrganizations`,
which already uses the shared `endpoints/organizations` wrapper and
transport (#1722, #1737). No new endpoint code.
- The resolve step lives in mcp-core's constraint and param-prep layer.
The design keeps that layer in mcp-core ("an MCP tool fills them from
tool parameters and session constraints"), so 11.3 can keep it as is.
- The MCP hosts don't wire a `CacheStore` yet (7.1 is still open).
Instead, the list is memoized per context and credential for 5 minutes,
which matches the `stable` tier. When 7.1 lands, the `/organizations/`
request gets cached per identity on its own, and this memo can be
dropped. It doesn't conflict with 8.4–8.7 or 11.x.
- This doesn't complete a task in the plan. The closest is 11.3's
resolve step.

## Token cost
`measure-tokens` is unchanged at 9,014. That script reads the raw tool
schemas, and those didn't change. In the registered schemas,
`organizationSlug` is no longer in the `required` list on 8 direct
tools, which comes to 5,014 → 4,990 input-schema tokens.

## Open questions
- A `null` slug still gets the find_organizations hint. Accepting `null`
would add `anyOf` to every schema.
- When an optional-slug tool like `get_sentry_resource` is called by a
user with several organizations, it keeps its own error. It doesn't list
the organizations yet.

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

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

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3A0222050e9c46ef2289687231ff960e16)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A0222050e9c46ef2289687231ff960e16/?project=4510944073809921)

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

---------

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

This branch was successfully deployed

1 active deployment
Preview — fc6aa087 Deployed Oct 29, 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