Skip to content

Commit 67f28aa

Browse files
cliffhallclaude
andcommitted
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>
1 parent 9b373a6 commit 67f28aa

1 file changed

Lines changed: 32 additions & 10 deletions

File tree

‎clients/web/src/App.tsx‎

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
}from"@modelcontextprotocol/sdk/types.js";
99
importtype{AppBridge}from"@modelcontextprotocol/ext-apps/app-bridge";
1010
import{InspectorClient}from"@inspector/core/mcp/index.js";
11+
importtype{JsonValue}from"@inspector/core/mcp/index.js";
1112
importtype{MessageEntry,ServerEntry}from"@inspector/core/mcp/types.js";
1213
import{API_SERVER_ENV_VARS}from"@inspector/core/mcp/remote/constants.js";
1314
import{ManagedToolsState}from"@inspector/core/mcp/state/managedToolsState.js";
@@ -253,10 +254,26 @@ function App() {
253254
}
254255
},[connectionStatus]);
255256

257+
// Disconnect the previous InspectorClient when it's replaced (server
258+
// switch) or when App unmounts (HMR, tests). Without this the prior
259+
// session's transport — a spawned stdio subprocess, an SSE stream, or
260+
// an HTTP session — stays open until GC eventually lets go. The
261+
// state-manager destroys in `setupClientForServer` only handle the
262+
// listener side; this effect handles the transport side. `disconnect()`
263+
// is the canonical lifecycle hook (InspectorClient has no `destroy()`);
264+
// it closes the transport, clears subscriptions, cancels receiver TTLs.
265+
useEffect(()=>{
266+
return()=>{
267+
if(inspectorClient){
268+
voidinspectorClient.disconnect();
269+
}
270+
};
271+
},[inspectorClient]);
272+
256273
// Build the InitializeResult the connected ViewHeader expects from the
257274
// hook's split fields. `protocolVersion` is hard-coded for now — the
258-
// useInspectorClient hook doesn't expose it; revisit if/when InspectorView
259-
// grows a need for the real negotiated value.
275+
// useInspectorClient hook doesn't expose it. TODO(#1324): consume the
276+
// negotiated value once the hook surfaces it.
260277
constinitializeResult=useMemo<InitializeResult|undefined>(()=>{
261278
if(connectionStatus!=="connected"||!serverInfo)returnundefined;
262279
return{
@@ -358,6 +375,10 @@ function App() {
358375
try{
359376
awaitclient.connect();
360377
}catch(err){
378+
// Handshake-only. A mid-session transport failure transitions the
379+
// client status to "error" without rejecting any pending promise,
380+
// and `errorMessage` stays stale. TODO(#1323): consume an `error`
381+
// event from `InspectorClientEventMap` once it exists.
361382
connectStartRef.current=undefined;
362383
constmessage=errinstanceofError ? err.message : String(err);
363384
setErrorMessage(message);
@@ -386,9 +407,14 @@ function App() {
386407
if(!tool)return;
387408
setToolCallState({status: "pending"});
388409
try{
410+
// ToolsScreen types the args as `Record<string, unknown>` (it accepts
411+
// anything the user types into the schema form). `callTool` requires
412+
// `Record<string, JsonValue>` — narrow at the boundary instead of
413+
// claiming the object is empty (which the previous `as Record<string,
414+
// never>` cast did, misleadingly).
389415
constinvocation=awaitinspectorClient.callTool(
390416
tool,
391-
argsasRecord<string,never>,
417+
argsasRecord<string,JsonValue>,
392418
);
393419
setToolCallState({
394420
status: invocation.success ? "ok" : "error",
@@ -531,6 +557,9 @@ function App() {
531557
prompts={prompts}
532558
resources={resources}
533559
resourceTemplates={resourceTemplates}
560+
// TODO(#1325): drop the empty fallback once `useResourceSubscriptions`
561+
// surfaces the live subscription list — subscribe/unsubscribe buttons
562+
// currently fire but the screen never reflects the result.
534563
subscriptions={[]}
535564
logs={logs}
536565
tasks={tasks}
@@ -591,10 +620,3 @@ function App() {
591620
}
592621

593622
exportdefaultApp;
594-
595-
// Surface so the FetchRequestLog + Stderr state managers are kept alive
596-
// (their effects subscribe to client events for #1262 / future panels).
597-
// Marking them void here keeps the no-unused-vars lint quiet without
598-
// hiding the intent.
599-
voidFetchRequestLogState;
600-
voidStderrLogState;

0 commit comments

Comments
 (0)