Skip to content

feat: mcp health checker tool#2190

Merged
mxkaske merged 6 commits into
mainfrom
feat/mcp-health
May 25, 2026
Merged

feat: mcp health checker tool#2190
mxkaske merged 6 commits into
mainfrom
feat/mcp-health

Conversation

@mxkaske

@mxkaske mxkaske commented May 23, 2026

Copy link
Copy Markdown
Member

No description provided.

@vercel

vercel Bot commented May 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openstatus-web Ready Ready Preview, Comment May 24, 2026 8:14am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
openstatus-dashboard Skipped Skipped May 24, 2026 8:14am
openstatus-status-page Skipped Skipped May 24, 2026 8:14am

Request Review

@mxkaske mxkaske changed the title feat: mcp-health feat: mcp health checker tool May 23, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 23, 2026

Copy link
Copy Markdown

Deploying openstatus-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8eaacd6
Status: ✅  Deploy successful!
Preview URL: https://cb1ec22e.openstatus-1sy.pages.dev
Branch Preview URL: https://feat-mcp-health.openstatus-1sy.pages.dev

View logs

@vercel vercel Bot temporarily deployed to Preview – openstatus-dashboard May 23, 2026 19:37 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-status-page May 23, 2026 19:37 Inactive
@openstatusHQ openstatusHQ deleted a comment from claude Bot May 23, 2026
@mxkaske

mxkaske commented May 23, 2026

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented May 23, 2026

Copy link
Copy Markdown

Claude finished @mxkaske's task in 2m 12s —— View job


Code Review: feat/mcp-health

Todo:

  • Gather context on PR changes
  • Review core MCP health check library (src/lib/mcp/health-check.ts)
  • Review API route (play/mcp-health/api/route.ts)
  • Review UI components (play/mcp-health/client.tsx, page files)
  • Review utility files and tests
  • Review supporting changes (analytics, sitemap, content)
  • Post final review feedback

Overall this is a well-structured feature. The core library is high quality: Edge-safe, good SSE parsing, proper SSRF protection, validated Redis persistence. A few things worth attention below.

Security

1. User headers can override Content-Type and Accept (health-check.ts:119–140)

In buildHeaders, the user-supplied header loop runs after setting the defaults, so any header key the user provides silently overrides Content-Type or Accept. Overriding Content-Type would break the MCP handshake silently. Consider skipping reserved keys:

const RESERVED = new Set(["content-type", "accept", "mcp-session-id", "mcp-protocol-version"]);
for (const { key, value } of extra ?? []) {
  if (!key || RESERVED.has(key.toLowerCase())) continue;
  headers.set(key, value);
}

Fix this →

2. No max-length validation on custom header values (api/route.ts:21–27)

requestSchema constrains headers to .max(20) rows but doesn't limit the key or value length. A single long value could inflate the request body significantly. Consider adding .max(256) on both:

z.array(z.object({ key: z.string().max(256), value: z.string().max(2048) })).max(20)

Fix this →

3. Self-request guard is too narrow (api/route.ts:88–97)

urlObject.hostname.toLowerCase().endsWith("openstatus.dev") &&
urlObject.pathname.startsWith("/play/mcp-health/api")

This only blocks self-calls to the MCP API path. Any other path on openstatus.dev (e.g. other API routes) is still reachable. Since assertSafeUrlSync handles SSRF for private IPs, this guard's real purpose is loop prevention — checking just the hostname and this endpoint is sufficient, but documenting that intent would prevent future confusion.


Logic

4. Latency timestamps drift in parallel steps (health-check.ts:686–687)

const pingStarted = Date.now();
const toolsStarted = Date.now();

Both timestamps are captured synchronously before the parallel requests start, so they measure effectively the same start point. This is fine functionally — the latencies will both be measured from launch time. A minor note: if one wanted the latency to reflect time-to-first-byte of each request independently, you'd need to instrument inside sendRpc. Current behavior is documented by the parallel execution intent.

5. resolveAuthChallenge ignores outer cancellation (health-check.ts:648–656, 818–820)

Both call sites pass new AbortController().signal — a fresh, never-aborted signal. This means metadata fetches inside resolveAuthChallenge are not cancellable by the outer step controller, even though METADATA_FETCH_TIMEOUT_MS already limits them. This is acceptable given the timeout, but worth noting.


UX / Minor

6. SampleHint mutates a DOM node directly (client.tsx:287–290)

const input = document.querySelector<HTMLInputElement>("input[name='url']");
if (input) input.value = url;

Querying by name='url' works, but silently fails if the input is ever renamed or moved outside the current <form> scope. Since the <Form> is the only consumer today this is fine, but a useRef wired from the form to the hint would be more robust.

7. Rate limit is quite tight (api/route.ts:18–19)

3 requests / 60 seconds per IP is very restrictive — a developer who checks their own MCP server twice and then immediately tests a second one is already rate-limited. Worth considering a slightly wider window (e.g. 5–10 per minute) for a public dev tool.

8. No aria-label on status dots (client.tsx:677–679)

function StatusDot({ input }: { input: boolean | null | StepResult }) {
  return <div className={cn("size-4", statusDotClass(input))} />;
}

Color-only indicators have no accessible label. Screen readers won't know the step's status. Adding aria-label={step.ok ? "ok" : step.error?.code ?? "failed"} would cover this.


What's solid

  • SSE parsing (readSseUntilId) handles the SSE_SCAN_LIMIT vs RAW_BODY_CAP distinction correctly — large tools/list payloads don't break frame matching.
  • normalizeUrlForStorage strips credentials and query params before persistence — good for privacy.
  • toPersistedReport validates with the schema before Redis write; malformed reports are soft-failed.
  • assertSafeUrlSync guards both the MCP target URL and the OAuth metadata URL fetched during challenge resolution.
  • protocol.ts sharing the request bodies between server and client is a clean pattern — display can't drift from what's sent.
  • Test coverage is thorough for the pure utility functions (parseBearerChallenge, normalizeUrlForStorage, formatResponseBody).

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

4 issues found across 24 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/src/app/(landing)/play/mcp-health/api/route.ts">

<violation number="1" location="apps/web/src/app/(landing)/play/mcp-health/api/route.ts:79">
P1: The SSRF check only validates the hostname string and can be bypassed with domains that resolve to private/internal IPs.</violation>
</file>

<file name="apps/web/src/app/(landing)/play/mcp-health/[id]/page.tsx">

<violation number="1" location="apps/web/src/app/(landing)/play/mcp-health/[id]/page.tsx:69">
P2: Use `notFound()` instead of redirecting when the health report does not exist.

(Based on your team's feedback about Use framework-provided notFound handling in Next.js.) [FEEDBACK_USED]</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic


try {
// protocol check + private/loopback/metadata-host block. Throws on bad input.
assertSafeUrlSync(parsed.url);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: The SSRF check only validates the hostname string and can be bypassed with domains that resolve to private/internal IPs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/app/(landing)/play/mcp-health/api/route.ts, line 79:

<comment>The SSRF check only validates the hostname string and can be bypassed with domains that resolve to private/internal IPs.</comment>

<file context>
@@ -0,0 +1,193 @@
+
+  try {
+    // protocol check + private/loopback/metadata-host block. Throws on bad input.
+    assertSafeUrlSync(parsed.url);
+  } catch (err) {
+    return errorResponse(
</file context>

Comment thread apps/web/src/lib/mcp/health-check.ts Outdated
const page = getToolsPage("mcp-health-slug");

const data = await getHealthReportById(id);
if (!data) redirect("/play/mcp-health");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Use notFound() instead of redirecting when the health report does not exist.

(Based on your team's feedback about Use framework-provided notFound handling in Next.js.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/app/(landing)/play/mcp-health/[id]/page.tsx, line 69:

<comment>Use `notFound()` instead of redirecting when the health report does not exist.

(Based on your team's feedback about Use framework-provided notFound handling in Next.js.) </comment>

<file context>
@@ -0,0 +1,97 @@
+  const page = getToolsPage("mcp-health-slug");
+
+  const data = await getHealthReportById(id);
+  if (!data) redirect("/play/mcp-health");
+
+  const jsonLDGraph = createJsonLDGraph([
</file context>

Comment thread apps/web/src/lib/mcp/health-check.ts
@vercel vercel Bot temporarily deployed to Preview – openstatus-status-page May 24, 2026 07:36 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-dashboard May 24, 2026 07:36 Inactive

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/src/lib/mcp/health-check.ts">

<violation number="1" location="apps/web/src/lib/mcp/health-check.ts:257">
P1: Manual redirect following forwards the same headers to redirected hosts, which can leak sensitive user-supplied headers (such as Authorization) across origins.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment on lines +257 to +258
target = new URL(location, target).toString();
assertSafeUrlSync(target);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Manual redirect following forwards the same headers to redirected hosts, which can leak sensitive user-supplied headers (such as Authorization) across origins.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/lib/mcp/health-check.ts, line 257:

<comment>Manual redirect following forwards the same headers to redirected hosts, which can leak sensitive user-supplied headers (such as Authorization) across origins.</comment>

<file context>
@@ -223,6 +234,32 @@ async function readSseUntilId(
+    if (res.status < 300 || res.status >= 400) return res;
+    const location = res.headers.get("location");
+    if (!location) return res;
+    target = new URL(location, target).toString();
+    assertSafeUrlSync(target);
+  }
</file context>
Suggested change
target = new URL(location, target).toString();
assertSafeUrlSync(target);
const next = new URL(location, target);
if (next.origin !== new URL(target).origin) {
throw new Error("refusing cross-origin redirect");
}
target = next.toString();
assertSafeUrlSync(target);

@vercel vercel Bot temporarily deployed to Preview – openstatus-dashboard May 24, 2026 07:51 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-status-page May 24, 2026 07:51 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-dashboard May 24, 2026 08:12 Inactive
@vercel vercel Bot temporarily deployed to Preview – openstatus-status-page May 24, 2026 08:12 Inactive
@mxkaske mxkaske merged commit b7026ef into main May 25, 2026
13 of 14 checks passed
@mxkaske mxkaske deleted the feat/mcp-health branch May 25, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant