* fix(server): guard the /fetch proxy against SSRF to link-local/metadata
The /fetch proxy forwarded any user-supplied http/https URL straight into
fetch(), so a malicious remote MCP server could advertise OAuth metadata
pointing at the cloud metadata endpoint (169.254.169.254) and have the proxy
exfiltrate instance credentials (CodeQL js/request-forgery, alert #53).
Add a targeted SSRF guard: resolve the target host and reject link-local /
cloud-metadata addresses (169.254.0.0/16, IPv6 link-local, AWS IPv6 IMDS).
Loopback and RFC1918/private LAN targets stay reachable, since connecting to
local and self-hosted MCP servers is a core Inspector use case. Redirects are
now followed manually so every hop is re-validated, closing the
redirect-to-internal bypass. Returns 403 on a blocked target.
Adds endpoint tests for a direct metadata target, a redirect into a blocked
address, and normal redirects between allowed hosts.
Resolves code-scanning alert #53. Part of #1706.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HiccCEh9mwCfVzE8qYcop1
* fix(server): block IPv4-mapped IPv6 form of metadata IP in /fetch guard
The IPv4-mapped IPv6 branch of isBlockedProxyAddress only matched the
dotted-decimal form (::ffff:169.254.169.254), but the WHATWG URL parser
serializes IPv4-mapped addresses in hex (::ffff:a9fe:a9fe), so
http://[::ffff:169.254.169.254]/ resolved past the guard and still reached
the cloud metadata service on dual-stack hosts — the exact vector this guard
targets.
Extract the embedded IPv4 from both the hex and dotted forms and re-check it.
Adds a regression test for the IPv4-mapped IPv6 metadata address.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HiccCEh9mwCfVzE8qYcop1
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
npm does not reliably apply these transitive overrides to an existing
lockfile (bare and targeted overrides via install/update leave residual
vulnerable copies). This regenerates the lockfile from scratch with
version-targeted overrides so every copy of the affected packages
resolves to a patched version.
Resolves Dependabot alerts:
- esbuild #121 (-> 0.28.1)
- minimatch #69, #66, #61 (3.x -> 3.1.5)
- @babel/core #132 (-> 7.29.7; completes the partial fix from #1712)
Also pins js-yaml overrides (3.15.0 / 4.2.0) to prevent regression.
Supersedes #1713, #1714, #1715. Part of #1706.
Verified: 0 vulnerable copies across all advisories; build + client (535)
+ cli tests pass; lockfile internally consistent.
Claude-Session: https://claude.ai/code/session_01HiccCEh9mwCfVzE8qYcop1
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves Dependabot alert #132. Uses a version-targeted override
(@babel/core@<=7.29.0) so all copies of the vulnerable range are bumped.
Part of #1706.
Claude-Session: https://claude.ai/code/session_01HiccCEh9mwCfVzE8qYcop1
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps shell-quote from ^1.8.3 to ^1.8.4 in the root and server
packages to resolve a critical security advisory. Adds an npm
override so concurrency's pinned shell-quote@1.8.3 also resolves to
a patched version, leaving no vulnerable copy in the tree.
Claude-Session: https://claude.ai/code/session_01HiccCEh9mwCfVzE8qYcop1
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps the client's vite dev dependency from ^7.1.11 to ^7.3.5 to
resolve Dependabot security alerts.
- #123 (HIGH): vite server.fs.deny bypass, patched in 7.3.5
- #129 (MEDIUM): vulnerability in launch-editor, a transitive dep of
vite; the newer vite no longer pulls in launch-editor, clearing it
Claude-Session: https://claude.ai/code/session_01HiccCEh9mwCfVzE8qYcop1
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps vitest from ^4.0.17 to ^4.1.0 in the cli workspace to resolve
Dependabot alert #119 (CRITICAL). Resolves via vitest@4.1.10.
Claude-Session: https://claude.ai/code/session_01HiccCEh9mwCfVzE8qYcop1
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Check formatting" step in main.yml ran `npx prettier --check .`
before `npm install`, so npx fetched the LATEST prettier rather than the
version pinned in package-lock.json. This made the format check
non-deterministic and red on open PRs whenever the latest published
prettier differed from the pinned one.
- Pin prettier to exact 3.9.5 in devDependencies and update the lockfile
- Reformat the repo with the pinned prettier (expected source churn)
- Move the format check after `npm install --no-package-lock` and run it
via `npm run prettier-check` so CI uses the locally-installed pinned
prettier
Claude-Session: https://claude.ai/code/session_01HiccCEh9mwCfVzE8qYcop1
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix resource refresh button not triggering re-fetch (#1120)
The readResource function had an early return when the resource URI
was already cached, preventing the Refresh button from re-fetching.
Add a force parameter to bypass the cache on refresh, and update the
cache-hit path to still sync the displayed content when switching
between previously viewed resources.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: don't cache failed resource reads and rename force to bypassCache
Failed resource reads were cached in resourceContentMap, causing stale
errors to be served on re-click instead of retrying. Errors now only
set the display content without polluting the cache.
Also renamed the `force` boolean parameter to a `{ bypassCache }` options
object for self-documenting call sites.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: remove flaky resource-refresh e2e test
The test depends on npx downloading @modelcontextprotocol/server-everything
at runtime and uses waitForTimeout, making it unreliable in CI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: invalidate stale cache on refresh failure and rename opts to options
Clear cached resource content when a bypassCache refresh fails, so
subsequent clicks retry the fetch instead of serving stale data.
Rename the inline wrapper parameter from opts to options for consistency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: surface read errors for resource_link in Tool results
Previously, failed resource_link reads left resourceContentMap[uri]
empty, so ResourceLinkView rendered an empty expansion with no error
feedback when a read failed in the Tools tab. Track errors in a
separate resourceErrorMap and thread it through ToolsTab/ToolResults
to ResourceLinkView so the user sees the error message. The existing
cache-skip behavior is preserved (errors don't pollute the content
map, so retries still work).
Also adds a ResourcesTab test asserting the Refresh button passes
{ bypassCache: true } to readResource, to prevent regression of #1120.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: preserve cached content on refresh failure and stop writing error JSON to resourceContent
Don't delete resourceContentMap[uri] when a bypassCache refresh fails;
a transient network error should not wipe the last-known-good content.
To prevent serving stale content after a known error, the cache-hit
guard now also skips cache when resourceErrorMap[uri] is set, forcing
a re-fetch until a successful read clears the error.
Also stop writing JSON.stringify({ error }) into resourceContent in
the catch block. sendMCPRequest already sets errors.resources (which
the ResourcesTab Alert displays), and the Tools tab shows errors via
resourceErrorMap. Writing error JSON into resourceContent was both
redundant and could bleed through as JsonView content after callers
cleared errors.resources before the next read.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use presence checks instead of truthiness for resource error state
Switch from truthiness checks to presence checks in the resource
error/content handling so an empty-string error message is still
treated as an error. readResource's cache-hit guard now uses
`uri in resourceContentMap` / `!(uri in resourceErrorMap)`,
ToolResults passes `resourceError?.[item.uri]` (possibly undefined)
through to ResourceLinkView, and ResourceLinkView renders the error
branch when `resourceError !== undefined`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use hasOwnProperty instead of `in` for resource cache checks
The `in` operator also matches inherited properties like `toString`
and `constructor`, which would cause false cache hits if a server ever
returns a URI with one of those names. Switch the cache-hit guard and
the error-map cleanup to Object.prototype.hasOwnProperty.call so only
real cached entries match.
Object.hasOwn would be cleaner but requires ES2022 lib support which
the current tsconfig doesn't include.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: clear resource error on read start, not only on success
Previously, resourceErrorMap[uri] was only cleared when a read
succeeded. During an in-flight retry (e.g., after clicking Refresh on
a previously-failed resource), the stale error would remain in the
map and render in ResourceLinkView while the retry was still pending.
Clear the error entry after the cache-hit check passes, right before
sending the request, so the UI shows the last-known-good content (or
a loading state) during the retry instead of the prior error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use || instead of ?? for error message fallback
The ?? operator only falls back on null/undefined, so an Error with
an empty message (new Error("")) would produce an empty string and
render a blank error block. Switch to || so empty strings also fall
through to String(error), which produces a non-empty representation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: only sync resourceContent on cache hit from Resources tab
The cache-hit path called setResourceContent unconditionally, which
meant expanding a resource_link in the Tools tab could overwrite the
Resources tab's displayed content without updating selectedResource.
Guard the setResourceContent call with a currentTabRef check so only
cache hits from the Resources tab update the display.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per the MCP spec (2025-11-25), when a client observes input_required
status during task polling it must call tasks/result to give the server
a chance to deliver queued elicitation/sampling requests. Previously the
polling loop only called tasks/result after completed status, leaving
the server unable to trigger elicitation.
After elicitation is handled the task transitions back to working so
taskCompleted is not set — polling continues until a terminal status
is reached.
* feat: add URL-mode elicitation support
The MCP spec (2025-11-25) added URL-mode elicitation alongside form mode,
allowing servers to direct users to external URLs for out-of-band
interactions (auth flows, payments, sensitive data). This implements
client-side support for rendering and handling URL-mode elicitation
requests, and announces the capability during initialization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add consent dialog for URL elicitation and separate Accept button
Replace the clickable URL link with a consent dialog that displays the
URL as plain text when "Open URL" is clicked, letting the user confirm
before navigating. Add a separate "Accept" button that resolves the
elicitation without the content field, per the MCP spec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: preserve structured McpError data in request history
When a tool call returns an MCP error (e.g. -32042 URLElicitationRequired),
the catch block was only capturing error.message (a string), discarding the
structured error.code and error.data fields.
Now McpError instances push the full JSON-RPC error structure to history:
{ error: { code, message, data } }
This means the Response panel will show the complete error including
data.elicitations for -32042, rather than just a message string.
* Revert "fix: preserve structured McpError data in request history"
This reverts commit 86f6d093f6596d6a9a52b85df24b66fa2e354d07.
* fix: enforce HTTPS-only for URL elicitation per SEP-1036
Address Claude review on #1423:
- Validate the server-supplied URL before opening it; refuse non-https
schemes (http/file/javascript/data) and malformed URLs, disabling the
"Open URL" button and showing an error state instead of calling
window.open (SEP-1036 URL Security Requirements, a spec MUST).
- Display the parsed destination host prominently in the consent dialog
to surface look-alike/embedded-credential URLs.
- Document that elicitation/complete is surfaced via the notification
panel and resolved manually (no auto-correlation), a conscious choice
per the spec's MAY/SHOULD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: evalstate <1936278+evalstate@users.noreply.github.com>
Resolves 15 of 16 Dependabot alerts via lockfile-only updates (no
breaking major bumps): handlebars (critical), fast-uri, flatted, hono,
path-to-regexp, rollup, vite, express-rate-limit, @hono/node-server,
ajv, brace-expansion, ip-address, picomatch, postcss, qs, ws, yaml,
and @tootallnate/once.
The remaining minimatch@3.1.2 advisory comes from @eslint/config-array
(dev/lint-only, not shipped) and will clear when ESLint bumps it.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per review feedback on PR #1270: replace the three-way `==` OR chain in
each of the four trigger branches with a single contains(fromJSON(...))
check against a shared list. Same semantics, less duplication, and the
allowed-roles list is now defined once per branch instead of three times.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the workflow's `if:` only checked the event type and the
presence of `@claude` in the body. The job would start for any commenter
and only later fail when the action attempted privileged operations on
behalf of a user without write access. That produced noisy red runs and
wasted runner minutes for what is effectively an authorization rejection.
Add an `author_association` check (OWNER, MEMBER, or COLLABORATOR) to
each of the four trigger branches (issue_comment, pull_request_review_comment,
pull_request_review, issues), so the job is short-circuited at workflow
evaluation time and never starts for unauthorized invokers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous fix only added repository.url to the root package.json, but npm
publish --workspaces publishes each workspace using its own package.json.
The provenance check runs per-package, so each published workspace package
also needs repository.url to match the GitHub repo URL. Also adds type: "git"
to the root and directory fields to the workspaces for correct monorepo
linking on npm/GitHub.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Upgrade npm to ^11.5.1 in the publish job (Node 22 bundles npm 10.x,
which lacks OIDC trusted publishing support)
- Drop NODE_AUTH_TOKEN / NPM_TOKEN secret in favor of OIDC
- Enable provenance attestation via NPM_CONFIG_PROVENANCE
The publish job already had id-token: write and registry-url configured.
* fix(server): sanitize error responses to prevent stack trace exposure
Replace raw error objects passed to res.json() with generic sanitized
messages so internal error details are not leaked to clients. Full
errors continue to be logged server-side via console.error.
Also adds a missing return in the /sse ECONNREFUSED branch, which
previously fell through and attempted a second response after headers
had already been sent.
Resolves CodeQL js/stack-trace-exposure alerts in server/src/index.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(server): address review feedback on error sanitization
- Sanitize /fetch route catch block: previously leaked error.message to
clients and silently swallowed errors with no server-side logging.
Now logs via console.error and returns a generic sanitized response.
- Replace JSON.stringify(error).includes("ECONNREFUSED") in the /sse
handler with a check against error.message and String(error.cause).
Error fields are non-enumerable so JSON.stringify(new Error(...))
produces "{}" — the old check only worked by accident when the error
happened to carry enumerable properties.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ola Hungerford <olahungerford@gmail.com>
* fix: validate serverInfo.websiteUrl before rendering as link
A connected MCP server's serverInfo.websiteUrl was rendered directly
as an <a href> with no protocol check, allowing a malicious server to
supply javascript: (or other unsafe) URLs that would execute on click.
Run the value through validateRedirectUrl and, on failure, show a
warning-colored "Potentially malicious websiteURL field" message
instead of a link. The offending URL is still surfaced via tooltip
for inspection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: render websiteUrl warning below server name
Move the "Potentially malicious websiteURL field" message onto its
own row beneath the icon+name row so it doesn't crowd the server
name horizontally in the narrow sidebar.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the MCP server is at a non-root path (e.g. /mcp), path-aware
discovery (/.well-known/oauth-protected-resource/mcp) may 404 if the
server only serves metadata at the root. The SDK's direct-mode
discovery already falls back to root-level URLs in this case. This
adds the same fallback to the proxy discovery functions.
The `redirect_uris` getter used `this.redirectUrl`, which is overridden
by `DebugInspectorOAuthClientProvider` to return `/oauth/callback/debug`.
This caused the `Set` deduplication to collapse both URIs into one,
so DCR only registered `/oauth/callback/debug`. The normal connection
flow then sent `/oauth/callback` in the authorize request, which was
never registered — causing spec-compliant OAuth servers to reject it.
Use the base callback URLs directly instead of the polymorphic getter
so both URIs are always registered regardless of which subclass
initiates DCR.
Fixes#930