Commit graph inspector/clients
Author SHA1 Message Date
cliffhall
c026d4d84a docs: address review nits on the v2/main merge
- transport.ts: update the SSE-branch comment to say the default path uses
  fetchWithOptionalAuthIntercept (proxy-aware baseFetch + optional auth-challenge
  intercept), not the bare proxy-aware baseFetch.
- clients/cli/README.md: fix the §5 mid-session validation link to use the
  ../../specification/ prefix like the sibling links in the same sentence
  (previously resolved to a nonexistent clients/cli/ path).

Both flagged by the @claude review of PR #1601; comment/doc-only, no runtime change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-04 15:42:22 -04:00
Bob Dickinson
512f45d357 fix(auth): reject load() when persist read fails 2026-07-03 14:50:04 -07:00
cliffhall
cfbdfdf2bd Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1
# Conflicts:
#	AGENTS.md
#	clients/cli/README.md
#	clients/web/src/App.test.tsx
#	clients/web/src/App.tsx
#	clients/web/src/test/core/auth/utils.test.ts
#	core/mcp/node/transport.ts
#	test-servers/src/composable-test-server.ts
2026-07-03 17:39:07 -04:00
Bob Dickinson
a24091484c fix(auth): await OAuth storage load() to fix remote OAuth races 2026-07-03 14:18:00 -07:00
Bob Dickinson
7bd26c938e refactor: migrate to RemoteOAuthStorage for web OAuth handling 2026-07-03 13:11:13 -07:00
cliffhall
143e0d3f71 test+docs: add inspectorTabs test; document mid-session auth modules
- Add a dedicated `inspectorTabs.test.ts` (was relying only on incidental
  coverage from App.test.tsx) — covers the tab-id constants and both branches
  of the `isInspectorTabId` type guard; 100% per-file.
- AGENTS.md: extend the `core/auth/` structure-tree entry to describe the new
  mid-session recovery modules (challenge parsing, scope union, shared OAuth UX
  copy, force-reauthorization) and the node loopback callback flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
2026-07-03 13:20:29 -04:00
cliffhall
14404bc405 refactor: remove unsafe non-null assertions / double-cast in auth paths
Address the TypeScript-safety review items on the mid-session auth work:

- oauthManager: replace the unguarded `this.oauthConfig.storage!` assertions on
  the EMA re-mint/persist paths with a `requireStorage()` helper that throws a
  clear error; in `getOAuthState` reuse the already-narrowed local `storage`.
- cli: `parseRunnerOAuthCallbackUrl` always returns a config, so make
  `callMethod`'s `callbackUrlConfig` (and the always-passed `cliAuthOverrides`)
  required instead of asserting them non-null at three call sites.
- test-servers/resolve-config: drop the `as unknown as T` double cast; collect
  into a base-shaped array and do a single honest downcast at the return.

No behavior change; validate + coverage green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
2026-07-03 13:16:05 -04:00
cliffhall
74c6491b52 refactor(web): resolve Mantine/CSS rule violations in mid-session auth UI
- App.tsx: replace the inline `style={{}}` on the re-auth banner wrapper with
  Mantine style props (pos/top/bg) plus a `.reauth-banner-bar` class for the
  z-index/box-shadow that have no Mantine shorthand.
- App.tsx: extract the ~100-line inline `onAuthorize` and the inline `onCancel`
  step-up handlers into named `handleStepUpAuthorize`/`handleStepUpCancel`.
- ReAuthBanner: move the flat `styles={{ root }}` (background/border) into a
  `reauth` Alert theme variant; replace the raw `<span>` with `<Text>`.
- StepUpAuthModal: extract the inline `onClick` wrapper into `handleAuthorize`.

No behavior change; validate + integration green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
2026-07-03 13:03:01 -04:00
cliffhall
f4b6095719 test: close ≥90% per-file coverage gaps for mid-session auth
The mid-session auth work passed `validate`/CI but failed the local-only
`npm run test:coverage` per-file gate (≥90% lines/statements/functions/
branches) on 12 files. Add targeted unit/integration tests so every changed
file clears the gate; annotate provably-dead defensive branches with justified
`/* v8 ignore */` comments rather than relaxing the gate.

Files brought to ≥90 on all four dimensions:
- core/auth: mcpAuth, oauthUx, utils, challenge, node/runner-interactive-oauth
- core/mcp: oauthManager, inspectorClient, node/authChallengeFetch,
  remote/remoteClientTransport, remote/node/{remote-session,server}
- clients/web/src/utils: oauthResume, browserTabVisibility

`npm run test:coverage` now passes (3575 tests, 0 threshold failures);
`npm run validate` green across all four clients.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
2026-07-03 12:54:37 -04:00
Bob Dickinson
a066873e37 Fixes for remaining issues 2026-07-02 18:36:44 -07:00
Bob Dickinson
8f1ed7588d Prettier fix on last commit 2026-07-02 18:03:18 -07:00
Bob Dickinson
52023ff1c3 fix(auth): clear OAuth callback timer and reorder EMA scope persist
Clear the loopback callback timeout in runner-interactive-oauth finally
so already_authorized and error paths do not hang the CLI/TUI. Check EMA
step-up satisfaction against minted token scope before persisting the
requested union, matching the standard OAuth path.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 17:55:34 -07:00
Bob Dickinson
0635d09e83 Fixed CI failure (test fixture) and added local CI script 2026-07-02 17:07:25 -07:00
Bob Dickinson
26876a98d7 Added pretest target to build test server 2026-07-02 16:44:04 -07:00
Bob Dickinson
479e757315 Merge remote-tracking branch 'origin/v2/main' into feat/mid-session-auth 2026-07-02 16:38:33 -07:00
Bob Dickinson
562bdd00f1 All of the mid-session and step-up auth work 2026-07-02 16:35:31 -07:00
cliffhall
e19d11070a merge: #1599 gate src/lib coverage (#1594) 2026-07-01 22:48:03 -04:00
cliffhall
775b98a019 docs(core): clarify redaction scope + add nested request-body test
Address round-1 review feedback on fetchTracking redaction:
- Cross-reference REDACTED_VALUE / REDACTED_HEADER_VALUE so a reader
  understands why two identical "[REDACTED]" constants coexist.
- Document that redactBody deliberately scopes to form-encoded + JSON
  (multipart/binary bodies pass through; OAuth never uses multipart).
- Add a tracker-path test asserting a nested sensitive key in a JSON
  request body is redacted in the recorded copy while the live outbound
  body stays byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 22:34:40 -04:00
cliffhall
1fd1313c96 test(web): gate src/lib/** under the coverage include globs (closes #1594)
Broaden the web vitest `coverage.include` from the single-file
`src/lib/sandbox-csp.{ts,tsx}` entry to `src/lib/**/*.{ts,tsx}` so the
per-file >=90 four-dimension gate applies to every library module under
`src/lib`, not just sandbox-csp.

- Add unit tests for the previously-ungated `environmentFactory.ts`
  (createWebEnvironment) and `types/customHeaders.ts` value helpers,
  bringing both to 100% on all four dimensions.
- Annotate downloadFile.ts's provably-dead `?? ""` fallback (String.split
  always returns a non-empty array so .pop() is never undefined) with a
  justified `v8 ignore`, closing its last uncovered branch (92.3 -> 100).

All five src/lib files now report 100/100/100/100.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 22:23:43 -04:00
cliffhall
524bf2d543 feat(core): redact sensitive body + URL-query fields in fetch log (closes #1593)
Extends the header redaction from #1585. Before a fetch entry reaches any
sink, mask known-sensitive credentials in the request/response bodies and
the URL query string, in addition to headers:

- redactBody(): form-urlencoded + JSON bodies, recursive over nested
  objects/arrays, best-effort (never throws), values-only.
- redactUrlQuery(): masks sensitive query params, preserves path,
  non-sensitive params, and any fragment.
- Sensitive fields: client_secret, code, refresh_token, access_token,
  id_token, code_verifier, client_assertion, assertion, password, token
  (case-insensitive), reusing the [REDACTED] sentinel.

Applied to the request success + error entries and the async response-body
callback. Live outbound requests are byte-identical — only the logged copy
is redacted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 22:20:42 -04:00
cliffhall
c6b5af3b81 merge: v2/1596-test-stability (#1597) 2026-07-01 22:05:57 -04:00
cliffhall
e425602295 merge: v2/1548-web-remote-oauth-storage (#1592)
# Conflicts:
#	clients/web/src/test/core/auth/utils.test.ts
2026-07-01 22:05:52 -04:00
cliffhall
cc37a755ae merge: v2/1562-oauth-state-hardening (#1589) 2026-07-01 22:04:22 -04:00
cliffhall
52cd90bd50 merge: v2/1560-downloadfile (#1586) 2026-07-01 22:04:18 -04:00
cliffhall
3f22299b14 merge: v2/1559-hostcontext-utils (#1591) 2026-07-01 22:04:18 -04:00
cliffhall
e55867cd45 merge: v2/1558-sandbox-csp (#1588) 2026-07-01 22:04:12 -04:00
cliffhall
9257c849b9 merge: v2/1557-mcp-app-demo (#1587) 2026-07-01 22:04:07 -04:00
cliffhall
4c5036e032 merge: v2/1564-cli-exit-codes (#1583) 2026-07-01 22:04:01 -04:00
cliffhall
4e3cd591a4 merge: v2/1563-node-proxy (#1590) 2026-07-01 22:03:55 -04:00
cliffhall
2d1a99ab37 merge: v2/1561-redact-headers (#1585) 2026-07-01 22:03:51 -04:00
cliffhall
dd7812063c test(web): make the debounce-cancellation test load-bearing (review round 1)
The "cancels a pending debounce timer when the input is re-focused" test
passed trivially after the delay:null change: synchronous typing meant the
assertion ran at t≈0, before the 300ms debounce could ever fire, so it
passed whether or not handleFocus's clearTimeout worked.

Rewrote it with fake timers so the debounce window elapses deterministically
(advance the clock by 400ms directly). The interaction is driven with
fireEvent rather than userEvent because userEvent's async internals deadlock
under vitest fake timers with the Mantine/happy-dom stack. Verified
load-bearing: removing the clearTimeout in handleFocus makes it fail
(expected 1 to be 0), and it passes once restored.

Also tightened the sibling-context comment (with delay:null the sibling is
reliably "es"); the tolerant /^es?$/ regex is left as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 21:22:04 -04:00
cliffhall
4bad110942 test(web): eliminate timeout flakiness in the web test suite (closes #1596)
Root-cause and deterministically fix the recurring 5s-timeout flakes that
surfaced under v8-instrumented, concurrent load. All fixes are test-only; no
product source changed. No test was skipped or disabled, and the per-file
coverage gate (>=90 on all four dims) still holds.

Common root cause across the modal/form/screen suites: userEvent.setup()
schedules a real setTimeout between keystrokes. Under CPU contention those
yields balloon, so multi-field interactions blew past the 5s per-test
timeout. Fix: userEvent.setup({ delay: null }) removes the per-keystroke
real-timer dependence (typing dispatches synchronously), making the suites
load-independent.

Per file:
- ServerConfigModal / ServerImportConfigModal / ServerImportJsonModal /
  ResourcesScreen / InspectorView: userEvent.setup({ delay: null }).
- PromptArgumentsForm (completions): replaced wall-clock sleeps sized just
  past the 300ms completion debounce (await setTimeout(400)) — which race
  the debounce timer under load — with awaited conditions (findBy/waitFor)
  on the real rendered outcome, plus delay:null. Negative assertions now
  lean on the component's synchronous state/timer teardown instead of a
  timed window, so they are deterministic regardless of machine speed.
- inspectorClient integration "tracks stderr logs": the child's stderr is
  piped out-of-band from the tool's JSON-RPC response, so reading the log
  synchronously after callTool raced the stderr chunk. Wrapped the assertion
  in vi.waitFor so it polls until the line lands.

Verified: web validate + 6x test:coverage (4 sequential + 2 concurrent
under load) + standalone test:integration, all green (229 files / 3237
tests per coverage run; 799 integration tests), zero intermittent failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 21:11:11 -04:00
cliffhall
4e149719bd fix(auth): route clearClientInformation through clearAfterHydration too
Round-2 @claude review of #1592 caught a seventh member of the clear
family that the first pass missed: clearClientInformation still wrote to
the store synchronously, carrying the same pre-hydration clobber risk the
clearAfterHydration helper was written for (a clear before hydration lands
persists a near-empty blob, overwriting the whole on-disk oauth.json).
Wrap its mutation the same way and add a clear-before-hydration test
proving a sibling token (and thus every other server's blob) survives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 19:26:25 -04:00
cliffhall
8c10b8eb8e fix(auth): make clear* hydration-safe, migrate raw-key blobs, doc keepalive/memo caveats
Address round-1 @claude review of #1592:

- OAuthStorageBase.clear* no longer mutate the pre-hydration (empty) store —
  a clear issued before hydration is deferred until it lands, so it merges
  onto the real persisted state instead of (a) being resurrected by the late
  rehydrate merge or (b) persisting a near-empty blob that clobbers every
  other server's on-disk credential. Already-hydrated clears stay synchronous.
- store.setServerState migrates a pre-normalization raw-key blob onto the
  canonical key on first partial write (was: shadowed it with a fresh
  canonical entry, orphaning the raw blob's other fields). clearServerState
  now also drops the raw-key orphan.
- Document the keepalive 64KB combined-body ceiling on the remote-storage
  write, the getRemoteOAuthStorage memo key intentionally omitting fetchFn,
  and the real ordering invariant behind the sync getScope.
- Tests for the clear-before-hydration and raw-key-migration paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 19:20:49 -04:00
cliffhall
cb54de50d8 refactor(web): document + guard sandbox CSP builder per review
Address round-1 @claude review of #1588 (no behavior change to the
enforced policy):

- approveCspSources / buildSandboxCspPolicy: document that
  resourceDomains intentionally feeds script-src/style-src per the
  McpUiResourceCsp contract, and that approve screens injection-safety
  only (not breadth — a bare `*` is accepted; safe under the
  opaque-origin sandbox).
- CSP_KEYS: replace the `satisfies` (proves listed keys valid) with an
  exhaustiveCspKeys() helper that ALSO fails to compile if the upstream
  ext-apps type gains a domain key CSP_KEYS omits, so a requested
  restriction can never be silently dropped.
- SAFE_CSP_SOURCE: accept case-insensitive schemes (URL schemes are
  case-insensitive); previously over-rejected `HTTPS://…`. Adds tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 19:17:18 -04:00
cliffhall
b152f74ccd fix(ci): run CI on Node 22.x and correct undici 8 Node floor (#1563)
undici 8.5.0 requires Node >= 22.19.0 (its declared engines), so CI's
Node 20.x could not load undici's CacheStorage (missing
webidl.util.markAsUncloneable) and the proxy integration tests failed.
Bump the CI setup-node to 22.x to match the repo's Node floor.

Reconcile the declared floor with undici's real requirement: bump root
engines.node to >=22.19.0 and correct the CLI README note (undici 8.5.0
needs >=22.19.0, not >=22.7.5). Also document proxy support in the web
README and clarify that a caller-supplied eventSourceInit.fetch bypasses
the proxy wrapper by design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 19:17:05 -04:00
cliffhall
d21da16983 refactor(auth): address review — accurate toast copy, document state asymmetry, tighten test
- Soften the OAuth-callback-rejected toast: the shape guard is
  defense-in-depth, not full CSRF prevention (PKCE remains primary), so the
  message no longer overstates ("rejecting to prevent a cross-site request" ->
  "did not originate from this session").
- Document the intentional asymmetry between a present-but-malformed `state`
  (rejected) and a wholly absent `state` (accepted, matched via
  OAUTH_PENDING_SERVER_KEY) — rejecting the null case would mask real provider
  error redirects that omit `state`.
- Make the "valid state is not rejected" test assert the specific downstream
  "could not be matched" toast (seeding OAUTH_PENDING_SERVER_KEY) instead of an
  indirect "some toast fired" check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 19:13:17 -04:00
cliffhall
e5dbdca65c test(test-servers): export MCP_APP_DEMO_URI, dedupe in integration test
Address code-review DRY note on PR #1587: MCP_APP_DEMO_URI was defined
in both the fixture and the integration test. Export it from the fixture
and import it in the test so the two stay in lockstep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 19:12:44 -04:00
cliffhall
fa5514a380 fix(cli): guard numeric .code to HTTP range; bound cause-chain depth
Address @claude review of #1583:

- statusOf() now only treats a numeric `.code` as an HTTP status when it
  is in the 100-599 range, so an MCP SDK McpError JSON-RPC code (e.g.
  -32601 MethodNotFound) is no longer leaked into the envelope `status`
  or misclassified as AUTH_REQUIRED (it falls back to USAGE).
- causeOf() now carries a depth cap (MAX_CAUSE_DEPTH) so a cyclic/
  self-referential `error.cause` chain terminates instead of recursing
  infinitely.
- Add tests for both: a -32601 McpError-shaped error (status undefined,
  exit USAGE) and a self-referential cause chain (finite string).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 19:10:25 -04:00
cliffhall
c299b6e9d8 feat(web): migrate OAuth store to shared /store API (RemoteOAuthStorage parity) (closes #1548)
Migrate the web client's OAuth storage from BrowserOAuthStorage
(sessionStorage) to RemoteOAuthStorage, which POSTs through the backend's
/api/storage/oauth route to ~/.mcp-inspector/storage/oauth.json (mode 0600).
A credential obtained in the browser is now the same blob the TUI/CLI read
on the same host, giving web ⇄ TUI ⇄ CLI parity (Wave 1 of #1579).

Core (shared with TUI/CLI):
- OAuthStorageBase now drives a single async hydration (`ready()`,
  `getHydrationError()`); every post-redirect read (getClientInformation,
  getTokens, getCodeVerifier, getServerMetadata, getIdpSession) and every
  save awaits it so a late hydration merge cannot clobber a fresh write.
  getCodeVerifier/getServerMetadata become async; the store is created with
  skipHydration:true so there is no auto-hydration to race the explicit one.
- store.ts adds normalizeServerUrl so a token saved under
  https://Example.com/mcp is found when the CLI asks for
  https://example.com/mcp/; getServerState falls back to the raw key.
- remote-storage adapter POSTs with keepalive so the write survives the
  OAuth authorize redirect, surfaces swallowed persist failures, and gives
  richer read/write error messages.
- generateOAuthState throws instead of silently degrading to Math.random.
- NodeOAuthStorage honors MCP_INSPECTOR_OAUTH_STATE_PATH for isolated
  fixtures. Provider codeVerifier()/getServerMetadata() + the EMA idpOidc and
  connection-state readers await the now-async storage.

Web:
- environmentFactory + a new shared lib/remoteOAuthStorage accessor
  (memoized per {baseUrl, authToken}) wire RemoteOAuthStorage into the
  connection path, the EMA IdP hook, and the per-server "clear OAuth" action
  so all three share one in-memory view of oauth.json.

Tests: async-hydration + normalizeServerUrl suite, keepalive/error-path
coverage, and the sync→async migration across existing storage/provider/EMA
tests. Web validate + coverage gate + integration suite green; CLI/TUI/
launcher validate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:39:17 -04:00
cliffhall
d5be4929e9 feat(core): honor HTTPS_PROXY/HTTP_PROXY/NO_PROXY in the Node transport (closes #1563)
Wrap the Node transport's fetch with an undici EnvHttpProxyAgent dispatcher
when a standard proxy env var is set, so the CLI and web backend can reach
remote MCP servers through corporate proxies with zero new flags. undici is
imported lazily (only when a proxy var is present) and added as a dependency
at the repo root and in the CLI client; a missing undici raises an actionable
error. Documents the behavior and dependency in the CLI README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:25:16 -04:00
cliffhall
2b84281346 feat(auth): harden OAuth state generation and callback validation (closes #1562)
generateOAuthState() now throws when crypto.getRandomValues is unavailable
instead of silently degrading to a non-cryptographic Math.random() fallback —
OAuth state is a CSRF token and must be unpredictable.

The web /oauth/callback handler now rejects a returned `state` param that does
not parse to the expected 64-char-hex authId shape (a forgery indicator),
surfacing a clear error toast instead of proceeding with an undefined session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:23:38 -04:00
cliffhall
77e57e0d25 feat(web): add sandbox CSP builder library (closes #1558)
Add a pure, DOM-free CSP-builder library at
clients/web/src/lib/sandbox-csp.ts that validates app-supplied
`_meta.ui.csp` requests, filters them to safe sources, builds the
locked-down Content-Security-Policy string, and wraps untrusted widget
HTML in a fixed host shell whose first <head> child is the CSP meta.

- SAFE_CSP_SOURCE: strict regex that rejects directive/attribute
  breakouts (`;`, `"`, `<`, `>`, whitespace).
- approveCspSources(): drops unsafe entries (with a warning) and omits
  empty keys; echoes back only what the host will enforce.
- buildSandboxCspPolicy(): `default-src 'none'` catch-all, `form-action
  'none'`, source-allowlist filtering, `'unsafe-inline'` for the app's
  own inline script/style.
- escapeHtmlAttr() / wrapSandboxedHtml(): defense-in-depth so untrusted
  bytes never precede the applied policy.

Re-implements the sandbox-csp portion of PR #1510 (Wave 1 of #1579) as a
standalone library; wiring into the app bridge is a follow-up issue.
Adds full unit tests (100% lines/statements/functions/branches) and
lists the file in the web coverage `include` so it is gated (the
untested legacy src/lib siblings stay out until they get tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:23:13 -04:00
cliffhall
5831b5c3d9 feat(web): extract hostContext utilities for the Apps host (closes #1559)
Add clients/web/src/components/elements/AppRenderer/hostContext.ts — a pure
utilities module for the MCP Apps host:

- currentTheme(): read the resolved Mantine color scheme from the DOM
- currentStyles(): map the inspector's Mantine/-inspector design tokens to a
  McpUiHostStyles snapshot via STYLE_VARIABLE_SOURCES
- measureContainerDimensions(): whole-pixel container measurement
- snapshotHostContext(): assemble the initial McpUiHostContext seed

Wave 1 of the PR #1510 decomposition (#1579). Pure extraction only — the
AppRenderer/AppsScreen/createAppBridgeFactory rewrites belong to Wave 2.
Full unit coverage (100% lines/statements/functions/branches).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:22:46 -04:00
cliffhall
0d50b908a3 feat(test-servers): mcp_app_demo preset + _meta on tool/resource defs (closes #1557)
Add an `mcp_app_demo` tool/resource preset — a self-contained MCP App widget
that exercises size-changed, ui/message, log notifications, and host-context
rendering with no external server. Requires plumbing an optional `_meta` field
through the composable test server's `ToolDefinition` and `ResourceDefinition`
so clients can read tool-level `_meta.ui.resourceUri` and resource-level
`_meta.ui.csp`.

- composable-test-server: optional `_meta` on ToolDefinition/ResourceDefinition,
  passed through to registerTool config and the resource content item.
- test-server-fixtures: `createMcpAppDemoTool()`, `createMcpAppDemoResource()`,
  inline `MCP_APP_DEMO_HTML` widget; wired into getDefaultServerConfig.
- preset-registry: `mcp_app_demo` tool + `mcp_app_demo_widget` resource presets.
- integration test asserting the `_meta` plumbing end-to-end.

Wave 1 of PR #1510 decomposition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:22:25 -04:00
cliffhall
26478159be feat(web): generalize downloadFile.ts with downloadBlob, fileNameFromUri, isHttpUrl (closes #1560)
Extend the browser download library beyond JSON so the Apps host's
ui/download-file support can download arbitrary embedded resources and
open http(s) links safely, with no UI wiring in this change:

- downloadBlob(): temp-anchor download core with a setTimeout(...,0)-
  deferred revokeObjectURL so a synchronous revoke can't abort the
  scheduled download (Firefox/Safari, intermittently Chrome).
- downloadJsonFile(): refactored on top of downloadBlob; callers unchanged.
- fileNameFromUri(): last path segment sanitized (control/format chars
  stripped, disallowed filename chars -> _, capped at 255, "download"
  fallback).
- isHttpUrl(): parse-and-allowlist http(s) only, else null.

Re-implementation of the downloadFile.ts slice of PR #1510 as Wave 1 of
the #1579 decomposition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:21:41 -04:00
cliffhall
1a6e32be50 feat(core): add AppInfo + extractAppInfo() to core/mcp/apps.ts (closes #1556)
Add a machine-readable summary of an MCP App's UI metadata: the AppInfo
interface plus extractAppInfo(), which merges a tool's _meta.ui
(resourceUri, visibility) with the linked UI resource's _meta.ui (csp,
permissions, domain, prefersBorder, mimeType). Content-block matching is
exact-URI first, then a lowercase + trailing-slash normalized match, then
a single-block fallback since resources/read returns the requested
resource by definition. Shared plumbing for the programmatic-review path
(CLI --app-info, integration tests) without rendering the app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:20:13 -04:00
cliffhall
381027063d feat(core): redact sensitive headers in the fetch log (closes #1561)
Redact Authorization, Cookie, Set-Cookie, Proxy-Authorization, X-Api-Key,
and x-mcp-remote-auth to [REDACTED] in createFetchTracker before any
request/response entry reaches the in-memory log, pino logger, or
persisted session storage. Comparison is case-insensitive; original key
casing is preserved and the live outbound request is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:19:59 -04:00
cliffhall
a5d8d80ffc feat(cli): exit-code map + structured JSON error envelopes (closes #1564)
Give the CLI a stable machine-readable failure surface:

- EXIT_CODES map (0 ok / 1 usage / 2 no-app / 3 auth-required /
  4 unreachable / 5 tool-error)
- ErrorEnvelope interface + CliExitCodeError for pre-classified exits
- pure classifyError() (status/cause/pattern heuristics) and
  formatErrorOutput()/handleError() that emit one JSON line on stderr
- in-process cli-runner mirrors the binary via formatErrorOutput so tests
  observe the real exit code and envelope
- document the exit-code map + envelope shape in clients/cli/README.md

Wave 1 foundation of the PR #1510 decomposition; shapes kept clean for
reuse by --app-info (exit 2), stored-auth (exit 3), and --format json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 18:08:56 -04:00
Cliff Hall
2a60ac5dce
feat(web): add a View Source toggle to the resource preview (closes #1552) (#1553)
* feat(web): add View Source toggle to resource preview (closes #1552)

Markdown, CSV, and HTML resources render in a way that hides the
underlying text (react-markdown, the CSV table, the sandboxed HTML
iframe). Add a "View Source" button to the resource preview footer, left
of Refresh and styled to match it, that swaps the rendered preview for
the raw resource text. In source mode the button reads "View Rendered".

- Only shown for markdown/csv/html (other kinds already show their text,
  or have no meaningful text source).
- Source mode forces ContentViewer through its plain-text renderer; the
  displayed MIME label keeps the resource's real type.
- The toggle resets to the rendered view when the previewed resource
  changes (React's adjust-state-during-render pattern — no effect).

Tests cover the toggle round-trip, per-kind visibility, and the reset;
a Storybook play function exercises rendered → source → rendered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(web): address View Source review nits (#1553)

- Per-item source gating: in source mode only source-toggleable items
  switch to plain text, so a mixed multi-part resource doesn't force an
  image/PDF blob through the text decoder.
- Share a FooterButton constant between Refresh and the View Source
  toggle so the two stay visually identical (per the subcomponent
  guidance — both had the same two inline props).
- Add aria-pressed to the toggle so assistive tech gets explicit on/off
  state beyond the label flip.

Tests added for the aria-pressed state and the mixed multi-part case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:05:16 -04:00