| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
de6960ec5a |
fix: make the proxy deadline per-request, not unconditional (#2319 round 13)
The server-side timer added in round 9 applied to every /api/fetch request. But that route carries MCP traffic as well as OAuth work, so a Streamable HTTP tool call that legitimately withholds its response headers past the budget was aborted there — reintroducing on the backend exactly the regression exemptMcpEndpoint prevents on the client, and reporting it as an OAuth timeout besides. The caller now decides. `withOAuthRequestTimeout` stamps its budget on the RequestInit it hands down, `createRemoteFetch` reads it back and puts it in the /api/fetch envelope, and the route applies a timer only when one is present — so an exempt MCP request, which never gets an init of its own, is bounded by nothing. The carrier is a WeakMap keyed on the init rather than a header: serializeRequest copies request headers verbatim into the payload and the route re-sends them to the authorization server, so a marker header would leak to a third party. It needs no cast onto RequestInit and is collected with the object. The `fetchTimeoutMs` option from round 9 is gone — the deadline now lives in exactly one place, the client wrapper, which is also what makes the 504 marker honest: only a request the OAuth wrapper bounded can produce one. Ten tests: the stamping and its rounding, nothing stamped on an exempt call or an init that never went through the wrapper, the envelope carrying the budget (and not a header), its omission on both exempt and direct calls, and at the route the 504 marker, no deadline without one, and a non-numeric deadline ignored. The no-deadline case is verified by mutation: an unconditional timer makes it fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFnM6RqB4nXqNysgUYmjmq Signed-off-by: cliffhall <cliff@futurescale.com> |
||
|
|
f491c7fc9a |
feat: bound SDK-internal OAuth on the transport chain too (#2319 round 11)
The transport chain carried the SDK's own OAuth work — the discovery it runs from inside the transport on the 401/refresh path — and that leg was left to the SDK's incidental per-request timeout, reporting a bare `Request timed out` naming no endpoint. On a direct (non-proxied) path the /api/fetch backstop cannot reach it either. `withOAuthRequestTimeout` takes an optional `isExempt` predicate; an exempt request passes through completely untouched — no deadline, no signal, and no body buffering, which is what a stream needs. `exemptMcpEndpoint` exempts the MCP endpoint by origin + pathname, and FAILS OPEN: no server URL, or either side unparseable, means exempt. The errors are not symmetric. Bounding what should not be bounded severs a long-running tool call withholding its headers, or an SSE body; failing to bound leaves the SDK's timeout as the backstop it already was. Two test corrections, both real: - The metadata-preservation case asserted url/redirected/type against a synthetic Response, whose values are the rebuild's own defaults — so it stayed green with the defineProperty loop deleted. It now seeds non-default values first; verified by mutation that removing the loop makes it fail. - Two signal assertions compared against the controller rather than `request.signal`. The Fetch standard gives a Request a dependent signal, so those encoded non-standard identity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFnM6RqB4nXqNysgUYmjmq Signed-off-by: cliffhall <cliff@futurescale.com> |
||
|
|
baefc96806 |
fix: carry the proxy deadline's identity, correct the rounding note (#2319 round 9)
A deadline enforced by the backend could not arrive as an OAuthRequestTimeoutError: /api/fetch serialized it as a 500 and createRemoteFetch turned that into a plain Error, so every instanceof check downstream was false — including the one that lets a stalled probe escape withRfc8414OidcCompat. That matters most where no client-side wrapper exists to have fired first: the discovery the SDK runs from inside the transport. The route now answers 504 with a typed marker (OAUTH_TIMEOUT_WIRE_CODE plus url and timeoutMs) and createRemoteFetch reconstructs the error from it, guarded by a shape check so an upstream's own JSON cannot forge one. The route's deadline becomes a `fetchTimeoutMs` option defaulting to the shared constant, so the backstop can be proven to fire in milliseconds instead of thirty seconds of wall clock — and because a very slow authorization server is a legitimate reason to want a longer one. The rounding rationale was wrong: setTimeout accepts a fractional delay and truncates it (verified on Node 26); ERR_OUT_OF_RANGE is AbortSignal.timeout's behaviour, which this wrapper never calls. The rounding stays, for the reason that is actually true — the budget is interpolated into the message and exposed as timeoutMs, and a performance.now() budget would otherwise report 1000.4000000953674ms. Also drops a sentence in the discarded-stream test that contradicted the qualification above it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFnM6RqB4nXqNysgUYmjmq Signed-off-by: cliffhall <cliff@futurescale.com> |
||
|
|
8dd7bc2920 |
feat: bound every OAuth-path request with a deadline
Protected-resource-metadata discovery, authorization-server metadata discovery, dynamic client registration, the token exchange and the refresh all went out with no AbortSignal at all. Token revocation was the single exception, and its withDeadline race is the pattern this generalizes. Adds core/auth/requestTimeout.ts: withOAuthRequestTimeout wraps a fetch so every call through it is bounded, composing any caller signal with AbortSignal.any and racing a timer so the deadline holds even where the signal is dropped in flight (the browser's remote fetch re-issues the call to /api/fetch and forwards no signal). The rejection is an OAuthRequestTimeoutError naming the URL, so a stall says which endpoint stalled rather than riding the SDK handshake timeout and reporting "Request timed out". Applied inside InspectorClient.buildEffectiveAuthFetch rather than to this.fetchFn, because the transport is handed this.fetchFn directly and a deadline on a Streamable HTTP / SSE response would sever the stream it is supposed to hold open. Also applied to the CLI's storedAuthFetch, which calls SDK discovery directly and so has nothing else bounding it. Closes #2319 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFnM6RqB4nXqNysgUYmjmq Signed-off-by: cliffhall <cliff@futurescale.com> |
||
|
|
84a65f28d8 |
fix(auth): floor the shared revocation budget instead of racing zero
The shared teardown deadline was decided on `remainingMs > 0` against `Date.now()`. Both halves of that are noise-sensitive: the deadline is enforced by a `setTimeout`, and a wall clock at millisecond resolution can still read a hair short of it when the loop comes round, so the next grant inherits a fractional budget and spends it on a request that cannot possibly complete. The same run then issues one request or two depending on scheduling — which is what makes the "shares one deadline across grants" test intermittent on CI (#2252). Measure the deadline with `performance.now()` (monotonic, so an NTP step or a suspend cannot expire or extend the budget, and sub-millisecond, so none is lost to rounding), and skip any grant reaching the loop with less than MIN_REVOCATION_REQUEST_BUDGET_MS left. The skipped grant is already reported as `failed`, so nothing is silently dropped — the needless call to the authorization server is. The CLI's outer per-plan budget in `sendPlans` shares both the shape and the defect, so it gets the same treatment. One consequence worth naming: a monotonic clock hands `revokeToken` a fractional budget, and Node's `AbortSignal.timeout` throws `ERR_OUT_OF_RANGE` on a non-integer delay — before the fetch, so the request would never be sent and the revocation would report that as its failure. `revokeToken` now rounds its own budget to whole milliseconds, which also covers any other caller passing a fractional one. Tests: the epsilon skip is pinned by a grant that lands inside the floor but above zero (removing the floor fails it, verified); the fractional budget by a `revokeToken` call at 19.996ms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017wEXtbs8UEHUMxDEAxbs99 Signed-off-by: cliffhall <cliff@futurescale.com> |
||
|
|
2eeab05d00 |
Merge remote-tracking branch 'origin/v2/main' into v2/fix/2172-oidc-discovery-compat
Signed-off-by: cliffhall <cliff@futurescale.com> # Conflicts: # README.md # core/mcp/inspectorClient.ts # test-servers/src/test-server-oauth.ts |
||
|
|
463227c947 |
fix(auth): make snapshot-and-clear one atomic storage step (#2144)
Copilot review round 18. Three of its findings were the same check-then-act: `planOAuthRevocation` is asynchronous, so the gap between its reads and the following `storage.clear()` is still a window — shorter than the network one closed in round 13, but the same shape. An OAuth completion landing at any of those awaits saved a grant the clear then destroyed. `OAuthStorage.takeRevocationSnapshot(serverUrl)` reads what revocation needs and clears the server in ONE synchronous pass over the in-memory state, with the persist after the mutation. `clearAndPlanRevocation` replaces `planOAuthRevocation`, and there is no separate `clear` call left in any caller — the clear cannot be forgotten or reordered. The snapshot returns values unparsed, deliberately: schema validation is pure and belongs after the mutation, since running it inside would reintroduce the `await` this exists to remove. Two consequences worth naming. A failure of the take-and-clear now REJECTS rather than returning an outcome, because the state was not cleared and the caller's contract is that it was; both clients already have a rejection path. And cross-process atomicity is explicitly not claimed — nothing in this store takes a lock, every mutation is a read-modify-write over a loaded snapshot, and `clear` alone always had that property. Also from round 18: - The CLI's budget check ran before considering `plan.outcome`, so a key that needed no network was reported as budget-exhausted — warning that a grant may be live when that key held none. - A disconnect failure after a successful clear was reported as "could not clear OAuth state" in both clients. It now goes to the disconnect channel, and the success is still reported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh Signed-off-by: cliffhall <cliff@futurescale.com> |
||
|
|
69fcdd364c |
fix(auth): clear before waiting on the network, not after (#2144)
Copilot review round 13. Three of its four findings were one root cause, and it is a design flaw in the premise this PR started from. Revoking *before* the clear meant the clear waited up to five seconds on the network — and anything that wrote OAuth state in that window was then deleted by a clear reasoning about the grant it replaced. A same-server reauthorization in the TUI (whose Disconnect/Connect accelerators stay live), a stored-only web clear for a server that becomes active mid-flight, and another CLI/TUI process writing to the shared file-backed store all hit it. `revokeStoredOAuthTokens` is therefore split into `planOAuthRevocation` and `executeOAuthRevocation`, and every caller runs plan → `storage.clear()` → execute. The snapshot still precedes the clear, because the store is where the token, the credentials and the endpoint live; but the clear no longer waits on anything. There is deliberately no single-call wrapper left, so no caller can reintroduce the racy order by accident. Fourth finding, same file: `handleConnect` now retires `clearOAuthAttemptRef` alongside `disconnectAttemptRef`. The server-name check does not cover disconnect/reconnect to the SAME server — the name still matches on the other side of it — so a clear still in flight could tear down the session the connect had just established. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh Signed-off-by: cliffhall <cliff@futurescale.com> |
||
|
|
781aa69d19 |
fix(auth): revoke every issuer-bound grant, not just the active one (#2144)
Copilot review round 3. `clear(serverUrl)` deletes EVERY `byIssuer` slot, but revocation read only the context-free (active-issuer) token. A server that had authorized against issuers A and B therefore revoked B, then destroyed the local record of A while A's grant stayed live at its authorization server — the same leak this feature exists to close, one level down. - `OAuthStorage.listIssuers(serverUrl)` is the new seam (one implementation: every backend extends `OAuthStorageBase`). `collectGrants` walks it plus the ctx-less read, deduping by token, so every grant the clear will delete is covered. - Metadata is cached once per server, not per issuer, so a grant bound to an issuer the cached document does not describe cannot be revoked: sending its token to that endpoint would hand a credential to a server that never minted it. That grant is reported as failed — dropped unrevoked — rather than silently discarded or misdirected. - `aggregateOutcomes` gives a failure precedence over another grant's success, so one live grant cannot hide behind another's 200. Exported for its own test, since `computeOutcome` returns early on the empty case. Also from that round: - The fixture's /oauth/revoke now verifies token ownership (RFC 7009 §2.1). Access tokens record their owning `client_id`; a token belonging to another client is left alone and still answered 200 (§2.2 — the response must not tell one client whether another's token exists). New e2e case proves a second registered client cannot revoke the first's grant. - The TUI rendered the revocation-failure note in cyan, the informational tone. It is a partial success — the local clear really happened — so it is not an `error` status, but the grant may still be live and cyan understated that. `AuthTab` takes an `oauthMessageTone`; the message says so explicitly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh Signed-off-by: cliffhall <cliff@futurescale.com> |
||
|
|
65fd2b9e35 |
feat(auth): revoke OAuth tokens at the authorization server on clear (#2144)
Clearing the Inspector's OAuth state deleted the local copy of the tokens and stopped there. The access token — and the refresh token, which is long-lived by design — stayed valid at the authorization server until they expired on their own, so a day of connect/disconnect iteration left it holding grants for sessions that ended hours ago. RFC 7009 §1 describes exactly that case, and nothing in the repo constructed a revocation request, so a server author implementing `revocation_endpoint` had no way to exercise it with the tool that exists to exercise this. core/auth/revocation.ts builds and sends the request; the three clear paths call it, so the behavior is written once — web's "Clear OAuth state and disconnect" (both the active and stored-only paths), the TUI's Auth tab, and the CLI's --relogin. It runs BEFORE the local clear, because the token, the client credentials and the discovered `revocation_endpoint` all live in the store the clear is about to empty. The request names the refresh token when there is one: §2.1 asks the AS to also invalidate the access tokens issued under the same grant, so one request covers both halves. Best-effort by construction. Every path returns a TokenRevocationOutcome rather than throwing: no advertised endpoint means nothing is sent and that authorization server behaves exactly as before, and a network error, a non-2xx, a 5s timeout or an unreadable store is reported and nothing more. Forgetting the tokens is what the user asked for. `lost_authorization_state` recovery deliberately skips it — it clears a half-finished flow to retry it, and an authorization that never completed has no grant to revoke. Client authentication resolves the preregistered slot first, mirroring BaseOAuthClientProvider.clientInformation; reading only the dynamic slot would send no authentication for a server configured with `oauth.clientId`. The Basic credential is byte-identical to the SDK's applyBasicAuth so this request and the token request cannot present the same secret differently. Opt out per server with `oauth.revokeOnClear` (default on, only `false` written to disk) or per CLI run with --no-revoke. Read at clear time rather than connect time, so toggling needs no reconnect. Turning it off is a testing affordance: a client that walks away still holding live tokens is a case a server author may want to reproduce. test-servers now advertises `revocation_endpoint` and serves POST /oauth/revoke, faithful on §2.2 (an unknown token is a success) and §2.1 (revoking a refresh token invalidates the access tokens under the same grant), with oauth-revocation-http.json / oauth-no-revocation-http.json as showcase configs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh Signed-off-by: cliffhall <cliff@futurescale.com> |
||
|
|
7ca753bc4c |
fix(auth): accept RFC 8414 metadata served at the OIDC well-known path
The SDK's `discoverAuthorizationServerMetadata` picks its validation schema from the well-known filename that resolved rather than from the document that came back, so anything found at `/.well-known/openid-configuration` is validated as an OpenID provider document — requiring `jwks_uri`, `subject_types_supported` and `id_token_signing_alg_values_supported`, three fields RFC 8414 does not define. RFC 8414 §5 permits that filename for general OAuth metadata, so a conforming plain OAuth 2.0 authorization server is rejected; and because the parse throws instead of continuing the candidate loop, discovery aborts and the connection fails outright. Filed upstream as modelcontextprotocol/typescript-sdk#2733. `core/auth/oidcDiscoveryCompat.ts` works around it without fabricating a field: on a failed RFC 8414 candidate it probes the OIDC candidates the SDK would try next and, when one returns a document that satisfies `OAuthMetadataSchema` but fails the OIDC schema, serves that body as the RFC 8414 response so the SDK picks the schema that describes it. A genuine OpenID provider document is left to the SDK's own OIDC leg. Issuer validation runs unchanged, since the substituted document is the one the server published. It wraps `effectiveAuthFetch` — above the fetch tracker, the opposite of `withOAuthEndpointOverrides` — so the Network tab still records the real 404 and the real probe rather than the substitution. The candidate derivation mirrors the SDK's `buildDiscoveryUrls`, which a fetch wrapper cannot call, and a test pins it against that export so the two cannot drift. Adds `oauth.asMetadataPath` to the composable test server (mirroring `oauth.resourceMetadataPath`) and the `oauth-rfc8414-at-oidc-path-http.json` showcase config that reproduces the failure. Closes #2172 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4 Signed-off-by: cliffhall <cliff@futurescale.com> |
||
|
|
364d8c9dbc |
feat(auth): per-server authorization and token URL overrides
Adds two optional per-server fields — `oauth.authorizationUrl` and `oauth.tokenUrl` — that override the endpoints authorization-server metadata discovery resolved, so a server can be pointed at a development or staging authorization server without changing what it advertises. SDK v2 routes both endpoints through the discovered metadata document and neither through OAuthClientProvider, so the override is applied by wrapping the client's base fetch and patching the metadata document in flight (core/auth/endpointOverrides.ts). Wrapping the base fetch — rather than only the auth fetch — also covers the discovery the SDK runs from inside the transport on the 401/refresh path. The fields round-trip through mcp.json, the /api/servers routes, the runner (CLI/TUI), and the Server Settings → Authorization panel, where a value that is not an absolute http(s) URL is flagged inline; at runtime such a value is dropped with a single warning rather than failing the connection. Signed-off-by: cliffhall <cliff@futurescale.com> |
||
|
|
b25ece8ded |
feat: per-server custom OAuth authorization-request parameters (#2018)
Real deployments gate IdP selection, prompt behavior, or audience on a query
parameter the OAuth/OIDC core specs never standardized (Keycloak's
`kc_idp_hint`, OIDC's `login_hint` / `prompt` / `acr_values`, Auth0's
`audience`), and none of them could be driven from the Inspector.
The Inspector never builds the authorize URL — the SDK's `auth()` does, and it
exposes no hook for extra parameters — so the merge happens at the one seam
that sees the finished URL: `BaseOAuthClientProvider.redirectToAuthorization`.
It lives in `core/`, so web, CLI, and TUI all inherit it.
- `core/auth/authorizationParams.ts`: the reserved-key list (`client_id`,
`code_challenge`, `code_challenge_method`, `redirect_uri`, `resource`,
`response_type`, `scope`, `state`), the key validator the form renders
inline, and the merge itself. Reserved keys are dropped with a warning
rather than silently overriding PKCE / the state binding / RFC 8707;
blank-key rows are skipped; the caller's URL is never mutated.
- Applied to the authorization request only — nothing wires it into the token
request.
- Persisted as `oauth.authorizationParams` (a string record) on the per-server
config beside `scopes`, edited as key/value rows in Server Settings ->
Authorization ("Additional authorization parameters"), with an inline error
per offending row plus a section-level warning.
- The enterprise-managed (EMA) leg redirects to a *different* authorization
server (the enterprise IdP), so it goes through the explicit
`redirectToExternalAuthorization` entry point, which skips the merge — a
`kc_idp_hint` or `audience` meant for the MCP server's AS must never be
appended to the IdP's authorize URL.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDXCSrnSBV7vZfxycG5Z6E
Signed-off-by: cliffhall <cliff@futurescale.com>
|
||
|
|
7784553674 |
fix(auth): explicit re-authorize when the OAuth callback state was lost (#1808) (#1810)
* fix(auth): offer explicit re-authorize when callback state is lost (#1808) A `/oauth/callback` that arrives with no recorded SEP-2352 discovery state made the SDK throw `AuthorizationServerMismatchError` and the Inspector dead-ended, surfacing the raw SDK text with no path forward. The SDK folds two very different failures into that one error class, so `core/auth/issuerBinding.ts` classifies them structurally: a recoverable "lost authorization state" (the SDK puts prose, not an issuer URL, in the `recordedIssuer` slot) versus a genuine cross-AS `issuer_mismatch`. Only the first gets a recovery affordance. Web: the callback failure now raises a dedicated banner ("Authorization state was lost" / "Authorize again") whose action clears the stale OAuth state for that server and starts a fresh authorization. A genuine mismatch stays a red, non-dismissible security notification with both issuers named and no one-click retry. Node runners (CLI/TUI): the same classification rewrites the callback rejection into the shared actionable copy instead of the SDK text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016S7h83UKNQ7kvSiFDNksCt * fix(auth): detect the SDK mismatch error via isInstance (#1808) The callback-leg classifier gated on `err.mcpBrand`, but the SDK declares that brand in a `static {}` block, so it lives on the constructor and is never reachable from an instance. The predicate always returned false, so `findIssuerBindingFailure()` never matched and the #1808 recovery banner was inert in production — the callback dead-ended exactly as before. Use the SDK's own `AuthorizationServerMismatchError.isInstance()`, which is cross-copy safe by construction (it consults a `Symbol.for()`-keyed brand set), with a `name` fallback for a serialization boundary. Correct the module doc, which cited cross-bundle safety as the reason to avoid `instanceof` — the very thing the SDK's branded `hasInstance` provides. Every fixture now constructs a real `AuthorizationServerMismatchError` instead of a hand-rolled object with an own `mcpBrand`, which is why a green `npm run ci` hid this. Adds a test pinning the brand's placement so the feature cannot silently go inert again. Review follow-ups: drop the unused `isLostAuthorizationStateError`, remove the duplicated banner title default, resolve the banner title/action label where the banner is raised, keep the clear-failure notification open, and bound remote-supplied issuers in user-facing copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016S7h83UKNQ7kvSiFDNksCt --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
01df9d81bb |
fix(core): start OAuth on a modern/auto-era connect (#1805) (#1809)
* fix(core): start OAuth on a modern/auto-era connect (#1805) The SDK's `server/discover` negotiation probe runs before anything else on `protocolEra: "auto" | "modern"`, and its classifier reports whatever the transport threw as `SdkError(ERA_NEGOTIATION_FAILED)` with the real error moved to `data.cause`. That buried both connect-time auth signals: the remote path's `AuthRecoveryRequiredError` (matched with `instanceof`, and carrying the authorization URL) and a direct transport's `AuthChallengeError`. An OAuth-protected server that authorized fine on the legacy era instead produced a dead-end "Version negotiation probe failed" — no redirect, and the connection status flipped to `error` despite `isConnectAuthRecoveryError` existing to prevent exactly that. - `findNestedAuthError()` walks `cause` / `data.cause` for either typed error. Not gated on the SDK's error code or message, so it survives rewording. - `connect()` unwraps the rejection at the earliest point, ahead of `withDirectAuthRecovery` (whose `isAuthChallengeError` check is shallow), the outer status guard, and every client's connect error handling — so web, CLI, and TUI are all fixed without a client-side change. - On the direct path, enable `interceptAuthChallenges` for the probing eras even with no stored tokens. Without an authProvider the 401 reaches the SDK as a raw `SdkHttpError`, and the classifier ignores the HTTP status (it only looks for a JSON-RPC error body) — so pin mode rethrew it with the 401 discarded entirely, no status and not even a cause for the walk to find. Verified against a live dual-era server that answers 401 on both `initialize` and `server/discover`: `auto` and `modern` now reject with `AuthRecoveryRequiredError` carrying a real authorization URL. Each half of the fix is load-bearing — reverting either fails the new integration tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SNjAqoEhUVVkr3UStR6om * docs(core): mark the era-probe challenge interception as a removable workaround Per review on #1806: the `|| this.probesProtocolEra()` clause compensates for an upstream SDK gap (modelcontextprotocol/typescript-sdk#2561, tracked as #1807). Make its lifespan explicit so whoever picks up #1807 knows this block is the thing to delete, rather than having to re-derive it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016S7h83UKNQ7kvSiFDNksCt * fix(core): resolve connect() when auth recovery is satisfied silently Review findings on #1809: - connect() built its handshake promise once, outside the runConnect closure withDirectAuthRecovery retries. On a "satisfied" outcome the recovery runs a complete nested connect() via reconnectAfterAuthRecovery(), so the retry leg re-awaited the already-rejected promise and rejected a connect() whose client was in fact connected. Short-circuit the retry when already connected, and skip the post-connect block the nested connect already ran (a duplicate `connect` event re-triggers every list-state manager's refresh). - Document the accepted 403 side effect of enabling interceptAuthChallenges with no stored tokens, and why every `{ pin }` counts as probing. - Replace the private-method double cast in the era-probe test with an observable assertion on the transport options, and split the non-auth failure case per era instead of a message alternation that passed either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016S7h83UKNQ7kvSiFDNksCt * fix(core): key the connect retry leg off attempt, bound nested recoveries Second-pass review findings on #1809: - The satisfied-recovery short-circuit keyed off live connection status, so an onclose landing between the nested connect() and the retry leg fell through to the rejected handshake promise and reported the stale 401 as the reason the session died. withDirectAuthRecovery now passes `attempt` to its operation, so connect() identifies the retry leg structurally and reports the teardown when the recovered session is already gone. - Bound nested silent recoveries. The `attempt >= 1` guard is per-call and a satisfied outcome recovers via a nested connect() with a fresh counter, so a server that keeps rejecting freshly refreshed tokens recursed without bound. Counted across the nesting boundary, capped at 3. - Strengthen the regression test (server info populated by the nested connect, no duplicate statusChange) and add the two missing cases: a failed reconnect still rejects, and the recursion bound terminates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016S7h83UKNQ7kvSiFDNksCt * test(core): cover the recovery teardown branch; fix a duplicated case Third-pass review findings on #1809, both about tests added in the previous commit: - The retry leg's teardown error had no coverage, so a regression to the old status fall-through would have stayed green. Covered deterministically: the nested connect dispatches `connect` after setting "connected", so closing the live transport from that listener flips the status before the retry leg runs. Asserts the error identity, which is the point of the change. - "still rejects when the reconnect underneath the recovery fails" used a challenging transport for every connect, so it walked the same bounded chain as the recursion-bound test rather than covering a non-auth reconnect failure, and its comment claimed an "error" status the auth path never reaches. Now fails the reconnect with a plain error and asserts both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016S7h83UKNQ7kvSiFDNksCt --------- Co-authored-by: rinormaloku <rinormaloku37@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
373f346e5f |
chore: close quality-gate coverage gaps (#1689) (#1766)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
Closes #1689. Typecheck cli/tui src, add a core/ format+lint gate, and gate core/json + core/client coverage. See PR #1766 for the full review history. |
||
|
|
562bdd00f1 | All of the mid-session and step-up auth work | ||
|
|
13dafe457b |
Guided-auth removal, OAuth smoke testing, and TUI/CLI support (#1531)
* refactor(web): update OAuth E2E tests to use new authentication method - Replaced `runGuidedAuth` with `authenticate` in the InspectorClient OAuth end-to-end tests to support quick authentication with CIMD pre-registration. - Updated comments to reflect the change in authentication method and its support for HTTP test metadata URLs. - Ensured that the tests continue to validate the expected authorization URL after the change. Additionally, imported `ensureCimdClientRegistration` in the OAuthManager to streamline client registration during the authentication process. * Guided-auth removal, OAuth smoke testing, and TUI/CLI support * Added CLI OAuth runner flags tests * Added clarifying comment on port 0 binding * README notes on TUI oauth callback port, prettier fixes for CLI tests * CIMD validation uses inline like EMA IdP * Fixed TUI test failures from merge * test: bring 5 OAuth files over the 90 per-file coverage gate The guided-auth-removal work added/touched several files that fell below the repo's >=90 per-file coverage gate (npm run coverage). CI doesn't run the gate (it's local-only), so validate + test:integration passed while these gaps slipped through. Bring them all back over the line: - core/auth/node/runner-oauth-callback.ts: test bad-URL/non-http-scheme throws, no-port->80 default, and IPv6 hostname bracketing. Annotate the provably-dead guards (empty host, out-of-range port, String(err) fallback, pathname "/" fallback) -- all rejected by new URL() upstream -- with justified v8 ignore comments instead of lowering the gate. - core/auth/cimd.ts: test the AS-doesn't-support-CIMD early return. - core/react/useClientSettingsDraft.ts: test the functional-updater path, the stale-onChange-after-close prev===null guard, and the timer-fires-after-close value===null guard. - ClientSettingsForm.tsx: test the CIMD checkbox toggle and CIMD URL clear. - clients/tui/src/utils/oauthDisplay.ts: test static/dcr registration kinds, the expired IdP session, and the configured-scope/empty-scope branches of formatScopes. npm run coverage now passes (exit 0) across web, cli, tui, launcher. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: cliffhall <cliff@futurescale.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5de4e911e3 |
feat(web,core): enterprise-managed authorization (EMA) (#1509) (#1512)
Adds enterprise-managed authorization (EMA): browser-side IdP login + RFC 8693 token-exchange and JWT-bearer resource-token mint, per-server EMA opt-in in Server Settings, IdP config in Client Settings, and IdP clientSecret keychain storage. Includes the v2/main merge and review follow-ups (400 on malformed client config; normalizeIdpIssuer import consolidation). |
||
|
|
0ead1deea6 |
feat(core): port v1.5 InspectorClient + transports + auth subsystem (#1302) (#1306)
* wip(core): bulk-copy v1.5 InspectorClient runtime files (#1302) Verbatim ports from origin/v1.5/main into v2/main core/: - logging/ (2 files) - storage/ + storage/adapters/ (4 files) - auth/ + auth/browser/ + auth/node/ + auth/remote/ (16 files) - mcp/config.ts + mcp/node/ (4 files, stdio transport) - mcp/remote/ + mcp/remote/node/ (12 files, HTTP/SSE transport) - mcp/inspectorClient.ts (~2169 LOC), messageTrackingTransport, fetchTracking, oauthManager, index, state/index Total: ~7,800 LOC of source files, copied unmodified via `git show origin/v1.5/main:<path> > <dest>`. Also: - Add deps: pino, hono, @hono/node-server, atomically, zustand, @types/node - tsconfig.app.json: extend paths to resolve new node-only deps from core/, add "node" to types, disable erasableSyntaxOnly (v1.5 code uses parameter properties — TODO to re-enable in follow-up) Compiles down to 47 type errors, all in three categories: - 24 TS2305: v2 dropped v1.5 type exports (InspectorClientOptions, CreateTransport, InspectorClientEnvironment, AppRendererClient, SamplingCreateMessage class, ElicitationCreateMessage class, jsonUtils helpers, OAuth events). Per user direction, will re-add these to v2's diverged files (types.ts, samplingCreateMessage.ts, etc.) so the ported code stays verbatim. - 15 TS7006: implicit-any params in v1.5 code (v1.5 tsconfig was looser than v2's noImplicitAny). Need explicit annotations. - 8 other TS errors: ../package.json import, Transport|null narrowing, hono Context generic mismatch, one unused parameter. Tests not yet ported (19 files, ~11,170 LOC). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wip(core): integrate ported v1.5 InspectorClient + port tests (#1302) Source integration (typecheck + build now pass): - Re-add v1.5 types/classes dropped during v2 evolution: - types.ts: CreateTransport*, AppRendererClient, InspectorClient{Environment,Options} - samplingCreateMessage.ts / elicitationCreateMessage.ts: classes + keep v2's InspectorPending{Sampling,Elicitation} interface - inspectorClientEventTarget.ts: oauth* event keys - json/jsonUtils.ts: convertParameterValue / convertToolParameters / convertPromptArguments + ParameterSchema - inspectorClient.ts: replace `../package.json` import (v2 has no core/package.json) with a hardcoded identity constant - remote/node/server.ts: cast hono Context for streamSSE (hono's generic typing tightened since v1.5) - oauth-callback-server.ts: drop unused hostname field - tsconfig: enable @types/node, add paths for new bare-module deps (pino, zustand, hono, atomically) in app + test configs - vite.config.ts: mirror those module aliases for the unit project (which runs from repoRoot) Tests ported (19 files, ~11K LOC) under clients/web/src/test/core/: - All core/__tests__/auth/* (7 files) - inspectorClient.test.ts + 4 OAuth variants - mcp/oauthManager.test.ts, jsonUtils.test.ts - transport, remote-transport, remote-server-config, storage-adapters - helpers/oauth-client-fixtures.ts Bulk-rewrote `from "../mcp/…"` → `from "@inspector/core/mcp/…"` etc. Current state: - typecheck: 0 errors - build: passes (vite v8, 788 modules) - unit tests: 1014 / 1034 pass (937 pre-existing + 77 new core tests) - 20 failures in 13 files; failure modes: - 13 file-load failures needing the v1.5 `test-servers/` package (@modelcontextprotocol/inspector-test-server) which isn't in v2 yet or real fs/network not available in happy-dom - 7 assertion failures in discovery.test.ts + state-machine.test.ts (SDK 1.29 vs 1.25 API drift — mocks don't match new shape) Coverage gate not yet checked against new files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wip(core): port test-servers, more types, partial test integration (#1302) - Port v1.5 test-servers/ package (12 files, ~5K LOC) under repo-root/test-servers/, wire @modelcontextprotocol/inspector-test-server alias in both vite.config.ts (regex form) and tsconfig.test.json. - Add deps: express ^5, yaml ^2, @types/express ^5. - Re-export InspectorClientOptions/Environment/Transport* from inspectorClient.ts so v1.5 tests that import them from that module path keep resolving. - Switch InspectorClientEventMap to use SamplingCreateMessage / ElicitationCreateMessage classes (matches v1.5 event payload contracts) while keeping the InspectorPending{Sampling,Elicitation} interfaces in the *.ts modules for v2 consumers that still use them. - Fix verbatim-module-syntax issue in test-servers and drop unused stdio config field. - Adjust inspectorClient.test.ts task types to TaskWithOptionalCreatedAt (SDK 1.29 tightened Task shape vs v1.5's 1.25). - Drop spurious maxMessages/maxFetchRequests/maxStderrLogEvents from remote-transport.test.ts (never on InspectorClientOptions). State: - typecheck: 0 errors - build: passes - unit tests: 1099 / 1270 pass - 171 failures in 10 test files, almost all integration tests that need real HTTP/fs/network (happy-dom can't host them). Next: add a node-env vitest project to run these properly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(core): port v1.5 InspectorClient + transports + auth subsystem (#1302) Bulk-port of the v1.5 core runtime (~7,800 LOC source + ~16K LOC tests + ~5K LOC test-servers/) into v2. Files copied verbatim from origin/v1.5/main via `git show >`; only minimal adapters added on the v2 side. Subsystems landed ----------------- - core/logging/, core/storage/, core/auth/ (browser + node + remote) - core/mcp/ runtime: inspectorClient.ts (2169 LOC), oauthManager, messageTrackingTransport, fetchTracking, config, index - core/mcp/node/ (stdio transport) - core/mcp/remote/ + remote/node/ (HTTP/SSE transport + Hono server) - test-servers/ package (composable MCP test servers, fixtures, harness) v2 adapter layer ---------------- - types.ts: append CreateTransport*, AppRendererClient, InspectorClient{Environment,Options} (v2 had pruned these — v1.5 needs them) - samplingCreateMessage / elicitationCreateMessage: keep v1.5 classes AND v2's Inspector Pending* interfaces in the same files - inspectorClientEventTarget: add oauth* event keys; switch pending sampling/elicitation payloads to the classes (matches v1.5 contracts) - json/jsonUtils: append convertParameterValue/convertToolParameters/ convertPromptArguments + ParameterSchema - inspectorClient.ts: re-export Options/Environment/Transport* so tests that import these from `inspectorClient.js` keep resolving; replace `../package.json` import with a hardcoded identity (v2 has no core/package.json) - remote/node/server.ts: cast hono Context for streamSSE (hono generic tightened since v1.5) - oauth-callback-server: drop unused hostname field - tsconfig.app.json / tsconfig.test.json: add @types/node + paths for pino/zustand/hono/@hono/node-server/atomically/express/yaml, disable erasableSyntaxOnly (parameter-property syntax from v1.5) - vite.config.ts: regex-form module aliases (so the package's exports field handles subpaths like `hono/streaming`, `zustand/middleware`), alias for @modelcontextprotocol/inspector-test-server → test-servers/src Tests ----- - 19 v1.5 test files copied under clients/web/src/test/core/, imports rewired to @inspector/core/* and @modelcontextprotocol/inspector-test-server - 1004 unit tests pass; 298 storybook tests pass; coverage gate passes Known follow-ups (out-of-scope here, tracked in TODOs) ------------------------------------------------------ - 13 integration test files are excluded from the unit project (spawn real HTTP/stdio servers; run real OAuth flows; mock SDK auth → happy-dom CORS). Needs a node-env vitest project. Files listed under `test.projects[0].test.exclude`. - Source files exercised only by those integration tests are excluded from the coverage gate pending the node-env project. Listed under `test.coverage.exclude` with comment. - Manual smoke tests against real stdio / HTTP / OAuth server per the AC on #1302 still need verification locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs/fix: address #1306 review (AGENTS tree, #1307 links, @types/node, .substr) - AGENTS.md: expand the project-structure tree to include all v1.5-ported subsystems (core/auth/{browser,node,remote}, core/storage, core/logging, core/mcp/{node,remote/{,node}}, core/mcp/state) plus the new top-level test-servers/ directory (with a note about its @modelcontextprotocol/ inspector-test-server alias). - vite.config.ts: reference #1307 from both the test exclude block and the coverage exclude block so the next person editing either sees the marching order for shrinking the lists. - package.json: restore @types/node to ^24 (an earlier `npm install --save-dev @types/node@^22` accidentally pinned it lower than the repo was using; nothing here required ^22). - core/mcp/fetchTracking.ts: replace deprecated .substr(2, 9) with .slice(2, 11) to match the convention in core/mcp/inspectorClient.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de6960ec5a | ||
|
|
f491c7fc9a | ||
|
|
baefc96806 | ||
|
|
8dd7bc2920 | ||
|
|
84a65f28d8 | ||
|
|
2eeab05d00 | ||
|
|
463227c947 | ||
|
|
69fcdd364c | ||
|
|
781aa69d19 | ||
|
|
65fd2b9e35 | ||
|
|
7ca753bc4c | ||
|
|
364d8c9dbc | ||
|
|
b25ece8ded | ||
|
|
7784553674 |
||
|
|
01df9d81bb |
||
|
|
373f346e5f |
||
|
|
562bdd00f1 | ||
|
|
13dafe457b |
||
|
|
5de4e911e3 |
||
|
|
0ead1deea6 |