Skip to content

Add clickable server primitive inspection to show servers#1937

Merged
chelojimenez merged 4 commits intomainfrom
codex/ui-tool
Apr 26, 2026
Merged

Add clickable server primitive inspection to show servers#1937
chelojimenez merged 4 commits intomainfrom
codex/ui-tool

Conversation

@chelojimenez
Copy link
Copy Markdown
Contributor

@chelojimenez chelojimenez commented Apr 26, 2026

Summary

  • Extend show_servers payloads with compact tool, resource, and prompt summaries.
  • Make server cards clickable and render a master-detail view for the selected server.
  • Regenerate the bundled MCP app HTML and add coverage for the new inspection data.

Testing

  • npm run test -w @mcpjam/mcp -- showServersCore
  • npm run typecheck -w @mcpjam/mcp
  • npm run test -w @mcpjam/mcp
  • git diff --check

Note

Medium Risk
Adds new server-inspection data to the show_servers payload and new UI navigation to drill into per-server details, which could affect tool output compatibility and increase probe/doctor call surface area.

Overview
Enhances show_servers to optionally inspect reachable HTTPS servers and return compact tools, resources, and prompts summaries (with per-collection loaded/skipped/error status and details) alongside existing reachability data.

Updates the bundled show-servers UI to make server cards clickable and present a master-detail view that renders the inspected primitives (plus copy-URL and richer status messaging), and introduces a reusable ServersLoadingSkeleton component for loading states.

Extends test coverage in showServersCore.test.ts to validate the new inspection-derived payload fields and existing probe/authorization outcomes.

Reviewed by Cursor Bugbot for commit d3c3a2c. Bugbot is set up for automated code reviews on this repo. Configure here.

@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Apr 26, 2026
@chelojimenez
Copy link
Copy Markdown
Contributor Author

chelojimenez commented Apr 26, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@dosubot dosubot Bot added the enhancement New feature or request label Apr 26, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 26, 2026

MCP worker preview

Preview worker mcpjam-mcp-pr-1937 deleted — the preview URL no longer resolves.
Merged changes are live on mcpjam-mcp-staging via deploy-mcp-staging.yml.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

Walkthrough

Adds typed server primitives (tools, resources, prompts) with per-collection load status and an optional primitives?: ServerPrimitives field on ServerEntry. Introduces an inspection pathway: buildShowServersPayload can accept an inspect callback; a new InspectMcpServerResult type and an exported inspectMcpServer implementation return primitive lists with per-primitive checks. UI changes add server selection, a ServerDetailScreen showing grouped primitives via a generic PrimitiveDropdown, a loading skeleton component, and tests that stub and assert inspection-derived primitives.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
mcp/src/tools/showServersCore.ts (2)

466-485: clientName quietly disappears on the inspect path.

probeMcpServer receives clientName: "mcpjam-show-servers", but inspectMcpServer builds its HttpServerConfig without forwarding it, so server logs that key off client identity will see a different (or default) name when inspection is on. Minor, but worth aligning if observability on the server side relies on this.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/tools/showServersCore.ts` around lines 466 - 485, The inspect path is
not preserving the client identity so server-side logs differ; ensure the same
clientName ("mcpjam-show-servers") is forwarded to inspection by passing the
full probeConfig (including clientName) into inspect or by mapping
probeConfig.clientName into the HttpServerConfig that inspect/inspectMcpServer
expects. Update the call site where inspect(probeConfig) is invoked (and/or
adjust inspectMcpServer to accept/forward clientName) so inspect uses the same
clientName as probe (probeConfig → inspect), preserving observability
consistency.

207-208: Default behavior just got a lot heavier — confirm this is intended for every show_servers call.

When the caller omits both probe and inspect, you now opt every workspace probe into the full doctor pipeline (probe → connect → list tools → list resources → list prompts). For workspaces with many servers this multiplies request fan-out and tail latency, and any flaky server is now five-ish calls’ worth of opportunity to time out instead of one. If the lightweight probeMcpServer path is still acceptable for callers that don't need primitives (e.g. the legacy text-only renderer), consider gating inspection behind an explicit input flag rather than auto-upgrading the default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/tools/showServersCore.ts` around lines 207 - 208, Currently
inspectWithDefault auto-upgrades omitted inspect/probe calls into the heavy
inspectMcpServer path; change that so inspection is only enabled when explicitly
requested: remove the implicit fallback and set inspectWithDefault = inspect ??
undefined (or check an explicit boolean flag like enableInspect and only use
inspectMcpServer when that flag is true), updating callers of show_servers / the
probe call site to pass the new enableInspect flag if they want the full doctor
pipeline; reference inspectWithDefault, inspect, probe, probeMcpServer, and
inspectMcpServer when making the change.
mcp/src/ui/show-servers.tsx (2)

310-339: Heading inside <button> collapses heading semantics.

<h2> is flow content and is not valid as a <button> descendant — assistive tech and heading-navigation tooling will lose the heading anchor for each server. Consider rendering the server name as a <span> (or moving the <h2> outside the clickable region and labelling the button via aria-labelledby) so the inventory remains heading-navigable.

♻️ Minimal swap to keep semantics intact
-              <h2 className="truncate text-sm font-semibold text-foreground">
+              <span className="truncate text-sm font-semibold text-foreground">
                 {server.name}
-              </h2>
+              </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/ui/show-servers.tsx` around lines 310 - 339, The server list item
uses an <h2> (server.name) inside the clickable <button>, which breaks heading
semantics; either move the heading outside the button and reference it from the
button via aria-labelledby, or replace the <h2> with a non-heading inline
element (e.g., <span>) and keep the heading outside for navigability. Update the
JSX around the button that renders server.name (currently the <h2> in the
component that also uses props/vars like selected, onSelect, displayUrl,
StatusDot, STATUS_LABELS, server.status) so that headings remain real headings
in the document flow—if you keep the heading outside, give the button an
aria-labelledby pointing to the heading id; if you swap to a <span>, remove the
<h2> and keep the accessible labeling via aria-label or aria-labelledby as
appropriate.

287-301: stopPropagation here is decorative.

The copy button is now a sibling of the select button rather than nested inside it, so a click on copy never reaches onSelect to begin with. The call is harmless, just noise — feel free to drop it (and event: MouseEvent<HTMLButtonElement>) if you want a tidier signature.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/ui/show-servers.tsx` around lines 287 - 301, The copyUrl handler
contains an unnecessary stopPropagation and event parameter because the copy
button is now a sibling to the select button; remove the MouseEvent parameter
from the copyUrl signature and delete the event.stopPropagation() call inside
copyUrl, and update any call sites to invoke copyUrl() with no argument; keep
the rest of the logic (clipboard write, setCopied state, timeout, and catch)
unchanged and ensure the function name copyUrl is used to locate the handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@mcp/src/tools/showServersCore.ts`:
- Around line 466-485: The inspect path is not preserving the client identity so
server-side logs differ; ensure the same clientName ("mcpjam-show-servers") is
forwarded to inspection by passing the full probeConfig (including clientName)
into inspect or by mapping probeConfig.clientName into the HttpServerConfig that
inspect/inspectMcpServer expects. Update the call site where
inspect(probeConfig) is invoked (and/or adjust inspectMcpServer to
accept/forward clientName) so inspect uses the same clientName as probe
(probeConfig → inspect), preserving observability consistency.
- Around line 207-208: Currently inspectWithDefault auto-upgrades omitted
inspect/probe calls into the heavy inspectMcpServer path; change that so
inspection is only enabled when explicitly requested: remove the implicit
fallback and set inspectWithDefault = inspect ?? undefined (or check an explicit
boolean flag like enableInspect and only use inspectMcpServer when that flag is
true), updating callers of show_servers / the probe call site to pass the new
enableInspect flag if they want the full doctor pipeline; reference
inspectWithDefault, inspect, probe, probeMcpServer, and inspectMcpServer when
making the change.

In `@mcp/src/ui/show-servers.tsx`:
- Around line 310-339: The server list item uses an <h2> (server.name) inside
the clickable <button>, which breaks heading semantics; either move the heading
outside the button and reference it from the button via aria-labelledby, or
replace the <h2> with a non-heading inline element (e.g., <span>) and keep the
heading outside for navigability. Update the JSX around the button that renders
server.name (currently the <h2> in the component that also uses props/vars like
selected, onSelect, displayUrl, StatusDot, STATUS_LABELS, server.status) so that
headings remain real headings in the document flow—if you keep the heading
outside, give the button an aria-labelledby pointing to the heading id; if you
swap to a <span>, remove the <h2> and keep the accessible labeling via
aria-label or aria-labelledby as appropriate.
- Around line 287-301: The copyUrl handler contains an unnecessary
stopPropagation and event parameter because the copy button is now a sibling to
the select button; remove the MouseEvent parameter from the copyUrl signature
and delete the event.stopPropagation() call inside copyUrl, and update any call
sites to invoke copyUrl() with no argument; keep the rest of the logic
(clipboard write, setCopied state, timeout, and catch) unchanged and ensure the
function name copyUrl is used to locate the handler.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 95ed4c92-8a99-41f0-9ccb-befda9ee8d26

📥 Commits

Reviewing files that changed from the base of the PR and between 740089b and aea51a5.

⛔ Files ignored due to path filters (1)
  • mcp/src/generated/McpAppsHtml.bundled.ts is excluded by !**/generated/**
📒 Files selected for processing (4)
  • mcp/src/shared/show-servers.ts
  • mcp/src/tools/showServersCore.ts
  • mcp/src/ui/show-servers.tsx
  • mcp/tests/showServersCore.test.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
mcp/src/ui/show-servers.tsx (3)

467-540: A small accessibility courtesy for the disclosure pattern.

PrimitiveDropdown is a textbook disclosure widget — aria-expanded is wired up correctly and the chevron rotates in sympathy. Two refinements would round it out for assistive tech:

  • Pair the trigger and panel with aria-controls / id so screen readers can announce the relationship.
  • Mark the chevron as aria-hidden since its meaning is already conveyed by aria-expanded.

Neither blocks the feature; they simply tighten the semantics now that the primitives surface is non-trivial.

♻️ Suggested tightening
 function PrimitiveDropdown<TItem>({
   label,
   icon: Icon,
   collection,
   emptyLabel,
   getKey,
   renderItem,
 }: {
   label: string;
   icon: ComponentType<{ className?: string }>;
   collection: ServerPrimitiveCollection<TItem>;
   emptyLabel: string;
   getKey: (item: TItem) => string;
   renderItem: (item: TItem) => ReactNode;
 }) {
   const [open, setOpen] = useState(false);
   const hasItems = collection.items.length > 0;
+  const panelId = useId();
@@
       <button
         type="button"
         onClick={() => setOpen((value) => !value)}
         aria-expanded={open}
+        aria-controls={panelId}
         className="flex w-full cursor-pointer items-center justify-between gap-3 p-4 text-left transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
       >
@@
           <ChevronDown
+            aria-hidden="true"
             className={cn(
               "h-4 w-4 text-muted-foreground transition-transform",
               open && "rotate-180"
             )}
           />
@@
       {open ? (
-        <div className="border-t border-border/60 px-4 py-2">
+        <div id={panelId} className="border-t border-border/60 px-4 py-2">

(Don't forget to add useId to the React import.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/ui/show-servers.tsx` around lines 467 - 540, The disclosure button in
PrimitiveDropdown should explicitly link to its panel and hide the decorative
chevron for screen readers: generate a stable id with useId inside
PrimitiveDropdown, set that id on the panel container (the div rendered when
open), add aria-controls={panelId} to the button alongside the existing
aria-expanded, and mark the ChevronDown icon aria-hidden="true"; keep the
existing open state and class rotation logic unchanged.

241-263: Selection-reset effect is sound; a one-liner of derivation would be even tidier.

The effect correctly clears selectedServerId when the previously selected server disappears from the payload (e.g., after a workspace refresh). It does the job and avoids stale-detail-screen states.

If you'd like to shave the extra render that the effect introduces, the same outcome can be achieved by deriving the selected server first and only short-circuiting to the detail screen when it actually exists:

♻️ Optional simplification
-  const [selectedServerId, setSelectedServerId] = useState<string | undefined>(
-    undefined
-  );
-
-  useEffect(() => {
-    if (
-      selectedServerId === undefined ||
-      payload.servers.some((server) => server.id === selectedServerId)
-    ) {
-      return;
-    }
-
-    setSelectedServerId(undefined);
-  }, [payload.servers, selectedServerId]);
-
-  const selectedServer = payload.servers.find(
-    (server) => server.id === selectedServerId
-  );
-
-  if (selectedServer) {
+  const [selectedServerId, setSelectedServerId] = useState<string | undefined>(
+    undefined
+  );
+
+  const selectedServer =
+    selectedServerId !== undefined
+      ? payload.servers.find((server) => server.id === selectedServerId)
+      : undefined;
+
+  if (selectedServer) {

That keeps the back action functional and lets a stale id naturally fall through to the grid without a second render pass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/ui/show-servers.tsx` around lines 241 - 263, The useEffect that
clears selectedServerId when the server disappears causes an extra render;
instead remove that effect and derive the selectedServer directly from
payload.servers (using payload.servers.find(server => server.id ===
selectedServerId)), then short-circuit to render ServerDetailScreen only when
selectedServer is truthy; keep the onBack handler calling
setSelectedServerId(undefined) so the back action continues to work and allow a
stale id to naturally fall through to the grid without an extra effect-induced
render.

326-374: Two stacked buttons in shared territory — defensible, but the copy control deserves a hand-off.

The copy button is now a sibling of the card-level select button rather than a child, which sidesteps the invalid-nested-<button> HTML pitfall — nicely done. Two small polish items worth considering:

  1. event.stopPropagation() in copyUrl is benign but unnecessary, since the copy button is no longer nested inside onSelect's button — clicks never bubble through the select handler. Safe to keep as a defensive belt-and-suspenders, but noting it for the next reader who wonders why it's there.
  2. The absolutely-positioned copy button visually overlays the URL block of the card-wide button. With position: absolute but no explicit z-index, paint order alone keeps it clickable; if a future tweak introduces stacking contexts on the inner button, the copy affordance could become inert. A defensive z-10 (or equivalent) on the copy button would future-proof it.
♻️ Optional hardening
       <button
         aria-label={`Copy URL for ${server.name}`}
         title={`Copy URL for ${server.name}`}
         type="button"
         onClick={copyUrl}
-        className="absolute bottom-5 right-5 cursor-pointer p-1 text-muted-foreground/60 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
+        className="absolute bottom-5 right-5 z-10 cursor-pointer p-1 text-muted-foreground/60 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/ui/show-servers.tsx` around lines 326 - 374, The copy button sits
alongside the card-level onSelect button so stopPropagation in copyUrl is
unnecessary and the absolute-positioned copy control needs an explicit stacking
context; remove the defensive event.stopPropagation() call inside the copyUrl
handler and add a high z-index utility (e.g., "z-10") to the copy button element
(the button that uses aria-label/ title `Copy URL for ${server.name}`) to ensure
it remains clickable even if inner elements gain stacking contexts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mcp/src/ui/show-servers.tsx`:
- Around line 520-535: The empty-state message always shows emptyLabel even when
collection.status indicates a failure; update the JSX inside the expanded panel
(the block that currently renders emptyLabel) to check collection.status and,
when status is "skipped" or "error" and a collection.statusDetail exists, render
that statusDetail (or a small status-aware fallback message) instead of
emptyLabel; keep existing behavior for successful/empty runs, and use the same
rendering conventions as renderItem/getKey so styling and semantics remain
consistent.

---

Nitpick comments:
In `@mcp/src/ui/show-servers.tsx`:
- Around line 467-540: The disclosure button in PrimitiveDropdown should
explicitly link to its panel and hide the decorative chevron for screen readers:
generate a stable id with useId inside PrimitiveDropdown, set that id on the
panel container (the div rendered when open), add aria-controls={panelId} to the
button alongside the existing aria-expanded, and mark the ChevronDown icon
aria-hidden="true"; keep the existing open state and class rotation logic
unchanged.
- Around line 241-263: The useEffect that clears selectedServerId when the
server disappears causes an extra render; instead remove that effect and derive
the selectedServer directly from payload.servers (using
payload.servers.find(server => server.id === selectedServerId)), then
short-circuit to render ServerDetailScreen only when selectedServer is truthy;
keep the onBack handler calling setSelectedServerId(undefined) so the back
action continues to work and allow a stale id to naturally fall through to the
grid without an extra effect-induced render.
- Around line 326-374: The copy button sits alongside the card-level onSelect
button so stopPropagation in copyUrl is unnecessary and the absolute-positioned
copy control needs an explicit stacking context; remove the defensive
event.stopPropagation() call inside the copyUrl handler and add a high z-index
utility (e.g., "z-10") to the copy button element (the button that uses
aria-label/ title `Copy URL for ${server.name}`) to ensure it remains clickable
even if inner elements gain stacking contexts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2760b05b-1a7e-4c3b-8c68-fbc485c52397

📥 Commits

Reviewing files that changed from the base of the PR and between aea51a5 and f36a68f.

⛔ Files ignored due to path filters (1)
  • mcp/src/generated/McpAppsHtml.bundled.ts is excluded by !**/generated/**
📒 Files selected for processing (1)
  • mcp/src/ui/show-servers.tsx

Comment on lines +520 to +535
{open ? (
<div className="border-t border-border/60 px-4 py-2">
{hasItems ? (
<ul>
{collection.items.map((item) => (
<li
key={getKey(item)}
className="border-t border-border/50 py-3 first:border-t-0"
>
{renderItem(item)}
</li>
))}
</ul>
) : (
<p className="py-3 text-sm text-muted-foreground">{emptyLabel}</p>
)}
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.

⚠️ Potential issue | 🟡 Minor

The empty-state copy ignores why the list is empty.

When collection.status is "skipped" or "error" and items is empty, the panel still renders the cheerful "No tools discovered." line. The header already shows a status badge and (optionally) statusDetail, but inside the expanded panel the user gets a message that contradicts the failure state. A small tweak — surfacing the status detail or a status-aware empty message — keeps the disclosure honest when inspection didn't actually run.

🐛 Suggested status-aware empty state
-          {hasItems ? (
+          {hasItems ? (
             <ul>
               {collection.items.map((item) => (
                 <li
                   key={getKey(item)}
                   className="border-t border-border/50 py-3 first:border-t-0"
                 >
                   {renderItem(item)}
                 </li>
               ))}
             </ul>
           ) : (
-            <p className="py-3 text-sm text-muted-foreground">{emptyLabel}</p>
+            <p className="py-3 text-sm text-muted-foreground">
+              {collection.status === "loaded"
+                ? emptyLabel
+                : (collection.statusDetail ??
+                  `${PRIMITIVE_STATUS_LABELS[collection.status]} — no items available.`)}
+            </p>
           )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/ui/show-servers.tsx` around lines 520 - 535, The empty-state message
always shows emptyLabel even when collection.status indicates a failure; update
the JSX inside the expanded panel (the block that currently renders emptyLabel)
to check collection.status and, when status is "skipped" or "error" and a
collection.statusDetail exists, render that statusDetail (or a small
status-aware fallback message) instead of emptyLabel; keep existing behavior for
successful/empty runs, and use the same rendering conventions as
renderItem/getKey so styling and semantics remain consistent.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 26, 2026

Internal preview

Preview URL: https://mcp-inspector-pr-1937.up.railway.app
Deployed commit: 7cddb9e
PR head commit: d3c3a2c
Backend target: staging fallback.
Health: ❌ Convex unreachable — see upsert-preview job logs (staging may need convex deploy)
Access is employee-only in non-production environments.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
mcp/src/ui/show-servers.tsx (4)

311-325: event.stopPropagation() is now ornamental.

The copy-URL <button> is a sibling of the card's selection <button>, not a descendant — clicks no longer bubble through one to the other. The stopPropagation call is harmless but misleading to future readers who may infer a nested-interactive layout that no longer exists.

♻️ Drop the no-op
-  const copyUrl = async (event: MouseEvent<HTMLButtonElement>) => {
-    event.stopPropagation();
-
+  const copyUrl = async () => {
     if (!server.url) {
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/ui/show-servers.tsx` around lines 311 - 325, Remove the misleading
no-op stopPropagation call in the copyUrl handler: edit the copyUrl function to
delete the event.stopPropagation() line (since the copy button is a sibling of
the card selection button and clicks do not bubble into it). Keep the rest of
copyUrl intact (navigator.clipboard.writeText, setCopied(true), timeout fallback
and catch) so behavior is unchanged but the code no longer implies a nested
interactive layout.

488-519: Tighten the disclosure semantics with aria-controls.

The toggle exposes aria-expanded but doesn't point assistive tech at the panel it governs. A stable id wired to aria-controls rounds out the disclosure pattern:

♻️ Wire up aria-controls
 function PrimitiveDropdown<TItem>({
   ...
 }) {
   const [open, setOpen] = useState(false);
+  const panelId = useId();
   const hasItems = collection.items.length > 0;

   return (
     <section className="overflow-hidden rounded-lg border border-border/60 bg-card/60">
       <button
         type="button"
         onClick={() => setOpen((value) => !value)}
         aria-expanded={open}
+        aria-controls={panelId}
         className="..."
       >
         ...
       </button>

       {open ? (
-        <div className="border-t border-border/60 px-4 py-2">
+        <div id={panelId} className="border-t border-border/60 px-4 py-2">
           ...
         </div>
       ) : null}
     </section>
   );
 }

(Add useId to the existing react import.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/ui/show-servers.tsx` around lines 488 - 519, The button toggle
exposes aria-expanded but lacks aria-controls; import useId from React, generate
a stable id (e.g., const panelId = useId()) near the component top, add
aria-controls={panelId} to the button that uses setOpen/open, and ensure the
corresponding panel element (the collapsible container that shows collection
items / uses Badge/PRIMITIVE_STATUS_LABELS) has id={panelId} so assistive tech
can associate the control with the panel.

238-264: Selection-reset effect: correct but a touch eager.

The reset effect re-runs on every new payload.servers reference, even when the selected server is still present, which is harmless but wasteful. Computing selectedServer first and reacting to its absence avoids the extra work and reads slightly cleaner:

♻️ Derive then reconcile
-  const [selectedServerId, setSelectedServerId] = useState<string | undefined>(
-    undefined
-  );
-
-  useEffect(() => {
-    if (
-      selectedServerId === undefined ||
-      payload.servers.some((server) => server.id === selectedServerId)
-    ) {
-      return;
-    }
-
-    setSelectedServerId(undefined);
-  }, [payload.servers, selectedServerId]);
-
-  const selectedServer = payload.servers.find(
-    (server) => server.id === selectedServerId
-  );
+  const [selectedServerId, setSelectedServerId] = useState<string | undefined>(
+    undefined
+  );
+
+  const selectedServer =
+    selectedServerId !== undefined
+      ? payload.servers.find((server) => server.id === selectedServerId)
+      : undefined;
+
+  useEffect(() => {
+    if (selectedServerId !== undefined && !selectedServer) {
+      setSelectedServerId(undefined);
+    }
+  }, [selectedServer, selectedServerId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/ui/show-servers.tsx` around lines 238 - 264, Compute selectedServer
before the effect and make the effect only reconcile when that computed value is
missing: move the selectedServer = payload.servers.find(...) above the
useEffect, then change the useEffect to depend on selectedServer (and remove
payload.servers from deps) and call setSelectedServerId(undefined) only when
selectedServer is undefined and selectedServerId is not; update references to
selectedServerId, selectedServer, payload.servers, useEffect, and
setSelectedServerId accordingly.

82-97: Simplify by passing just hostContext to avoid unnecessary fallback calls.

The hostContext state is seeded from app.getHostContext() in the effect below (line 105), making the per-render fallback redundant. While the hook's internal useRef guard prevents problematic re-applications, passing a potentially fresh object on each render is unnecessary. Just pass the state value directly.

♻️ Drop the per-render fallback
-  useHostStyles(app, hostContext ?? app?.getHostContext());
+  useHostStyles(app, hostContext);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/ui/show-servers.tsx` around lines 82 - 97, The code passes
useHostStyles(app, hostContext ?? app?.getHostContext()) which redundantly falls
back to app?.getHostContext() each render; change it to call useHostStyles(app,
hostContext) so the hook receives the hostContext state directly (remove the
per-render fallback expression), ensuring useHostStyles, hostContext, and
app.getHostContext() are the referenced symbols to locate the change.
mcpjam-inspector/client/src/components/ServersTab.tsx (1)

1485-1485: Tidy reuse of the shared skeleton — LGTM.

A clean swap to the shared ServersLoadingSkeleton. One soft suggestion: if any tests or telemetry rely on identifying the loading container, consider passing a data-testid (akin to how show-servers.tsx calls it) to keep the loading state addressable.

-  const renderLoadingContent = () => <ServersLoadingSkeleton />;
+  const renderLoadingContent = () => (
+    <ServersLoadingSkeleton data-testid="servers-tab-loading" />
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcpjam-inspector/client/src/components/ServersTab.tsx` at line 1485, The
loading container lacks a test identifier; update the renderLoadingContent usage
of ServersLoadingSkeleton to pass a data-testid prop (matching the pattern used
in show-servers.tsx) so tests/telemetry can target the loading state—locate the
renderLoadingContent arrow function and add the data-testid attribute to the
ServersLoadingSkeleton element.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@design-system/src/components/servers-loading-skeleton.tsx`:
- Around line 1-8: The file is using React.ComponentProps<"div"> in the
ServersLoadingSkeletonProps interface but React is not imported; add the
namespace import "import * as React from 'react';" (or the project convention
equivalent) at the top of the file so React is in scope for the
React.ComponentProps type used in ServersLoadingSkeletonProps and any other type
references in this module.

---

Nitpick comments:
In `@mcp/src/ui/show-servers.tsx`:
- Around line 311-325: Remove the misleading no-op stopPropagation call in the
copyUrl handler: edit the copyUrl function to delete the event.stopPropagation()
line (since the copy button is a sibling of the card selection button and clicks
do not bubble into it). Keep the rest of copyUrl intact
(navigator.clipboard.writeText, setCopied(true), timeout fallback and catch) so
behavior is unchanged but the code no longer implies a nested interactive
layout.
- Around line 488-519: The button toggle exposes aria-expanded but lacks
aria-controls; import useId from React, generate a stable id (e.g., const
panelId = useId()) near the component top, add aria-controls={panelId} to the
button that uses setOpen/open, and ensure the corresponding panel element (the
collapsible container that shows collection items / uses
Badge/PRIMITIVE_STATUS_LABELS) has id={panelId} so assistive tech can associate
the control with the panel.
- Around line 238-264: Compute selectedServer before the effect and make the
effect only reconcile when that computed value is missing: move the
selectedServer = payload.servers.find(...) above the useEffect, then change the
useEffect to depend on selectedServer (and remove payload.servers from deps) and
call setSelectedServerId(undefined) only when selectedServer is undefined and
selectedServerId is not; update references to selectedServerId, selectedServer,
payload.servers, useEffect, and setSelectedServerId accordingly.
- Around line 82-97: The code passes useHostStyles(app, hostContext ??
app?.getHostContext()) which redundantly falls back to app?.getHostContext()
each render; change it to call useHostStyles(app, hostContext) so the hook
receives the hostContext state directly (remove the per-render fallback
expression), ensuring useHostStyles, hostContext, and app.getHostContext() are
the referenced symbols to locate the change.

In `@mcpjam-inspector/client/src/components/ServersTab.tsx`:
- Line 1485: The loading container lacks a test identifier; update the
renderLoadingContent usage of ServersLoadingSkeleton to pass a data-testid prop
(matching the pattern used in show-servers.tsx) so tests/telemetry can target
the loading state—locate the renderLoadingContent arrow function and add the
data-testid attribute to the ServersLoadingSkeleton element.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f1e17e78-e0d9-40b9-9b7c-9d40d2258f2e

📥 Commits

Reviewing files that changed from the base of the PR and between f36a68f and 717be29.

⛔ Files ignored due to path filters (1)
  • mcp/src/generated/McpAppsHtml.bundled.ts is excluded by !**/generated/**
📒 Files selected for processing (3)
  • design-system/src/components/servers-loading-skeleton.tsx
  • mcp/src/ui/show-servers.tsx
  • mcpjam-inspector/client/src/components/ServersTab.tsx

Comment on lines +1 to +8
import { cn } from "../cn";
import { Skeleton } from "./skeleton";

interface ServersLoadingSkeletonProps extends React.ComponentProps<"div"> {
cardClassName?: string;
cardCount?: number;
gridClassName?: string;
}
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether React is globally typed in the design-system package or imported elsewhere in the file's neighbors.
fd -t f 'tsconfig*.json' design-system | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"'
echo "--- sibling component imports ---"
fd -t f -e tsx -e ts . design-system/src/components | head -20 | xargs -I{} sh -c 'echo "== {} =="; head -5 "{}"'

Repository: MCPJam/inspector

Length of output: 4712


🏁 Script executed:

# Check the full servers-loading-skeleton.tsx file
cat design-system/src/components/servers-loading-skeleton.tsx

# Check aspect-ratio and collapsible for their full import statements
echo "=== aspect-ratio.tsx imports ==="
head -15 design-system/src/components/aspect-ratio.tsx

echo "=== collapsible.tsx imports ==="
head -15 design-system/src/components/collapsible.tsx

Repository: MCPJam/inspector

Length of output: 1714


Add missing React import for type reference.

React.ComponentProps<"div"> requires React to be in scope. Add the import at the top to match the codebase convention:

♻️ Add namespace import
+import * as React from "react";
 import { cn } from "../cn";
 import { Skeleton } from "./skeleton";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@design-system/src/components/servers-loading-skeleton.tsx` around lines 1 -
8, The file is using React.ComponentProps<"div"> in the
ServersLoadingSkeletonProps interface but React is not imported; add the
namespace import "import * as React from 'react';" (or the project convention
equivalent) at the top of the file so React is in scope for the
React.ComponentProps type used in ServersLoadingSkeletonProps and any other type
references in this module.

@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Apr 26, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
mcpjam-inspector/client/src/components/chat-v2/thread/mcp-apps/mcp-apps-renderer.tsx (1)

836-885: Unconditionally setting theme: resolvedTheme is the right move.

Dropping the previous "preserve baseHostContext.theme if it's literally light/dark" branch is the correct simplification — that branch is now subsumed by configuredHostTheme flowing through resolvedTheme whenever the playground is active, and outside the playground we explicitly don't want a stale draft value to override the host's theme. The memo deps already include resolvedTheme, so propagation through setHostContext (line 1274) is wired up.

One small consistency note for a future pass — not this PR: chatgpt-app-renderer.tsx still uses the older extractHostTheme(draftHostContext) ?? themeMode pattern. Worth aligning when you next touch that file so both renderers honor useChatboxHostTheme() identically.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mcpjam-inspector/client/src/components/chat-v2/thread/mcp-apps/mcp-apps-renderer.tsx`
around lines 836 - 885, Keep the simplified behavior: set theme: resolvedTheme
unconditionally inside the McpUiHostContext created in the useMemo (symbol:
hostContext), and ensure resolvedTheme remains in the memo dependency array (it
already is) so updates propagate via setHostContext; remove any leftover
conditional branch that tried to preserve baseHostContext.theme when it was
"light"/"dark". For consistency in a follow-up change, update the other renderer
(symbol: chatgpt-app-renderer and its draftHostContext handling) to derive theme
the same way (use the same resolvedTheme/useChatboxHostTheme pattern rather than
extractHostTheme(draftHostContext) ?? themeMode) so both renderers behave
identically.
mcp/src/ui/show-servers.tsx (1)

350-395: Tame the screen-reader name on the whole-card button.

By promoting the entire card to a <button>, every descendant text node — name, version, status word, URL — gets concatenated into the button's accessible name. On screens with several entries, that's a verbose mouthful per card. Consider giving the button an explicit aria-labelledby (pointing to the <h2>) or an aria-label like Open ${server.name} details, so AT users hear an actionable label while sighted users keep all the visual context.

♻️ Suggested concise accessible name
       <button
         type="button"
         onClick={onSelect}
+        aria-label={`Open details for ${server.name}`}
         className="block h-full w-full cursor-pointer rounded-xl p-4 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/ui/show-servers.tsx` around lines 350 - 395, The card's top-level
button (onClick handler on the Card wrapper / onSelect) exposes every descendant
text as the accessible name; change it to an explicit accessible name by adding
either aria-label={`Open ${server.name} details`} or aria-labelledby pointing to
the heading id—give the <h2> a stable id (e.g., `server-title-${server.id}`) and
set the button's aria-labelledby to that id so screen readers announce a concise
actionable label while keeping the visible title/version/status/URL intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@mcp/src/ui/show-servers.tsx`:
- Around line 350-395: The card's top-level button (onClick handler on the Card
wrapper / onSelect) exposes every descendant text as the accessible name; change
it to an explicit accessible name by adding either aria-label={`Open
${server.name} details`} or aria-labelledby pointing to the heading id—give the
<h2> a stable id (e.g., `server-title-${server.id}`) and set the button's
aria-labelledby to that id so screen readers announce a concise actionable label
while keeping the visible title/version/status/URL intact.

In
`@mcpjam-inspector/client/src/components/chat-v2/thread/mcp-apps/mcp-apps-renderer.tsx`:
- Around line 836-885: Keep the simplified behavior: set theme: resolvedTheme
unconditionally inside the McpUiHostContext created in the useMemo (symbol:
hostContext), and ensure resolvedTheme remains in the memo dependency array (it
already is) so updates propagate via setHostContext; remove any leftover
conditional branch that tried to preserve baseHostContext.theme when it was
"light"/"dark". For consistency in a follow-up change, update the other renderer
(symbol: chatgpt-app-renderer and its draftHostContext handling) to derive theme
the same way (use the same resolvedTheme/useChatboxHostTheme pattern rather than
extractHostTheme(draftHostContext) ?? themeMode) so both renderers behave
identically.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b37bf320-46bd-453e-ae65-b452bc3d33f3

📥 Commits

Reviewing files that changed from the base of the PR and between 717be29 and d3c3a2c.

⛔ Files ignored due to path filters (1)
  • mcp/src/generated/McpAppsHtml.bundled.ts is excluded by !**/generated/**
📒 Files selected for processing (3)
  • mcp/src/ui/show-servers.tsx
  • mcpjam-inspector/client/src/components/chat-v2/thread/mcp-apps/__tests__/mcp-apps-renderer.test.tsx
  • mcpjam-inspector/client/src/components/chat-v2/thread/mcp-apps/mcp-apps-renderer.tsx

@chelojimenez chelojimenez merged commit 044678c into main Apr 26, 2026
14 checks passed
@chelojimenez chelojimenez deleted the codex/ui-tool branch April 26, 2026 05:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant