Skip to content

Commit 4d74b94

Browse files
cliffhallclaude
andauthored
Wire App.tsx to v2 core/ hook layer; remove demo stub from InspectorView (#1322)
* feat(web): wire App.tsx to v2 core/ hook layer; remove demo stub from InspectorView App.tsx mounts InspectorView with live data from the v2 hook layer instead of the title-page placeholder. InspectorView becomes pure prop-driven; the demo handshake stub and connection state machinery are removed. Connection lifecycle: - App.tsx lazily instantiates `InspectorClient` on the connect edge, rebuilding it (and its state managers) when the user picks a different server. State managers (Managed{Tools,Prompts,Resources,ResourceTemplates, RequestorTasks}State + MessageLog/FetchRequestLog/StderrLog) are destroyed on switch. - `useInspectorClient` + the per-primitive `useManaged*` hooks drive the view's data. `latencyMs` is captured at the connecting → connected edge via a ref so the intermediate rerenders don't lose the start timestamp. - `errorMessage` is captured from `client.connect()` rejection; status- machine-driven errors that don't surface through the promise are a known follow-up (no `error` event in the v2 InspectorClientEventMap yet). Action handlers: - `onCallTool` / `onGetPrompt` / `onReadResource` await the corresponding InspectorClient method and reflect pending → ok/error in `ToolCallState` / `GetPromptState` / `ReadResourceState` panel props. Manually verified end-to-end against `npx @modelcontextprotocol/server-filesystem` — the Tools result panel renders the real "Allowed directories: /private/tmp" output rather than a stub. - `onSetLogLevel` calls `client.setLoggingLevel(level)` and optimistically bumps `currentLogLevel` locally (the request has no echo notification). - `onSubscribeResource` / `onUnsubscribeResource` / `onCancelTask` route to their respective client methods. Server CRUD (`onServerAdd`/`onServerEdit` /…) and history pinning are intentionally `todoNoop` for now; the seed server list is hardcoded per the agreed scope (a `useServers` v2-only hook is a separate effort). - `notifications/message` notifications flowing through `MessageLogState` are filtered into `LogEntryData[]` and passed to the Logs screen, so `logging/setLevel` and the resulting log stream round-trip. InspectorView (`InspectorView.tsx`): - Removed: `STUB_*` constants, `handshakeTimer` ref, `handleToggleConnection` / `disconnect` functions, stub `bridgeFactory`/`sandboxPath`, internal connection state, `logLevel` state. - Added 38 props covering connection state, panel states, log level, and action callbacks. The view is pure presentational. - `availableTabs` is derived from `connectionStatus`; `activeTab` is clamped to whatever's currently available (avoiding the `set-state-in-effect` lint and keeping the previously-selected tab on reconnect). Stories + tests: - `InspectorView.stories.tsx` adds spy callbacks for every action prop and new `Connected` / `ConnectionError` stories for the connected/error narratives. - `InspectorView.test.tsx` is rewritten around the prop-driven contract (10 tests covering the empty list, server cards, toggle dispatch, connected header, error banner, tab snap-back on disconnect, app filtering, log-level dispatch, autoScroll local toggle). Dev backend port wiring (`vite.config.ts`): - Vite's dev server is now pinned to `CLIENT_PORT` (default 6274) with `strictPort: true`, matching the dev backend's `allowedOrigins`. Without this, Vite would fall back to 5173 while the Hono origin check still required 6274 — breaking every browser `/api/*` fetch out of the box. - Top-level `resolve.alias` now spreads the same bare-module aliases the vitest projects use. Rolldown can't reach `pino/browser.js` / `zustand/middleware` from `core/`'s parent (no node_modules there); the wired App pulls those modules into the browser dep graph for the first time, so the aliases need to be visible at build time too. createWebEnvironment helper (`src/lib/environmentFactory.ts`): - Mirrors v1.5's `lib/adapters/environmentFactory.ts`: builds `InspectorClientEnvironment` from `createRemoteTransport` + `createRemoteFetch` + `createRemoteLogger` + `BrowserOAuthStorage` + `BrowserNavigation`. Returns the assembled environment plus the logger for direct app-level use. Closes #1244. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): wire dev-backend auth token into createWebEnvironment + show full stdio command in ServerCard Two issues surfaced after the #1244 wiring landed locally: 1. **401 on `/api/mcp/connect`** when running `npm run dev` with the default auth (no `DANGEROUSLY_OMIT_AUTH`). The dev backend generates a random token and prints `http://localhost:6274?MCP_INSPECTOR_API_TOKEN=…` in the banner, but App.tsx was calling `createWebEnvironment(undefined, …)` so the browser never sent `x-mcp-remote-auth`. The Hono middleware correctly rejected with "Authentication required. Use the x-mcp-remote-auth header with Bearer token." Adds a small `getAuthToken()` helper in App.tsx that reads `MCP_INSPECTOR_API_TOKEN` off `window.location.search`, falls back to `sessionStorage`, and persists fresh values into sessionStorage so client-side navigation / OAuth round-trips don't drop the token. The resolved value is passed to `createWebEnvironment` whenever a new InspectorClient is built. 2. **Misleading ServerCard display** — the seed server's `config` is `{ command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] }` but the card only rendered `"npx"`, so it looked like the command was incomplete. `getCommandOrUrl` now joins `command + args` for stdio configs, so the card shows the same string that gets spawned. SSE / streamable-http paths unchanged. Manual end-to-end re-verified against `npm run dev` (no DANGEROUSLY_OMIT_AUTH): - Banner prints the auth-token URL. - Browser navigates to that URL → `getAuthToken()` picks the token off the query string, persists to sessionStorage, threads it through to every `/api/*` request. - Server card shows `npx -y @modelcontextprotocol/server-filesystem /tmp`. - Toggle connect → connection succeeds; ViewHeader shows `Connected (~1.3s)`; Tools tab populates with the 14 filesystem tools. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): use dark-9 instead of blue-9 for dark-mode page background `main.tsx` overrode `--mantine-color-body` to `--mantine-color-blue-9` in dark mode, while `.storybook/preview.tsx` used `--mantine-color-dark-9`. The dev app then rendered with a blue background while every story (and the design intent visible in storybook) used dark grey. Switch main.tsx to `--mantine-color-dark-9` so the dev app matches the storybook reference. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(web): seed the Servers screen with the "everything" reference server Adds `@modelcontextprotocol/server-everything` to the hardcoded seed list alongside the filesystem server. The everything server exposes tools, prompts, resources, sampling, and completion — covering most of the surface a developer might want to exercise against the wired-up InspectorView without configuring anything. Comment updated to explain the two-seed shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): address #1244 PR review feedback (JsonValue cast, client lifecycle, dead code) - **#1 — Misleading `Record<string, never>` cast**: Imported `JsonValue` from `@inspector/core/mcp/index.js` and changed `args as Record<string, never>` to `args as Record<string, JsonValue>` in `onCallTool`. The new cast honestly narrows from the screen-level `Record<string, unknown>` to what `InspectorClient.callTool` actually requires. Added a comment explaining the boundary. - **#2 + #3 — Previous InspectorClient not disconnected on server switch or unmount**: Added a `useEffect` keyed on `inspectorClient` whose cleanup calls `inspectorClient.disconnect()`. One effect covers both the swap-on-switch leak (prior session's transport stayed open until GC) and the unmount-during-connected leak (HMR / tests). `setupClientForServer`'s existing state-manager `destroy()` calls handle the listener side; the new effect handles the transport side. - **#4 — Dead `void` block at bottom of file**: Deleted the trailing `void FetchRequestLogState; void StderrLogState;` block. The state managers are already constructed via `new FetchRequestLogState(client)` / `new StderrLogState(client)`, so the imports are referenced by the linter's reckoning. The `void` pair was dead code with a comment that no longer matched reality. - **#5 / #7 / #8 — Follow-up TODOs**: Added TODO refs to the new follow-up issues: - TODO(#1323): `error` event in InspectorClientEventMap so mid-session transport failures surface (connect-only catch is incomplete). - TODO(#1324): negotiated `protocolVersion` through `useInspectorClient` (currently hard-coded `"2025-06-18"`). - TODO(#1325): `useResourceSubscriptions` hook so subscribe/unsubscribe buttons reflect their server-side effect. Items #6 (useCallback dep churn), #9 (helper unit tests), and #10 (shared no-op bridge factory fixture) are acknowledged in the PR reply as deferred — no functional impact and out of scope for a "wire the hook layer" PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 37a4d99 commit 4d74b94

8 files changed

Lines changed: 1241 additions & 335 deletions

File tree

clients/web/src/App.tsx

Lines changed: 610 additions & 48 deletions
Large diffs are not rendered by default.

clients/web/src/components/groups/ServerCard/ServerCard.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,13 @@ function getCommandOrUrl(config: MCPServerConfig): string {
7575
if (config.type === "sse" || config.type === "streamable-http") {
7676
return config.url;
7777
}
78-
return config.command;
78+
// Show the full argv for stdio so the card displays the same thing
79+
// that gets spawned. Otherwise a `command: "npx", args: ["-y", "pkg"]`
80+
// config renders as just "npx", which is misleading.
81+
const args = config.args ?? [];
82+
return args.length > 0
83+
? `${config.command} ${args.join(" ")}`
84+
: config.command;
7985
}
8086

8187
export function ServerCard({

clients/web/src/components/views/InspectorView/InspectorView.stories.tsx

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type {
2+
InitializeResult,
23
Prompt,
34
Resource,
45
ResourceTemplate,
56
Task,
67
Tool,
78
} from "@modelcontextprotocol/sdk/types.js";
9+
import type { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge";
810
import type {
911
InspectorResourceSubscription,
1012
MessageEntry,
@@ -17,6 +19,19 @@ import { mixedEntries as demoLogs } from "../../screens/LoggingScreen/LoggingScr
1719
import { longToolList as demoRegularTools } from "../../screens/ToolsScreen/ToolsScreen.fixtures";
1820
import { SUN_ICON_SVG } from "../../../test/fixtures/storyIcons";
1921
import type { TaskProgress } from "../../groups/TaskCard/TaskCard";
22+
import type { BridgeFactory } from "../../elements/AppRenderer/AppRenderer";
23+
24+
// Stories never drive a real MCP App bridge — render the iframe stage with
25+
// a no-op factory so the AppsScreen mounts without trying to postMessage to
26+
// a real sandbox.
27+
const noopBridgeFactory: BridgeFactory = () =>
28+
({
29+
sendToolInput: async () => {},
30+
sendToolResult: async () => {},
31+
sendToolCancelled: async () => {},
32+
teardownResource: async () => ({}),
33+
close: async () => {},
34+
}) as unknown as AppBridge;
2035

2136
// MCP App tools — `isAppTool` detects these via `_meta.ui.resourceUri`,
2237
// so they get filtered into the Apps tab while still appearing on Tools.
@@ -234,12 +249,18 @@ const demoHistory: MessageEntry[] = [
234249
},
235250
];
236251

252+
const demoInitializeResult: InitializeResult = {
253+
protocolVersion: "2025-06-18",
254+
capabilities: {},
255+
serverInfo: { name: "Local Dev Server", version: "1.2.0" },
256+
};
257+
237258
const meta: Meta<typeof InspectorView> = {
238259
title: "Views/InspectorView",
239260
component: InspectorView,
240261
parameters: { layout: "fullscreen" },
241262
args: {
242-
onToggleTheme: fn(),
263+
// Data
243264
servers: demoServers,
244265
tools: demoTools,
245266
prompts: demoPrompts,
@@ -250,6 +271,57 @@ const meta: Meta<typeof InspectorView> = {
250271
tasks: demoTasks,
251272
progressByTaskId: demoProgressByTaskId,
252273
history: demoHistory,
274+
275+
// Connection state — stories default to "disconnected"; per-story
276+
// overrides drive the connected / error narratives.
277+
activeServer: undefined,
278+
connectionStatus: "disconnected",
279+
initializeResult: undefined,
280+
latencyMs: undefined,
281+
errorMessage: undefined,
282+
283+
// Misc state
284+
currentLogLevel: "info",
285+
sandboxPath: "about:blank",
286+
bridgeFactory: noopBridgeFactory,
287+
288+
// Callbacks — all wired to storybook spies so play functions can assert
289+
// on dispatch. Real wiring routes these to InspectorClient methods (the
290+
// app shell at clients/web/src/App.tsx).
291+
onToggleTheme: fn(),
292+
onToggleConnection: fn(),
293+
onDisconnect: fn(),
294+
onServerAdd: fn(),
295+
onServerImportConfig: fn(),
296+
onServerImportJson: fn(),
297+
onServerInfo: fn(),
298+
onServerSettings: fn(),
299+
onServerEdit: fn(),
300+
onServerClone: fn(),
301+
onServerRemove: fn(),
302+
onCallTool: fn(),
303+
onRefreshTools: fn(),
304+
onGetPrompt: fn(),
305+
onRefreshPrompts: fn(),
306+
onReadResource: fn(),
307+
onSubscribeResource: fn(),
308+
onUnsubscribeResource: fn(),
309+
onRefreshResources: fn(),
310+
onCancelTask: fn(),
311+
onClearCompletedTasks: fn(),
312+
onRefreshTasks: fn(),
313+
onSetLogLevel: fn(),
314+
onClearLogs: fn(),
315+
onExportLogs: fn(),
316+
onCopyAllLogs: fn(),
317+
onClearHistory: fn(),
318+
onExportHistory: fn(),
319+
onReplayHistory: fn(),
320+
onTogglePinHistory: fn(),
321+
onSelectApp: fn(),
322+
onOpenApp: fn(),
323+
onCloseApp: fn(),
324+
onRefreshApps: fn(),
253325
},
254326
};
255327

@@ -263,3 +335,24 @@ export const NoServers: Story = {
263335
servers: [],
264336
},
265337
};
338+
339+
// Renders the connected-state shell (full tab list, ViewHeader in connected
340+
// mode). The other tabs still render their disconnected fixtures because
341+
// the lists are passed through as static data — that's fine for visual
342+
// regression / storybook play function coverage.
343+
export const Connected: Story = {
344+
args: {
345+
activeServer: demoServers[0]!.id,
346+
connectionStatus: "connected",
347+
initializeResult: demoInitializeResult,
348+
latencyMs: 142,
349+
},
350+
};
351+
352+
export const ConnectionError: Story = {
353+
args: {
354+
activeServer: demoServers[0]!.id,
355+
connectionStatus: "error",
356+
errorMessage: "Handshake timeout",
357+
},
358+
};

0 commit comments

Comments
 (0)