Skip to content

fix(oauth): stop reporting DCR refusals as MCPJam errors - #6859

Open
weimar-torres-herrera wants to merge 2 commits into
mainfrom
fix/plb-274-dcr-400-sentry-noise
Open

weimar-torres-herrera wants to merge 2 commits into
mainfrom
fix/plb-274-dcr-400-sentry-noise

Conversation

@weimar-torres-herrera

@weimar-torres-herrera weimar-torres-herrera commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Task, spec and plan

User story

As an on-call engineer watching Sentry and #mcpjam-alerts,
I want the OAuth debugger to stop filing an authorization server's refusal of Dynamic Client Registration as an MCPJam error,
so that INSPECTOR-CLIENT-2N2, -2JB, -2JM and their siblings (125 events from 17 users in the last 30 days, each refusal counted twice) stop alerting on server configuration we cannot fix.

Acceptance criteria

  • A registration endpoint that answers 400, 401, 403, 404 or 500 triggers no Sentry or PostHog error report, for either the bare message or its fallback-hint twin.
  • The debugger still shows Dynamic Client Registration failed (<status>)., with the hint when no pre-registered client exists, exactly as before.
  • A debug proxy failure during registration (Client registration failed: Backend debug proxy error: …) still reports.
  • A 2xx registration answer whose body is not a client (invalid_response) still reports.
  • Every other debugger step failure reports or stays silent exactly as before.

What this does

The debug proxy wraps whatever the server under test answered in an HTTP 200. Its own failures come back as non-2xx, which the client throws as Backend debug proxy error and executeDynamicClientRegistration turns into Client registration failed: …. So Dynamic Client Registration failed (N) only ever follows the authorization server's real answer: registration is closed, it wants an initial access token we were never given, or it rejects our metadata. The SDK now exports isDynamicClientRegistrationHttpError next to the code that writes that message, and the adapter adds it to isUnreportedStepFailure, which already silences a missing issuer, registration_endpoint or resource metadata and an unexpected probe status. It matches every status, not only 4xx, because the status is the server's answer; isUnexpectedProbeStatus follows the same rule. This supersedes #4484, which silenced only 4xx on the assumption that a 5xx could come from the proxy, and builds on its analysis of the doubled report. Failures that still report still arrive as that bare/hinted pair; #5473's finding key already files both in one Sentry issue, so collapsing them stays out of scope.

Validation:

  • RED first: the five new it.each cases (400/401/403/404/500) failed on main with reportCaught "called 2 times".
  • GREEN: debug-state-machine-step-reporting.test.ts and debug-state-machine-adapter.test.ts, 52 tests passing after merging main. The new cases run the real SDK executeDynamicClientRegistration and replay both updates the state machines write.
  • sdk/tests/oauth/dynamic-client-registration.test.ts, 75 passing (the message is byte-stable). server/routes/web/__tests__/oauth-proxy-projection.test.ts, 44 passing (pins the proxy contract above).
  • npm run typecheck -w @mcpjam/sdk and npm run typecheck:client -w @mcpjam/inspector both exit 0. npm run slop:check: no rule's count went up. Prettier is clean on the changed hunks. Re-checked on the merge with main (fix(oauth): name the server's certificate when TLS trust fails #6832 added isTlsTrustFailure to the same function; both predicates are kept).

Live check (local, 2026-10-26). I ran the OAuth debugger harness (/__e2e/oauth-debugger, ports 5373/6473) against a fake authorization server whose /register refuses, through the real local debug proxy with an access-link token. Every Sentry envelope was captured in the page and never sent, and a beforeSendEvent hook on the page's own Sentry client recorded each event's content. On main, one refusal produced 2 Sentry warnings for each of 400, 403 and 500. On this branch it produced 0, with the message unchanged on screen. The control, a registration endpoint on a closed port so the real proxy itself answered HTTP 500, still produced its 2 reports on both. The same four runs on the merge with main gave the same result.

Documentation

Nothing applicable: error reporting only, no user-facing behavior or docs change.

Architecture

flowchart LR
  Machine["OAuth state machine"] -->|"updateState: Dynamic Client Registration failed (N)"| Adapter["withStepFailureReporting"]
  Adapter -->|"isDynamicClientRegistrationHttpError: skip (new)"| UI["Debugger UI only"]
  Adapter -->|"Client registration failed: Backend debug proxy error"| Sentry["reportCaught: Sentry + PostHog"]
Loading

Screenshots and recordings

No UI change. The image shows the debugger before and after (same message on screen) and the Sentry report count from each local run.

plb274-before-after

Deleted

Nothing.

Reused

  • The isUnreportedStepFailure seam in debug-state-machine-adapter.ts, and the pattern of an SDK-exported predicate next to the code that writes the message (isUnexpectedProbeStatus, isAuthenticatedRequestFailure).
  • The existing step-reporting test harness (wrappedUpdateState) and the real executeDynamicClientRegistration.

Production signal

Sentry should show no message:"Dynamic Client Registration failed (*" events from the new release, and PostHog error tracking none on hosted. Older desktop builds keep sending them until users update, so 2JB, 2N2, 2JK, 2MF and 2JM get resolved in the first fixed release, not plainly; that stops old builds from reopening them and alerting Slack again. 2FH, 28B and 2C6 predate #5473's grouping and may hold other failures, so their latest events get checked first.

Automated decisions

None: no automated decision about users.

Checklist

  • I can explain every line of this diff without the agent that wrote it.
  • Under 800 changed lines of hand-written source, not counting tests (npm run slop:size: 27).
  • No new as any, empty catch, .catch(() => {}) or raw console on the server.
  • Comments say why the code is this way, not how it got here.

Refs PLB-274


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

An authorization server that answers the debugger's registration POST
with a non-2xx status (no open DCR, an initial access token required,
metadata it rejects) was filed in Sentry as an MCPJam error, twice per
refusal: the bare message and its fallback-hint twin. The debug proxy
returns whatever the server under test answered and its own failures
throw instead, so "Dynamic Client Registration failed (N)" is always the
server's answer. The SDK now exports isDynamicClientRegistrationHttpError
and the adapter adds it to isUnreportedStepFailure; the message stays on
screen, and proxy failures keep reporting.

Refs: PLB-274
…entry-noise

# Conflicts:
#	mcpjam-inspector/client/src/lib/oauth/debug-state-machine-adapter.ts
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-26T19:56:36.631521Z d9089de PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chelojimenez

Copy link
Copy Markdown
Contributor

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@coderabbitai

coderabbitai Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 091d66c5-2ce2-4193-ad73-e2dfeeb711bb
Commits

Reviewing files that changed from the base of the PR and between 584117c and d9089de.

Files selected for processing: 5
  • .changeset/oauth-dcr-http-error-unreported.md
  • mcpjam-inspector/client/src/lib/oauth/__tests__/debug-state-machine-step-reporting.test.ts
  • mcpjam-inspector/client/src/lib/oauth/debug-state-machine-adapter.ts
  • sdk/src/browser.ts
  • sdk/src/oauth/state-machines/shared/dynamic-client-registration.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Walkthrough

The SDK adds and exports isDynamicClientRegistrationHttpError, which identifies errors by a shared HTTP-error prefix. The inspector uses the predicate to exclude non-2xx Dynamic Client Registration responses from step-failure reporting. Tests cover several non-2xx statuses, proxy exceptions, and a successful response with a non-JSON body.

Priority: Low

Merge Risk: Minimal · up to d9089

The change appears mergeable after normal checks: registration refusals remain visible without being reported as MCPJam errors, while the described failure controls remain reportable.

- [ ] Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@blacksmith-sh

blacksmith-sh Bot commented Oct 26, 2026

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
e2e/visual/
mobile-webmcp-layout.spec.ts:28:5 › WebMCP layout light 1440 ───────────────────────
View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@github-actions

Copy link
Copy Markdown
Contributor

Internal preview

Preview URL will appear in Railway after the deploy finishes.
Deployed commit: 45f9d7e
PR head commit: d9089de
Backend target: staging fallback.
Access is employee-only in non-production environments.

@weimar-torres-herrera weimar-torres-herrera self-assigned this Oct 26, 2026

@olartgabo olartgabo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Process

  • PLB-274 is In Progress; set it to In review.
  • The "I can explain every line" box is unticked.

The rest of the record is complete: approved specs and plan, every criterion tested, and a changeset.

Code
Both debug proxy routes return the target's status inside a 200. A proxy non-2xx surfaces as "Client registration failed" and still reports. So the new match only silences answers from the authorization server. Two optional notes are inline. I'll approve once the process items are fixed.

* can keep it visible without filing it as an MCPJam defect.
*/
export function isDynamicClientRegistrationHttpError(error: string): boolean {
return error.startsWith(HTTP_ERROR_PREFIX);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Optional. The bare prefix also matches Dynamic Client Registration failed (undefined). and (0).. The client builds the status from data.status in the proxy's 200 body, so either form means the proxy contract broke, which is our bug, and those should still report. Matching /^Dynamic Client Registration failed \(\d{3}\)\./ silences only real HTTP answers and still covers the hinted form.

);

it("still reports the debug proxy failing on registration", async () => {
await replayRegistrationFailure(async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Optional. This case throws the proxy error by hand, so it would still pass if createDebugRequestExecutor stopped throwing on a proxy non-2xx, which is the contract this change relies on. wrappedUpdateState() already returns the real execute; passing it with authFetch resolving new Response("", { status: 500 }) would test the real path.

This branch had an error being deployed

1 failed deployment
preview-pr-6859 — d9089de6 Deployed Oct 26, 2026 by weimar-torres-herrera via upsert-preview #35665
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants