-
-
Notifications
You must be signed in to change notification settings - Fork 272
fix(oauth): stop filing DCR refusals as MCPJam bugs #4484
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| --- | ||
| "@mcpjam/sdk": patch | ||
| "@mcpjam/inspector": patch | ||
| --- | ||
|
|
||
| A registration endpoint answering 4xx is the server under test declining, not an MCPJam defect | ||
|
|
||
| The OAuth debugger reported every Dynamic Client Registration failure to Sentry. A 4xx from the registration endpoint is the authorization server declining to mint a client — DCR left unimplemented behind an advertised `registration_endpoint`, an allowlist, an initial access token the client was never given, or metadata the server will not accept. The debugger exists to show that; filing it as a bug against MCPJam is the same noise the client-side origin policy already removes everywhere else. | ||
|
|
||
| `isClientRegistrationRefusal` classifies those 4xx messages. 5xx and transport failures are deliberately not refusals: those can be the debug proxy failing, which is ours. 4xx is read as the server's policy knowingly rather than because it always is — RFC 7591 also spends `invalid_client_metadata` on a body the client built wrong, but in practice that code comes back for a valid body the server declines, and the request builder is covered by its own unit tests rather than by this signal. | ||
|
|
||
| One rejection also produced two reports. The machines write the bare message, then the same message with the fallback hint appended once no pre-registered client turns up, and the debugger's dedup guard keys on the string. `restatesWithFallbackHint` pairs them by matching the hint exactly rather than by prefix — a prefix rule would have swallowed `Token request failed: 400 Bad Request` followed by the same line carrying `invalid_grant`, which is a more informative failure and not a duplicate. | ||
|
|
||
| Both messages still render in the debugger. Only the Sentry issue goes away. | ||
|
|
||
| `REGISTRATION_FAILURE_PREFIX` is now exported and read by `trace.ts`, which previously repeated the literal, so a rewording cannot leave the recovered-fallback check matching stale text. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -212,6 +212,47 @@ function backfillHistory<THistory extends BackfillableHistoryEntry>( | |
| const FALLBACK_HINT = | ||
| "Configure a pre-registered client or enable DCR on the authorization server."; | ||
|
|
||
| export const REGISTRATION_FAILURE_PREFIX = "Dynamic Client Registration failed"; | ||
|
|
||
| /** | ||
| * Did the authorization server REFUSE to register, rather than fail to answer? | ||
| * 5xx and transport failures can be ours, so they are deliberately not refusals. | ||
| * | ||
| * 4xx is read as the server's policy knowingly, not because it always is: RFC | ||
| * 7591 also spends `invalid_client_metadata` on a body the client built wrong. | ||
| * In practice that code comes back for a valid body the server declines — a | ||
| * loopback redirect, an auth method it will not issue — and the request builder | ||
| * is covered by its own unit tests rather than by this signal. | ||
| * | ||
| * Parses the messages built below — the machines expose only the string. | ||
| */ | ||
| export function isClientRegistrationRefusal(error: string): boolean { | ||
| const prefix = `${REGISTRATION_FAILURE_PREFIX} (`; | ||
| if (!error.startsWith(prefix)) return false; | ||
| const status = Number.parseInt(error.slice(prefix.length), 10); | ||
| return status >= 400 && status < 500; | ||
| } | ||
|
|
||
| /** | ||
| * Is `error` the same failure as `previous`, restated with the fallback hint? | ||
| * | ||
| * One rejected registration writes both: the bare message, then the message | ||
| * plus this hint once no pre-registered client turns up. Matching the hint | ||
| * exactly keeps the pair together without swallowing an unrelated failure that | ||
| * merely extends the last one (`"…: 400 Bad Request"` → `"…: 400 Bad Request: | ||
| * invalid_grant"` is two reports, not one). | ||
| */ | ||
| export function restatesWithFallbackHint( | ||
| error: string, | ||
| previous: string | ||
| ): boolean { | ||
| return ( | ||
| error !== previous && | ||
| error.startsWith(previous) && | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Match the fallback variant exactly. Line 251 accepts a distinct message that starts with Proposed fix- error !== previous &&
- error.startsWith(previous) &&
- error.endsWith(FALLBACK_HINT)
+ error === `${previous} ${FALLBACK_HINT}`🤖 Prompt for AI Agents |
||
| error.endsWith(FALLBACK_HINT) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Executes an RFC 7591 registration POST via the caller's executor, back-fills | ||
| * a cloned last history entry with the redacted response, and classifies the | ||
|
|
@@ -255,13 +296,13 @@ export async function executeDynamicClientRegistration< | |
| const httpHistory = backfillHistory(input.httpHistory, redacted); | ||
|
|
||
| if (!response.ok) { | ||
| const registrationError = `Dynamic Client Registration failed (${response.status}).`; | ||
| const registrationError = `${REGISTRATION_FAILURE_PREFIX} (${response.status}).`; | ||
| return { | ||
| status: "http_error", | ||
| response: redacted, | ||
| httpHistory, | ||
| error: registrationError, | ||
| fallbackNote: `Dynamic Client Registration failed (${response.status}); using pre-registered client credentials.`, | ||
| fallbackNote: `${REGISTRATION_FAILURE_PREFIX} (${response.status}); using pre-registered client credentials.`, | ||
| errorWithFallbackHint: `${registrationError} ${FALLBACK_HINT}`, | ||
| }; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Export
REGISTRATION_FAILURE_PREFIXfrom the public entrypoint.This block exports the two new helpers but not
REGISTRATION_FAILURE_PREFIX. Consumers of@mcpjam/sdkcannot import the shared prefix described in the PR contract. Add the constant to this export list.Proposed fix
export { buildDynamicClientRegistrationRequest, executeDynamicClientRegistration, + REGISTRATION_FAILURE_PREFIX, isClientRegistrationRefusal, restatesWithFallbackHint, } from "./oauth/state-machines/shared/dynamic-client-registration.js";📝 Committable suggestion
🤖 Prompt for AI Agents