Skip to content

feat(inspector): stdio MCP server transport support - #132

Merged
gabrypavanello merged 8 commits into
mainfrom
feat/stdio-support
Jan 31, 2026
Merged

feat(inspector): stdio MCP server transport support#132
gabrypavanello merged 8 commits into
mainfrom
feat/stdio-support

Conversation

@gabrypavanello

Copy link
Copy Markdown
Contributor

Summary

Add stdio transport support to the MCP Inspector, enabling connection to MCP servers via stdin/stdout in addition to the existing HTTP transport.

Changes

Backend

  • createTestClient accepts ConnectionParams with transport: "stdio" | "http" discriminated union
  • ConnectionManager stores connectionParams in state, builds stdio display labels (stdio: command args)
  • Auto-restart: stdio connections auto-reconnect on transport close with exponential backoff (max 3 retries), with generation counter to prevent stale restarts after explicit disconnect
  • connect_to_server tool: accepts stdio params, backward-compatible with legacy { url } input
  • Dashboard POST /connections: validates stdio params (command required), returns transport type
  • ConnectionRegistry: passes through both transport types

Frontend

  • Transport dropdown in ConnectionBar: HTTP / stdio selector
  • Conditional inputs: HTTP shows URL field; stdio shows command + args fields
  • Advanced Settings panel (stdio only): environment variables textarea + working directory input
  • Server history: stores transport type, deduplicates stdio entries by command+args

Tests

  • 18 behavioral tests covering all transport paths, auto-restart logic, backward compatibility, and validation
  • Playwright E2E verified against real @modelcontextprotocol/server-basic-react --stdio

Testing

  • Build ✅ (all 11 projects)
  • 813 unit tests ✅
  • 18 behavioral tests ✅ (stdio-specific)
  • Playwright integration ✅ (connected to real stdio MCP server, tools + resources loaded)

Sirius added 2 commits January 30, 2026 18:55
Add stdio transport alongside existing HTTP for connecting to MCP servers
that communicate over stdin/stdout.

## Architecture
- ConnectionParams discriminated union: { transport: 'http', url } | { transport: 'stdio', command, args?, env?, cwd? }
- ONE branch point at createTestClient() — all other code is transport-agnostic
- Auto-restart for stdio with exponential backoff (1s, 2s, 4s, max 3 retries)

## Changes

### Types & Transport (Phase 1)
- Add ConnectionParams type to @mcp-apps-kit/testing
- createTestClient() accepts ConnectionParams, branches to StdioClientTransport or StreamableHTTPClientTransport
- Add onTransportClose callback to TestClientOptions
- Add connectionParams field to ConnectionState

### Connection Chain (Phase 2)
- ConnectionManager.connect() accepts ConnectionParams with input validation
- Auto-restart logic: onTransportClose → exponential backoff reconnect (stdio only)
- ConnectionRegistry.createConnection() accepts ConnectionParams
- All callers updated (28+ test files, 4 source files)

### Tool API (Phase 3)
- connect_to_server tool supports both transports via Zod union schema
- Backward compatible: plain { url } still works (defaults to HTTP)

### Dashboard API (Phase 4)
- POST /dashboard/connections accepts ConnectionParams body
- Backward compat: { url } without transport field defaults to HTTP
- Response includes transport type

### Dashboard UI (Phase 5)
- Transport dropdown (HTTP/stdio) in ConnectionBar
- stdio mode: command + args inputs replace URL input
- Advanced Settings toggle: env vars + cwd (stdio only)
- Server history stores transport type, shows stdio: badge
- Selecting stdio history entry populates command/args fields
…nconsistency

- Add connectionGeneration counter to ConnectionManager to prevent
  stale restart attempts after explicit disconnect()
- Align label format in test-client.ts to match connection.ts
  ('stdio: command' with space)
- Add stdio-transport behavioral test suite (18 tests)
@coderabbitai

coderabbitai Bot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Multi-transport connection support (HTTP + stdio) with structured connection parameters and human-friendly connection labels.
  • UI/UX Improvements

    • Connection UI: transport selector, transport-aware history, stdio inputs (command/args/env/cwd), advanced settings popover, improved keyboard navigation.
  • Bug Fixes / Validation

    • Stronger input validation and clearer, label-based error messages for connection failures.
  • Tests

    • Extensive test coverage covering transports, restart/backoff behavior, error paths, and connection flows.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Connection APIs moved from a positional URL string to a structured ConnectionParams object (transport + fields). Stdio transport support and onTransportClose hooks added, with auto-restart/backoff scaffolding. Dashboard, UI, tools, testing utilities, and many tests updated to accept and normalize the new param shape.

Changes

Cohort / File(s) Summary
Core testing types & client
packages/testing/src/types.ts, packages/testing/src/server/test-client.ts, packages/testing/src/index.ts
Add exported ConnectionParams union and onTransportClose option; change createTestClient to accept ConnectionParams; implement stdio transport path, label-based logging/errors, env merging, and on-transport-close plumbing.
Inspector connection core
packages/inspector/src/connection.ts, packages/inspector/src/connection-registry.ts, packages/inspector/src/types/connection-types.ts
Change ConnectionManager.connect / ConnectionRegistry.createConnection to accept ConnectionParams; persist connectionParams in state; add stdio auto-restart/backoff scaffolding and generation/timer fields.
Dashboard server & API
packages/inspector/src/dashboard/dashboard-server.ts
Accept ConnectionParams on POST /dashboard/connections with backward-compat { url }; validate http vs stdio inputs; normalize and pass params to registry; return transport + derived serverUrl.
Dashboard UI & hooks
packages/inspector/src/dashboard/react/components/ConnectionBar.tsx, packages/inspector/src/dashboard/react/InspectorDashboard.tsx, packages/inspector/src/dashboard/react/hooks/useConnections.ts, packages/inspector/src/dashboard/react/hooks/useServerHistory.ts
Add transport selector (http/stdio), stdio inputs (command/args/env/cwd), env parsing, buildParams helper, transport-aware history entries, and update handlers/hooks to accept ConnectionParams.
Connect tool & schemas
packages/inspector/src/tools/connect.ts
Introduce union input schema (http, stdio, legacy); normalize inputs via buildConnectionParams; use display labels for errors/serverUrl; update tool docs and error messages.
Server integration & call sites
packages/inspector/src/standalone-server.ts, packages/inspector/src/dual-server.ts, packages/create-app/src/index.ts
Update registry/createConnection and connect call sites to pass { transport: "http", url } and propagate trackHistory where applicable.
Examples, evaluator & UI test env
examples/.../tests/*, packages/testing/src/eval/mcp/evaluator.ts, packages/testing/src/ui/test-environment.ts
Replace createTestClient(url, ...) with createTestClient({ transport: "http", url }, ...) across examples and eval/test envs.
Inspector tests — stdio behavioral suite
packages/inspector/tests/stdio-transport.test.ts
Add comprehensive behavioral tests validating stdio/http creation, param normalization, serverUrl derivation, auto-restart/backoff behavior, dashboard endpoints, and tool normalization.
Inspector tests — widespread updates
packages/inspector/tests/*, packages/inspector/tests/test-utils.ts
Update 20+ test files and test utilities to pass ConnectionParams to connect/createConnection; adjust mocks and helpers for the new shapes.
Testing unit updates
packages/testing/tests/unit/server/test-client.test.ts
Revise unit tests to construct clients using ConnectionParams (http and stdio), add transport wiring and close-event tests, and validate new error/label/history behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ConnectionManager
    participant Transport
    participant MCPServer

    rect rgba(100, 200, 150, 0.5)
    note over Client,ConnectionManager: HTTP transport flow
    Client->>ConnectionManager: connect({transport:"http", url:"http://..."})
    ConnectionManager->>Transport: instantiate StreamableHTTPClientTransport(url)
    Transport->>MCPServer: open HTTP stream/request
    MCPServer-->>Transport: response / ready
    Transport-->>ConnectionManager: onReady()
    ConnectionManager-->>Client: connected
    end

    rect rgba(150, 150, 200, 0.5)
    note over Client,ConnectionManager: Stdio transport with auto-restart
    Client->>ConnectionManager: connect({transport:"stdio", command:"cmd", args:[]})
    ConnectionManager->>Transport: spawn StdioClientTransport(command,args,env)
    Transport->>MCPServer: child process spawned
    MCPServer-->>Transport: ready / then close
    Transport-->>ConnectionManager: onClose()
    alt closed unexpectedly
        ConnectionManager->>ConnectionManager: handleStdioProcessExit() (backoff & attempts++)
        ConnectionManager->>Transport: spawn (reconnect attempt)
        Transport->>MCPServer: new child process
        MCPServer-->>Transport: ready
    else max attempts exceeded
        ConnectionManager-->>Client: emit error / abort
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(inspector): stdio MCP server transport support' clearly and concisely describes the main feature addition: stdio transport support for MCP servers in the inspector.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, providing a detailed summary of backend/frontend changes, testing coverage, and validation results.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/stdio-support

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


it("should return empty array when no UI widgets", async () => {
const client = await manager.connect("http://localhost:3000/mcp");
const client = await manager.connect({ transport: "http", url: "http://localhost:3000/mcp" });
@claude

claude Bot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

Code Review: PR #132 - Stdio Transport Support

Summary

This PR adds stdio transport support to the MCP Inspector, enabling connections to MCP servers via stdin/stdout alongside the existing HTTP transport. The implementation is well-structured with proper type safety and comprehensive test coverage.

Verdict: ✅ Approve with Minor Fixes Recommended


Strengths

  1. Excellent Type Safety

    • Proper discriminated union pattern for ConnectionParams (packages/testing/src/types.ts:571-589)
    • Full compliance with TypeScript strict mode (noImplicitAny, noUncheckedIndexedAccess, strictNullChecks)
    • Clean separation between HTTP and stdio configuration
  2. Comprehensive Test Coverage

    • 18 new behavioral tests in packages/inspector/tests/stdio-transport.test.ts
    • Auto-restart logic thoroughly tested with fake timers
    • Backward compatibility verified
  3. Security Best Practices

    • No command injection risk - command and args passed separately
    • Proper environment variable handling with type validation
    • Input validation at API boundaries
  4. Backward Compatibility

    • Legacy { url } input still works, defaults to HTTP transport
    • All existing tests updated correctly

Issues Found

High Priority

1. Race Condition in Auto-Restart Logic (packages/inspector/src/connection.ts:459-488)

The auto-restart mechanism has a potential race condition:

this.autoRestartTimer = setTimeout(() => {
  if (this.connectionGeneration !== generationAtStart) {
    return;
  }
  
  this.connect(params, options)
    .then(() => {
      if (this.connectionGeneration !== generationAtStart) {
        void this.disconnect(); // Called after successful reconnect
      }
    })
}, delay);

Issue: If disconnect() is called while connect() is in-flight, the promise chain will still call disconnect() again after the connection succeeds. While probably safe due to state checking, this could cause resource cleanup issues.

Recommendation: Add a flag to track whether disconnect was requested during reconnect, or use AbortController pattern.

2. Error Handling in Auto-Restart (packages/inspector/src/connection.ts:481-487)

.catch(() => {
  void this.disconnect(); // Could throw, not caught
});

Issue: If disconnect() throws during cleanup, the error is swallowed by void.

Recommendation: Wrap in try-catch or use .catch() on the disconnect promise.

Medium Priority

3. Missing Path Validation (packages/inspector/src/connection.ts:390-402)

The cwd parameter accepts any string without validation.

Security Consideration: While not a direct vulnerability (stdio processes run in the inspector's security context), invalid paths will cause confusing errors. Consider adding validation or documenting that the inspector should not be exposed to untrusted users.

Recommendation:

if (params.transport === "stdio" && params.cwd) {
  // Validate cwd is a valid directory
  if (!fs.existsSync(params.cwd) || !fs.statSync(params.cwd).isDirectory()) {
    throw new Error(`Invalid working directory: ${params.cwd}`);
  }
}

4. Test Coverage Gaps

Missing tests for:

  • Concurrent disconnect during reconnect (the race condition above)
  • Environment variable merging edge cases (packages/testing/src/server/test-client.ts:46-52)
  • Distinction between stdio process crash vs. clean exit

Low Priority

5. Type Safety Enhancement (packages/testing/src/server/test-client.ts:64-66)

transport.onclose = options.onTransportClose;

Issue: The onTransportClose callback signature isn't explicitly typed in TestClientOptions.

Recommendation: Add explicit type to TestClientOptions.onTransportClose:

onTransportClose?: () => void;

6. Complex Environment Variable Filtering (packages/testing/src/server/test-client.ts:46-52)

The filter pattern is correct but complex:

Object.fromEntries(
  Object.entries({ ...process.env, ...params.env }).filter(
    (entry): entry is [string, string] => entry[1] !== undefined
  )
)

Recommendation: Add a comment explaining that process.env can have undefined values that must be filtered for stdio transport.


Code Quality Observations

Follows Repository Standards:

  • Adheres to AGENTS.md requirements (Zod v4, strict TypeScript, no any types)
  • Proper export contract through src/index.ts
  • Test coverage thresholds will be met (18 new tests, 557 lines of test code)

Architecture:

  • Clean separation of concerns with single branch point at createTestClient()
  • Transport-agnostic code throughout the stack
  • Proper use of discriminated unions

Error Messages:

  • Clear, actionable error messages (e.g., "stdio transport requires a non-empty command")
  • Proper validation at API boundaries

Performance Considerations

Auto-Restart Backoff:

  • Exponential backoff (1s, 2s, 4s) is appropriate
  • Max 3 retries prevents infinite loops
  • Resource cleanup is proper (timeout clearing on disconnect)

Memory Management:

  • Call history limited by maxHistorySize
  • Event listeners properly cleaned up
  • No obvious memory leaks

Final Recommendations

Before merging:

  1. CI must pass - Currently in progress
  2. ⚠️ Fix or document the race condition in auto-restart logic
  3. ⚠️ Add error handling for disconnect failures during cleanup
  4. 💡 Consider adding path validation for cwd parameter
  5. 💡 Consider adding missing test cases for edge cases

Optional improvements:

  • Add inline comments for complex logic (environment filtering, generation counter)
  • Document security considerations for stdio transport in README
  • Add JSDoc comments for public APIs

Testing Checklist

  • ✅ Unit tests pass (813 existing + 18 new)
  • ✅ Behavioral tests comprehensive
  • ✅ Backward compatibility verified
  • ⏳ Integration tests (Playwright E2E mentioned in PR description)
  • ⏳ Build succeeds across all 11 projects

Great work on this feature! The stdio transport is a valuable addition that's well-implemented. The identified issues are mostly edge cases that are unlikely to cause problems in practice, but addressing them would improve robustness.

cc: @Reviewer - please verify CI passes and consider addressing the high-priority items before merge.

@claude

claude Bot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

Code Review: PR #132 - stdio MCP Server Transport Support

Executive Summary

This PR adds stdio (stdin/stdout) transport support to the MCP Inspector, enabling connection to MCP servers via child process spawning in addition to the existing HTTP transport. The implementation is well-architected, thoroughly tested, and production-ready with strong type safety and backward compatibility.

Recommendation: ✅ APPROVE with minor suggestions


Strengths

1. Excellent Architecture & Design ⭐⭐⭐⭐⭐

  • Clean discriminated union: ConnectionParams uses TypeScript discriminated unions perfectly (transport: "http" | "stdio"), enabling type-safe branching throughout the codebase
  • Backward compatibility: Legacy { url } input format gracefully converts to { transport: "http", url } without breaking existing code
  • Separation of concerns: Transport-specific logic is isolated in test-client.ts, with higher-level components (ConnectionManager, ConnectionRegistry) remaining transport-agnostic
  • Auto-restart with exponential backoff: Sophisticated stdio process restart logic (max 3 attempts, 1s/2s/4s delays) with generation counters to prevent stale reconnects - this is production-grade resilience engineering

2. Comprehensive Test Coverage ⭐⭐⭐⭐⭐

  • 18 behavioral tests in stdio-transport.test.ts covering all critical paths:
    • Transport parameter forwarding (C1-C2)
    • Connection state management (C3-C4)
    • Auto-restart with backoff (C5-C7)
    • Registry integration (C8)
    • Tool validation (C9-C10)
    • Dashboard API validation (C11-C13)
  • Mocking strategy: Smart use of vi.hoisted() to coordinate mock state between factory and tests
  • Playwright E2E: Real integration test against @modelcontextprotocol/server-basic-react --stdio
  • Coverage includes edge cases: empty commands, intentional disconnects, max retry limits

3. Strong Type Safety ⭐⭐⭐⭐⭐

  • Strict adherence to repo's TypeScript policy (no any, proper narrowing)
  • Zod schemas for runtime validation: httpTransportSchema, stdioTransportSchema, legacyInputSchema
  • Type guards and narrowing in connection logic (packages/inspector/src/connection.ts:44-62)
  • Optional properties handled correctly with ?. chaining and undefined checks

4. Security & Input Validation ⭐⭐⭐⭐

  • URL protocol validation: Only allows http:, https:, ws:, wss: (dashboard-server.ts:200-208)
  • Command validation: Requires non-empty command string for stdio (dashboard-server.ts:176-179)
  • Environment variable handling: Merges with process.env safely, filters undefined values (test-client.ts:46-52)
  • Error messages: Clear, actionable error messages without exposing internals

Issues & Suggestions

Critical Issues

None - No blocking issues found.

High Priority

1. Potential Command Injection Risk 🔴

Location: packages/testing/src/server/test-client.ts:44-59

The stdio transport accepts arbitrary command and args from user input and spawns child processes. While the current use case (inspector tool) may be trusted, this creates potential command injection vectors if:

  • User-provided commands contain shell metacharacters
  • Args are not properly escaped
  • Commands are constructed from untrusted sources

Current code:

transport = new StdioClientTransport({
  command: params.command,  // Could be "sh -c 'malicious code'"
  args: params.args,        // Could contain shell metacharacters
  env: mergedEnv,
  cwd: params.cwd,
  stderr: "pipe",
});

Recommendations:

  1. Add documentation warning about command injection risks in ConnectionParams JSDoc
  2. Consider validating command against an allowlist of known-safe executables (e.g., node, python, python3, deno)
  3. Add security note in dashboard UI when entering stdio commands
  4. Sanitize or validate args to prevent shell injection if command is shell-like

Mitigation: The StdioClientTransport likely uses spawn() (not exec()), which provides some protection, but this should be verified and documented.


2. Environment Variable Merging Could Leak Secrets

Location: packages/testing/src/server/test-client.ts:46-52

The code merges user-provided env with process.env, which could inadvertently expose sensitive environment variables to spawned processes:

const mergedEnv = params.env
  ? Object.fromEntries(
      Object.entries({ ...process.env, ...params.env }).filter(
        (entry): entry is [string, string] => entry[1] !== undefined
      )
    )
  : undefined;

Issue: If params.env is empty or partial, ALL of process.env (including AWS_SECRET_ACCESS_KEY, DATABASE_PASSWORD, etc.) will be passed to the child process.

Recommendations:

  1. Default to isolated environment: Only pass params.env without merging process.env
  2. If merging is required, use explicit allowlist: PATH, HOME, NODE_PATH, etc.
  3. Add JSDoc warning in ConnectionParams about environment variable exposure
  4. Consider adding isolateEnv?: boolean option to control merging behavior

Example fix:

const env = params.env 
  ? params.env  // Use only user-provided env (isolated)
  : undefined;  // Use default env (or allowlist specific vars)

Medium Priority

3. Missing Timeout Handling for Stdio Connections

Location: packages/inspector/src/connection.ts:216-230

The onTransportClose callback is registered for stdio, but there's no connection timeout for the initial spawn. If the child process hangs during startup, the connection will wait indefinitely.

Recommendation: Add timeout to createTestClient call for stdio transport (similar to HTTP timeout handling).


4. Auto-restart Counter Not Reset on Successful Reconnect

Location: packages/inspector/src/connection.ts:438-488

The autoRestartAttempts counter is reset on successful initial connect (line 298) but NOT on successful reconnect within handleStdioProcessExit. This means if a process crashes, restarts successfully, then crashes again, it will only get 2 more attempts (not 3).

Recommendation:

this.connect(params, options)
  .then(() => {
    // Reset attempt counter on successful reconnect
    this.autoRestartAttempts = 0;  // Add this line
    if (this.connectionGeneration !== generationAtStart) {
      // ...
    }
  })

5. Inconsistent Display Labels

Location: Multiple files

The stdio connection label format varies:

  • connection.ts:187: "stdio: command args" (space after colon)
  • connect.ts:125: "stdio:command args" (no space after colon)

Recommendation: Standardize on one format (suggest with space for readability).


Low Priority (Code Quality)

6. Type Assertions Could Be Safer

Location: packages/inspector/src/connection.ts:309-323

Uses type assertions as ExtendedTool instead of runtime validation. While acceptable given the source (MCP SDK), consider adding Zod runtime validation for extra safety.


7. Magic Numbers

Location: packages/inspector/src/connection.ts:113-114, 450

  • MAX_RESTART_ATTEMPTS = 3
  • Backoff formula: 1000 * Math.pow(2, this.autoRestartAttempts)

Recommendation: Extract to named constants with JSDoc explaining the backoff strategy.


8. Dashboard HTML Path Resolution

Location: packages/inspector/src/dashboard/dashboard-server.ts:51-64

The findDashboardHtml() function tries multiple paths, but if both fail, returns the first candidate without logging. Consider adding debug logging when fallback is used.


Performance Considerations

  • ✅ Auto-restart uses timers efficiently (cleared on disconnect)
  • ✅ Generation counter prevents race conditions without locks
  • ✅ Mock cleanup in tests prevents memory leaks
  • ⚠️ Consider: Stdio process spawning overhead for rapid connect/disconnect cycles

Documentation

Strengths:

  • Excellent JSDoc coverage in core files
  • Clear PR description with test plan
  • Inline comments explain complex logic (generation counters, backoff)

Suggestions:

  • Add security warnings to ConnectionParams JSDoc
  • Document the auto-restart behavior in user-facing docs
  • Add migration guide for users switching from HTTP to stdio

Alignment with Repo Standards

Follows AGENTS.md requirements:

  • TypeScript strict mode, no any types
  • Zod v4 schemas for validation
  • Proper error handling with typed errors
  • Tests added to owning package
  • No circular dependencies introduced
  • Export contract maintained (src/index.ts)

Code style:

  • Consistent formatting
  • Meaningful variable names
  • Proper error propagation

Final Verdict

This is excellent work that demonstrates strong engineering practices:

  • Thoughtful architecture with discriminated unions
  • Comprehensive test coverage (18 behavioral + E2E)
  • Production-ready resilience (auto-restart with backoff)
  • Clean backward compatibility

The security concerns (command injection, environment variable leakage) are the only blocking items that should be addressed before merge. Once mitigated with documentation and safer defaults, this PR is ready to ship.

Score: 8.5/10 (would be 9.5/10 with security hardening)


Recommended Actions Before Merge

  1. ✅ Add security warnings to ConnectionParams JSDoc (command injection, env exposure)
  2. ✅ Change env merging to isolated-by-default (breaking change alternative: add isolateEnv option)
  3. ✅ Reset autoRestartAttempts on successful reconnect
  4. ✅ Standardize display label format
  5. ⚠️ Consider command allowlist for production use cases

Great work @gabrypavanello! 🎉

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/inspector/src/connection.ts`:
- Around line 183-188: The stdio display label in the const label generation
uses "stdio: " (with a space) which is inconsistent with connectionDisplayLabel
in connect.ts that uses "stdio:"; update the label generation in the
params-handling code (the const label that checks params.transport === "http")
to use "stdio:" (no space) when building the string from params.command and
params.args so the format matches connectionDisplayLabel across the codebase.

In `@packages/inspector/src/dashboard/react/hooks/useServerHistory.ts`:
- Around line 20-25: The current matching logic in useServerHistory (where
transport, command and args are compared) uses args.join(" ").toLowerCase(),
which can collide different arrays and throws if persisted args contain
non-strings; change the comparison to perform a strict element-wise array
equality for args and add a type-guard that confirms every element is a string
before doing any toLowerCase/command comparisons. Update the matching code paths
referenced (the args comparison in the blocks around lines shown) to first
verify Array.isArray(entry.args) and entry.args.every(a => typeof a ===
"string"), then compare lengths and each element via normalized equality (e.g.,
compare lowercased strings per element) instead of join, and ensure you apply
the same guard in all places noted (the other blocks around 95-106 and 129-135).
🧹 Nitpick comments (6)
examples/minimal/tests/integration.test.ts (1)

14-15: Consider using port: 0 for random port assignment.

The hardcoded port 3004 can cause port conflicts when running multiple test suites in parallel. Using port: 0 allows the OS to assign an available port dynamically, and the actual port can be retrieved from the server instance.

♻️ Suggested refactor
-    const testPort = 3004;
-    const server = await startTestServer(app, { port: testPort });
+    const server = await startTestServer(app, { port: 0 });
+    const testPort = server.port;

Based on learnings: "Use port: 0 for random port assignment in test servers to avoid port conflicts when running multiple test suites."

packages/testing/tests/unit/server/test-client.test.ts (1)

21-26: Consider clarifying test description.

The test description says "invalid URL format" but port 99999 is actually an out-of-range port number (valid range is 0-65535), not technically an invalid URL format. The URL itself is syntactically valid.

Consider renaming to better reflect what's being tested:

📝 Suggested clarification
-    it("should throw ConnectionError for invalid URL format", async () => {
-      // Invalid URLs should throw connection errors
+    it("should throw ConnectionError for out-of-range port", async () => {
+      // Out-of-range ports (>65535) should throw connection errors
       await expect(
         createTestClient({ transport: "http", url: "http://localhost:99999/mcp" })
       ).rejects.toThrow(ConnectionError);
examples/minimal/tests/greet-v1.test.ts (1)

14-16: Consider using port: 0 for dynamic port assignment.

Using a hardcoded port (3001) can cause conflicts when running multiple test suites in parallel. Using port: 0 lets the OS assign an available port, which the test server should expose via server.port.

Based on learnings: "Use port: 0 for random port assignment in test servers to avoid port conflicts when running multiple test suites".

♻️ Suggested refactor for dynamic port assignment
   beforeAll(async () => {
-    const testPort = 3001;
-    const server = await startTestServer(app, { port: testPort });
+    const server = await startTestServer(app, { port: 0 });
     await new Promise((resolve) => setTimeout(resolve, 100));

     const client = await createTestClient(
-      { transport: "http", url: `http://localhost:${testPort}/v1/mcp` },
+      { transport: "http", url: `http://localhost:${server.port}/v1/mcp` },
       {
         trackHistory: true,
       }
     );
packages/inspector/tests/dom-events-full-flow.test.ts (1)

33-33: Consider using port: 0 for random port assignment.

The hardcoded port 16274 could cause conflicts when running multiple test suites in parallel. Using port: 0 allows the OS to assign an available port automatically.

♻️ Suggested change
-  const port = 16274; // Use a different port to avoid conflicts
+  const port = 0; // Let the OS assign an available port

Note: You would need to retrieve the actual port from the server after it starts (e.g., via server.getPort() or similar method if available).

Based on learnings: "Use port: 0 for random port assignment in test servers to avoid port conflicts when running multiple test suites"

packages/inspector/tests/connection-target-schema.test.ts (1)

44-67: Consider adding afterEach cleanup for the connection manager.

While each test creates a fresh ConnectionManager instance in beforeEach, the connected managers from previous tests may not be explicitly disconnected. Adding an afterEach hook to disconnect would ensure proper resource cleanup and prevent potential leaks.

♻️ Suggested addition after line 67
   });
+
+  afterEach(async () => {
+    try {
+      await manager.disconnect();
+    } catch {
+      // Ignore disconnect errors during cleanup
+    }
+  });

   describe("target schema capture", () => {

Based on learnings: "Clean up servers in tests to avoid resource leaks and hanging processes"

packages/inspector/src/dashboard/dashboard-server.ts (1)

19-19: Use a Zod v4 schema for ConnectionParams validation (incl. stdio args/env types).

The current manual checks allow non-string args/env values through and bypass the repo’s Zod-based validation requirement. A schema also centralizes legacy { url } normalization. (You can still map Zod errors to your existing messages if you need parity.)

♻️ Suggested refactor (Zod parsing & normalization)
-import type { ConnectionParams } from "@mcp-apps-kit/testing";
+import type { ConnectionParams } from "@mcp-apps-kit/testing";
+import { z } from "zod";
@@
+const urlSchema = z
+  .string()
+  .trim()
+  .min(1, "Missing url")
+  .refine((value) => {
+    try {
+      const parsed = new URL(value);
+      return ["http:", "https:", "ws:", "wss:"].includes(parsed.protocol);
+    } catch {
+      return false;
+    }
+  }, "Unsupported URL protocol. Use http, https, ws, or wss.");
+
+const connectionParamsSchema = z.union([
+  z.object({
+    transport: z.literal("stdio"),
+    command: z.string().trim().min(1, "Missing or empty command for stdio transport"),
+    args: z.array(z.string()).optional(),
+    env: z.record(z.string()).optional(),
+    cwd: z.string().optional(),
+  }),
+  z.object({ transport: z.literal("http"), url: urlSchema }),
+  z.object({ url: urlSchema }).transform((value) => ({
+    transport: "http",
+    url: value.url,
+  })),
+]);
@@
-      // Normalize to ConnectionParams (backward compat: { url } → { transport: "http", url })
-      let params: ConnectionParams;
-      const transport = (body.transport as string | undefined) ?? (body.url ? "http" : undefined);
-
-      if (transport === "stdio") {
-        // Validate stdio params
-        const command = body.command;
-        if (typeof command !== "string" || command.trim().length === 0) {
-          res.writeHead(400, { "Content-Type": "application/json" });
-          res.end(JSON.stringify({ error: "Missing or empty command for stdio transport" }));
-          return true;
-        }
-        params = {
-          transport: "stdio",
-          command: command.trim(),
-          ...(Array.isArray(body.args) ? { args: body.args as string[] } : {}),
-          ...(body.env && typeof body.env === "object"
-            ? { env: body.env as Record<string, string> }
-            : {}),
-          ...(typeof body.cwd === "string" ? { cwd: body.cwd } : {}),
-        };
-      } else if (transport === "http") {
-        // Validate HTTP/WS URL
-        const urlStr = body.url;
-        if (typeof urlStr !== "string" || urlStr.trim().length === 0) {
-          res.writeHead(400, { "Content-Type": "application/json" });
-          res.end(JSON.stringify({ error: "Missing url" }));
-          return true;
-        }
-        try {
-          const parsedUrl = new URL(urlStr);
-          const allowedProtocols = new Set(["http:", "https:", "ws:", "wss:"]);
-          if (!allowedProtocols.has(parsedUrl.protocol)) {
-            res.writeHead(400, { "Content-Type": "application/json" });
-            res.end(
-              JSON.stringify({
-                error: "Unsupported URL protocol. Use http, https, ws, or wss.",
-              })
-            );
-            return true;
-          }
-        } catch {
-          res.writeHead(400, { "Content-Type": "application/json" });
-          res.end(JSON.stringify({ error: "Invalid URL format" }));
-          return true;
-        }
-        params = { transport: "http", url: urlStr };
-      } else {
-        res.writeHead(400, { "Content-Type": "application/json" });
-        res.end(JSON.stringify({ error: "Missing transport type or url" }));
-        return true;
-      }
+      // Normalize + validate via Zod (backward compat: { url } → { transport: "http", url })
+      const parsed = connectionParamsSchema.safeParse(body);
+      if (!parsed.success) {
+        const message = parsed.error.issues[0]?.message ?? "Invalid connection parameters";
+        res.writeHead(400, { "Content-Type": "application/json" });
+        res.end(JSON.stringify({ error: message }));
+        return true;
+      }
+      const params = parsed.data as ConnectionParams;

As per coding guidelines: Always use Zod v4 for schema/validation; do not use Zod v3 assumptions.

Also applies to: 167-229

Comment thread packages/inspector/src/connection.ts
Comment on lines +20 to +25
/** Transport type — defaults to HTTP when absent (backward compat). */
transport?: "http" | "stdio";
/** stdio command (only when transport === "stdio"). */
command?: string;
/** stdio args (only when transport === "stdio"). */
args?: 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 | 🟡 Minor

Avoid stdio args collisions and guard non-string args during matching.

join(" ") can treat different arg arrays as identical (e.g., ["a b"] vs ["a","b"]), and toLowerCase() will throw if a persisted history entry contains non-string args. A strict array comparison plus a type guard avoids both issues.

🔧 Suggested fix
+const argsEqual = (left?: string[], right?: string[]): boolean => {
+  if (!left && !right) return true;
+  if (!left || !right) return false;
+  return left.length === right.length && left.every((value, index) => value === right[index]);
+};
@@
-      const filtered =
-        entry.transport === "stdio"
-          ? prev.filter(
-              (e) =>
-                !(
-                  e.transport === "stdio" &&
-                  e.command === entry.command &&
-                  (e.args?.join(" ") ?? "") === (entry.args?.join(" ") ?? "")
-                )
-            )
-          : prev.filter((e) => e.url !== entry.url);
+      const filtered =
+        entry.transport === "stdio"
+          ? prev.filter(
+              (e) =>
+                !(
+                  e.transport === "stdio" &&
+                  e.command === entry.command &&
+                  argsEqual(e.args, entry.args)
+                )
+            )
+          : prev.filter((e) => e.url !== entry.url);
@@
-        if (entry.args?.some((a) => a.toLowerCase().includes(lowerFilter))) return true;
+        if (entry.args?.some((a) => typeof a === "string" && a.toLowerCase().includes(lowerFilter)))
+          return true;

Also applies to: 95-106, 129-135

🤖 Prompt for AI Agents
In `@packages/inspector/src/dashboard/react/hooks/useServerHistory.ts` around
lines 20 - 25, The current matching logic in useServerHistory (where transport,
command and args are compared) uses args.join(" ").toLowerCase(), which can
collide different arrays and throws if persisted args contain non-strings;
change the comparison to perform a strict element-wise array equality for args
and add a type-guard that confirms every element is a string before doing any
toLowerCase/command comparisons. Update the matching code paths referenced (the
args comparison in the blocks around lines shown) to first verify
Array.isArray(entry.args) and entry.args.every(a => typeof a === "string"), then
compare lengths and each element via normalized equality (e.g., compare
lowercased strings per element) instead of join, and ensure you apply the same
guard in all places noted (the other blocks around 95-106 and 129-135).

Add mocked unit tests covering stdio/HTTP transport creation, env
merging, cwd forwarding, onTransportClose wiring, connection error
labels, and client operations (callTool, history, disconnect).

Branches coverage: 49.73% → 52.41% (above 50% threshold).

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/inspector/src/tools/connect.ts`:
- Around line 115-118: The fallback to an empty string for url in the HTTP
branch can mask missing-url bugs; update the branch in connect.ts (the code that
currently does const url = "url" in input ? input.url : "" and returns {
transport: "http", url }) to defensively ensure url exists: either assert the
narrowed type (e.g., use a non-null assertion on input.url) or explicitly throw
an error if "url" is not present, so createConnection receives a guaranteed
valid URL rather than an empty string. Ensure the change targets the HTTP
transport branch where transport is set to "http" and affects the object
returned to createConnection.

Comment thread packages/inspector/src/tools/connect.ts

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/testing/tests/unit/server/test-client.test.ts`:
- Around line 268-276: The test currently asserts the tool call result with
plain expect calls; update it to use the fluent tool assertion API by replacing
the assertions after calling client.callTool("test", {}) with
expectToolResult(result) and its fluent checks (e.g., .toBeDefined(),
.toHaveContent([]) or equivalent on the expectToolResult chain). Locate the test
around createTestClient(...) and client.callTool(...) and swap the plain
expect(result) / expect(result.content) assertions to the expectToolResult(...)
fluent assertions.

Comment on lines +268 to +276
it("should call tools", async () => {
const client = await createTestClient({
transport: "http",
url: "http://localhost:3000/mcp",
});
const result = await client.callTool("test", {});
expect(result).toBeDefined();
expect(result.content).toEqual([]);
});

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

Use expectToolResult() for tool execution assertions.
The tool call result is validated with plain expect, but the test guideline requires the fluent expectToolResult() API for tool results.

As per coding guidelines, "Use the fluent assertion API with expectToolResult() for validating tool execution results".

🤖 Prompt for AI Agents
In `@packages/testing/tests/unit/server/test-client.test.ts` around lines 268 -
276, The test currently asserts the tool call result with plain expect calls;
update it to use the fluent tool assertion API by replacing the assertions after
calling client.callTool("test", {}) with expectToolResult(result) and its fluent
checks (e.g., .toBeDefined(), .toHaveContent([]) or equivalent on the
expectToolResult chain). Locate the test around createTestClient(...) and
client.callTool(...) and swap the plain expect(result) / expect(result.content)
assertions to the expectToolResult(...) fluent assertions.

- Throw error for missing HTTP URL instead of falling back to empty
  string (connect.ts)
- Use element-wise args comparison in server history dedup instead of
  join(' ') which can collide different arrays (useServerHistory.ts)
- Reset autoRestartAttempts on successful reconnect so subsequent
  crashes get full retry budget (connection.ts)
- Wrap disconnect() in auto-restart catch with .catch() to prevent
  swallowed errors (connection.ts)
- Standardize stdio label format with space after colon (connect.ts)
@claude

claude Bot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

Code Review: stdio MCP Server Transport Support

I've completed a comprehensive review of PR #132. This is an excellent implementation that adds stdio transport support to the MCP Inspector with strong architecture, robust error handling, and comprehensive test coverage.


✅ Strengths

Architecture & Design

  • Clean discriminated union pattern for ConnectionParams type in packages/testing/src/types.ts:580-588 provides excellent type safety
  • Proper separation of concerns between transport layers (HTTP vs stdio) throughout the codebase
  • Auto-restart mechanism with exponential backoff (packages/inspector/src/connection.ts:439-494) is well-designed with generation counter to prevent race conditions
  • Backward compatibility maintained in connect tool - accepts legacy { url } format (packages/inspector/src/tools/connect.ts:51-54)

Error Handling

  • Comprehensive validation at API boundaries (packages/inspector/src/dashboard/dashboard-server.ts:176-179)
  • Graceful degradation when server doesn't support capabilities (packages/inspector/src/connection.ts:238-263)
  • Clear error messages with context (e.g., "Failed to spawn process" for ENOENT errors in packages/inspector/src/tools/connect.ts:182-184)
  • Auto-restart abort logic properly handles race conditions during disconnect (packages/inspector/src/connection.ts:464-470, 474-481)

Type Safety

  • Strict TypeScript usage with proper type narrowing throughout
  • Zod schemas for runtime validation in connect tool (packages/inspector/src/tools/connect.ts:26-68)
  • Extended types properly capture all metadata (_meta, annotations) for proxy scenarios (packages/testing/src/server/test-client.ts:172-188)

Test Coverage

  • 557 lines of behavioral tests covering 18 criteria (packages/inspector/tests/stdio-transport.test.ts)
  • Comprehensive edge cases tested: Auto-restart with exponential backoff (C5), Max retry limits (C6), Intentional disconnect prevention (C7), Backward compatibility (C10), Dashboard validation (C11-C13)
  • Mock strategy allows testing without spawning real processes while capturing transport params
  • Unit tests added for createTestClient with both transports (packages/testing/tests/unit/server/test-client.test.ts)

Frontend Implementation

  • Transport selector in ConnectionBar with conditional inputs (packages/inspector/src/dashboard/react/components/ConnectionBar.tsx:441-491)
  • Advanced settings panel for env vars and cwd (stdio only)
  • Server history deduplication by command+args for stdio entries (packages/inspector/src/dashboard/react/hooks/useServerHistory.ts:96-120)
  • Accessible UI with keyboard navigation (ArrowUp/Down, Enter, Escape)

🔍 Areas for Improvement

1. Security: Command Injection Prevention ⚠️

Location: packages/inspector/src/dashboard/dashboard-server.ts:176

Issue: The stdio command validation only checks for empty string, but doesn't sanitize or validate the command/args for potential injection attacks.

Recommendation:

  • Add validation to prevent path traversal (e.g., ../../)
  • Consider maintaining an allowlist of permitted commands or implementing stricter validation
  • Document security considerations in CLAUDE.md for future developers

2. Environment Variable Handling

Location: packages/testing/src/server/test-client.ts:45-52

Concerns:

  • User-provided env vars override process.env completely
  • Could accidentally expose sensitive environment variables
  • No filtering of sensitive vars (PATH, HOME, etc.)

Recommendation:

  • Document the security implications in the tool description
  • Consider filtering out sensitive env vars by default
  • Add a note in CONTRIBUTING.md about env var handling best practices

3. Test Isolation

Location: packages/inspector/tests/stdio-transport.test.ts:283

Recommendation:

  • Ensure all timer advances use vi.advanceTimersByTimeAsync (you're already doing this!)
  • Consider adding explicit test timeout configuration to catch infinite loops
  • Add a test that verifies cleanup happens when timers are running

4. Minor: Type Assertions

Location: packages/inspector/src/connection.ts:309-324

Recommendation:

  • Consider adding these fields to the upstream SDK types if they're part of the protocol spec
  • Add a comment explaining why type assertion is necessary here

5. Minor: Error Message Consistency

Location: packages/inspector/src/tools/connect.ts:170-186

Some error messages include the transport label, some don't. Consider standardizing for consistency.


📊 Code Quality Metrics

  • Lines changed: +1930 / -378
  • Test coverage: 18 behavioral tests + unit tests (excellent)
  • Files changed: 54 (well-distributed across packages)
  • TypeScript strictness: ✅ All strict mode enabled
  • Zod version: ✅ Using Zod v4 (as per AGENTS.md requirements)
  • No any types: ✅ Uses unknown with proper narrowing

🎯 Recommendations Summary

High Priority:

  1. Add command injection prevention validation
  2. Document env var security implications

Medium Priority:
3. Consider filtering sensitive environment variables
4. Add type comments for SDK extensions

Low Priority:
5. Standardize error message formats
6. Add test timeout guards for fake timers


✨ Overall Assessment

This is a high-quality implementation that demonstrates:

  • Strong understanding of TypeScript discriminated unions
  • Excellent test-driven development practices
  • Thoughtful error handling and edge case coverage
  • Clean separation of concerns between packages
  • Proper backward compatibility considerations

The auto-restart mechanism with generation counters to prevent race conditions is particularly well-designed. The test coverage is comprehensive and uses proper mocking strategies.

Verdict: ✅ APPROVE with minor security recommendations to address.

The code is production-ready. The security recommendations are important but don't block merging - they can be addressed in a follow-up PR if preferred.


Great work, @gabrypavanello! 🚀

- Replace inline collapsible with floating Popover component
- Settings gear button on right side of connection bar
- Popover: dark bg, shadow, z-1000, dismiss on click-outside/Escape
- Reusable Popover accepts panelStyle for future settings
- Button order: connect/disconnect → settings → close
- No header layout shift when settings are open
@claude

claude Bot commented Jan 31, 2026

Copy link
Copy Markdown
Contributor

Pull Request Review: stdio MCP Server Transport Support

Summary

This PR adds stdio transport support to the MCP Inspector, enabling connection to MCP servers via stdin/stdout in addition to the existing HTTP transport. The implementation is well-architected with a discriminated union pattern, comprehensive test coverage (18 behavioral tests + existing suite), and thoughtful auto-restart logic for stdio connections.

Code Quality Assessment: ⭐⭐⭐⭐⭐ Excellent

Strengths

  1. Clean Architecture

    • Single branch point at createTestClient() - all other code is transport-agnostic
    • Discriminated union ConnectionParams type provides type-safe transport handling
    • Consistent display label generation across all layers
    • Excellent separation of concerns between testing, inspector, and UI layers
  2. Type Safety

    • Strict TypeScript usage throughout (no any types)
    • Proper Zod v4 schema validation at API boundaries
    • Discriminated unions prevent invalid state combinations
  3. Backward Compatibility

    • Legacy { url } format still supported (defaults to HTTP)
    • All existing tests updated to new API without breaking changes
    • Dashboard API accepts both old and new formats
  4. Test Coverage

    • 18 new behavioral tests covering all stdio scenarios
    • Auto-restart logic thoroughly tested with generation counter pattern
    • All 28+ existing test files updated and passing

Areas of Excellence

Auto-Restart Implementation (connection.ts:439-494)

The auto-restart logic is exceptionally well-designed:

  • Exponential backoff (1s, 2s, 4s) prevents resource thrashing
  • Generation counter prevents stale restarts after explicit disconnect
  • Proper cleanup of timers on disconnect
  • Max 3 retry attempts before giving up

Transport Abstraction (test-client.ts:44-62)

Clean transport initialization with proper environment variable merging.

Tool Schema Design (connect.ts:64-68)

The discriminated union schema is perfectly structured with backward compatibility.

Potential Issues and Concerns

1. Security: Environment Variable Injection ⚠️ MEDIUM

Location: packages/testing/src/server/test-client.ts:46-52

The code merges user-provided environment variables with process.env for stdio processes. Users can override critical environment variables like PATH, LD_PRELOAD, NODE_OPTIONS, etc.

Recommendation:

  • Document that stdio connections should only be used with trusted commands
  • Consider allowlisting safe environment variables or blocklisting dangerous ones
  • Add a security note to the connection tool description
  • If exposed via network API, add authentication/authorization

Severity: Medium (depends on deployment context - low for local dev tools, high for network-exposed services)

2. Resource Cleanup: Orphaned Child Processes ⚠️ LOW-MEDIUM

Location: packages/testing/src/server/test-client.ts

While the code properly calls transport.close() on disconnect, there's no explicit process termination logic.

Recommendation:

  • Add explicit process cleanup in error paths
  • Consider tracking spawned process PIDs for manual cleanup on disconnect
  • Add process monitoring/cleanup in connection manager shutdown

3. Performance: No stdio stderr Logging ℹ️ INFO

Location: packages/testing/src/server/test-client.ts:58

The code sets stderr: "pipe" but doesn't capture/log stderr output.

Recommendation:

  • Add optional stderr logging when debug mode is enabled
  • Consider passing stderr through to inspector logs

4. UI/UX: Server History Deduplication ℹ️ INFO

Location: packages/inspector/src/dashboard/react/hooks/useServerHistory.ts

Recommendation:

  • Add history size limit (e.g., max 50 entries)
  • Consider including env vars in deduplication key
  • Add "Clear History" button in UI

Security Assessment

Current Security Posture: ⚠️ MODERATE

Threat Model:

  • Local Dev Tool (Low Risk): Current security is adequate
  • Network Service (High Risk): Environment variable injection is a serious concern

Recommendations:

  1. Add security documentation explaining trusted vs. untrusted usage
  2. Consider adding an allowlist mode for production deployments
  3. Add rate limiting on connection attempts
  4. Validate command paths

Test Coverage: ⭐⭐⭐⭐⭐ Excellent

New Test File: stdio-transport.test.ts

Coverage Analysis:

  • ✅ Transport params forwarding (C1-C2)
  • ✅ Auto-restart with backoff (C3-C7)
  • ✅ Generation counter prevents stale restarts (C8-C9)
  • ✅ Max retry limits (C10)
  • ✅ Backward compatibility (C11-C13)

Test Quality:

  • Proper mocking with vi.hoisted() pattern
  • Comprehensive edge case coverage
  • Clear test names and documentation

Best Practices Adherence

✅ Repository Standards (per AGENTS.md)

  1. Zod v4: ✅ All schemas use Zod 4
  2. No any types: ✅ All code strictly typed
  3. Export contract: ✅ Types properly exported through index.ts
  4. Test coverage: ✅ New tests added, thresholds maintained
  5. No circular deps: ✅ Clean dependency graph

Recommendations Summary

Must Fix (Before Merge)

None - the PR is production-ready for local dev tool usage.

Should Fix (Security/Robustness)

  1. Security Warning: Add documentation about env var injection risks
  2. Process Cleanup: Add explicit SIGTERM handling for orphaned processes
  3. stderr Logging: Capture and log stderr in debug mode

Nice to Have (Future Enhancement)

  1. History size limits in UI
  2. Command path validation/allowlisting for production use
  3. Real-time stdio output streaming to dashboard

Final Verdict

Recommendation: ✅ APPROVE WITH MINOR SUGGESTIONS

This is an exceptionally well-implemented feature that:

  • Maintains backward compatibility
  • Has comprehensive test coverage (813 existing + 18 new tests)
  • Follows all repository conventions
  • Includes thoughtful auto-restart logic
  • Has clean architecture with type-safe abstractions

The security considerations around environment variables are worth addressing in documentation, but don't block the merge for a local development tool. The code quality is outstanding and demonstrates excellent software engineering practices.

Suggested Action Items (Post-Merge)

  1. Add security documentation for stdio transport
  2. Implement stderr logging in debug mode
  3. Add process cleanup hardening
  4. Consider command allowlisting for future network deployment

Diff Statistics

  • 54 files changed
  • 2,020 additions, 377 deletions
  • Net +1,643 lines (mostly comprehensive tests)

Complexity Assessment: High complexity feature delivered with excellent quality. Well done! 🎉

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/inspector/src/dashboard/react/components/ConnectionBar.tsx (2)

463-503: ⚠️ Potential issue | 🟡 Minor

Guard history-triggered connects while isCreating is true.

The dropdown-triggered paths in handleKeyDown (Enter with selection) and handleSelectHistory call onCreateConnection directly without checking isCreating, allowing duplicate requests when a user selects history while a connection is already being created. The main handleCreate path properly gates on isCreating, but these two paths bypass that check. Add guards and include isCreating in the dependency arrays.

🔧 Suggested fix
 const handleKeyDown = useCallback(
   (e: React.KeyboardEvent) => {
     if (e.key === "Enter" && showDropdown && hoveredIndex >= 0) {
       e.preventDefault();
+      if (isCreating) return;
       const entry = filteredHistory[hoveredIndex];
       if (entry) {
         const params = applyHistoryEntry(entry);
         setShowDropdown(false);
         void onCreateConnection(params);
       }
     } else if (e.key === "Enter") {
       void handleCreate();
     } else if (e.key === "Escape") {
       setShowDropdown(false);
       inputRef.current?.blur();
     } else if (e.key === "ArrowDown" && showDropdown) {
       e.preventDefault();
       setHoveredIndex((prev) => Math.min(prev + 1, filteredHistory.length - 1));
     } else if (e.key === "ArrowUp" && showDropdown) {
       e.preventDefault();
       setHoveredIndex((prev) => Math.max(prev - 1, 0));
     }
   },
-  [handleCreate, showDropdown, hoveredIndex, filteredHistory, onCreateConnection, applyHistoryEntry]
+  [
+    handleCreate,
+    showDropdown,
+    hoveredIndex,
+    filteredHistory,
+    onCreateConnection,
+    applyHistoryEntry,
+    isCreating,
+  ]
 );

 const handleSelectHistory = useCallback(
   (entry: ServerHistoryEntry) => {
+    if (isCreating) return;
     const params = applyHistoryEntry(entry);
     setShowDropdown(false);
     void onCreateConnection(params);
   },
-  [onCreateConnection, applyHistoryEntry]
+  [onCreateConnection, applyHistoryEntry, isCreating]
 );

363-569: ⚠️ Potential issue | 🟡 Minor

Use a timeout ref to prevent blur from closing dropdown when switching between stdio inputs.

In stdio mode, when transitioning between the command and args inputs, the blur timeout from the first input can fire after the second is focused, unexpectedly closing the dropdown. Track the timeout with a ref and clear any pending timeout on focus.

🔧 Suggested fix
   const inputRef = useRef<HTMLInputElement>(null);
   const containerRef = useRef<HTMLDivElement>(null);
   const settingsButtonRef = useRef<HTMLButtonElement>(null);
+  const blurTimeoutRef = useRef<number | null>(null);
+
+  const handleInputFocus = useCallback(() => {
+    if (blurTimeoutRef.current !== null) {
+      window.clearTimeout(blurTimeoutRef.current);
+      blurTimeoutRef.current = null;
+    }
+    setIsFocused(true);
+  }, []);
+
+  const handleInputBlur = useCallback(() => {
+    if (blurTimeoutRef.current !== null) {
+      window.clearTimeout(blurTimeoutRef.current);
+    }
+    blurTimeoutRef.current = window.setTimeout(() => setIsFocused(false), 200);
+  }, []);
+
+  useEffect(() => {
+    return () => {
+      if (blurTimeoutRef.current !== null) {
+        window.clearTimeout(blurTimeoutRef.current);
+      }
+    };
+  }, []);
-              onFocus={() => setIsFocused(true)}
-              onBlur={() => setTimeout(() => setIsFocused(false), 200)}
+              onFocus={handleInputFocus}
+              onBlur={handleInputBlur}
-                onFocus={() => setIsFocused(true)}
-                onBlur={() => setTimeout(() => setIsFocused(false), 200)}
+                onFocus={handleInputFocus}
+                onBlur={handleInputBlur}
-                onFocus={() => setIsFocused(true)}
-                onBlur={() => setTimeout(() => setIsFocused(false), 200)}
+                onFocus={handleInputFocus}
+                onBlur={handleInputBlur}
🧹 Nitpick comments (1)
packages/inspector/src/dashboard/react/components/ConnectionBar.tsx (1)

13-13: Consider relocating ConnectionParams to a shared package instead of @mcp-apps-kit/testing.

Pulling a production UI type from a testing package creates an upward dependency and risks circular or dev-only coupling. Prefer moving the shared type into a dedicated shared package and importing it from there.
As per coding guidelines: "packages/**/*.{ts,tsx}: Avoid cross-package circular dependencies; move shared types/utilities down or into a dedicated package instead of importing up".

…sistency, UI guards

- Standardize stdio display label to 'stdio:command args' (no space after colon)
  across connection.ts, connect.ts, test-client.ts and all tests
- Hoist argsEqual() to module scope in useServerHistory.ts for proper
  element-wise array comparison (prevents join collision)
- Add type guard for non-string args in history filter
- Add isCreating guard on handleKeyDown and handleSelectHistory in
  ConnectionBar.tsx to prevent duplicate connection requests
- Add JSDoc security warning on ConnectionParams.env about process.env merge
- Add inline SECURITY NOTE in test-client.ts env merging code
@claude

claude Bot commented Jan 31, 2026

Copy link
Copy Markdown
Contributor

Pull Request Review: stdio MCP Server Transport Support

Overview

This PR adds stdio transport support to the MCP Inspector, allowing connections to MCP servers via stdin/stdout in addition to HTTP. The implementation is well-structured with comprehensive tests and follows the repository's strict TypeScript and architectural standards.


✅ Strengths

Architecture & Design

  • Clean discriminated union pattern: ConnectionParams type uses proper TypeScript discriminated unions with transport as the discriminator
  • Single branch point strategy: Excellent design - transport branching happens once in createTestClient(), making all downstream code transport-agnostic
  • Backward compatibility: Legacy { url } input is maintained, defaulting to HTTP transport
  • Auto-restart with exponential backoff: Intelligent reconnection logic for stdio connections with generation counter to prevent stale restarts (1s, 2s, 4s delays, max 3 attempts)

Code Quality

  • Type safety: Strict TypeScript throughout, no any types, proper discriminated union handling
  • Error handling: Comprehensive validation at API boundaries (URL validation, command validation, empty command check)
  • Resource management: Proper cleanup of timers, connection state, and transport handlers
  • Test coverage: 18 new behavioral tests + comprehensive updates to 28+ existing test files

Security & Best Practices

  • Environment variable documentation: Clear security note in test-client.ts:45-47 about env merging behavior
  • Input validation: Command trimming and non-empty checks prevent empty subprocess spawns
  • State management: Generation counter pattern prevents race conditions during reconnect

🔍 Issues & Concerns

1. Security: Environment Variable Merging (MEDIUM PRIORITY)

Location: packages/testing/src/server/test-client.ts:49-54

Issue: The current implementation merges user-provided env vars with the entire process.env, potentially exposing sensitive parent environment variables (API keys, credentials, tokens) to child processes.

Risk:

  • Sensitive environment variables from the Inspector process could leak to arbitrary MCP servers
  • Malicious or compromised MCP servers could access credentials not intended for them

Recommendation:

// Option 1: Explicit allowlist approach
const SAFE_ENV_VARS = ['PATH', 'HOME', 'USER', 'LANG'];
const safeParentEnv = Object.fromEntries(
  Object.entries(process.env).filter(([key]) => SAFE_ENV_VARS.includes(key))
);
const mergedEnv = params.env
  ? { ...safeParentEnv, ...params.env }
  : undefined;

Consider adding a inheritEnv flag to ConnectionParams with default false, or at minimum add prominent security warnings in documentation.


2. Error Handling: Silent Reconnect Failures (MEDIUM PRIORITY)

Location: packages/inspector/src/connection.ts:485-492

Issue: Auto-restart failures are silently swallowed without notifying the user or emitting events.

Impact: Users won't know why their stdio connection disappeared.

Recommendation:

  • Emit an event when auto-restart exhausts retries
  • Add a user-visible notification in the Dashboard UI
  • Consider exposing connection state ("reconnecting", "failed") via the API

3. Type Safety: Missing Zod Validation for Dashboard POST (LOW PRIORITY)

Location: packages/inspector/src/dashboard/dashboard-server.ts

The dashboard POST endpoint parses JSON body manually but doesn't validate it with Zod before passing to createConnection. While the connection layer validates inputs, adding Zod validation at the HTTP boundary would provide better error messages.


4. UI: Environment Variables Input Parsing (LOW PRIORITY)

Location: packages/inspector/src/dashboard/react/components/ConnectionBar.tsx:20-34

Issues:

  • No support for quoted values with = inside (e.g., KEY="value=with=equals")
  • No escape sequence support
  • No error feedback for malformed input

Recommendation: Consider using a more robust parser or document the expected format clearly in the UI placeholder text.


5. Race Condition: Auto-restart Generation Counter (LOW PRIORITY)

Location: packages/inspector/src/connection.ts:456-481

The generation counter pattern is good, but there's a subtle race between disconnect and timer firing. Current mitigation with if (\!this.state.connected) return provides defense, but consider documenting this edge case.


📋 Test Coverage

Excellent Coverage

  • ✅ 18 behavioral tests covering all transport paths
  • ✅ Auto-restart logic verification
  • ✅ Backward compatibility tests
  • ✅ Input validation tests
  • ✅ E2E Playwright test against real stdio server

Missing Tests (Suggested for follow-up)

  • Security tests for environment variable isolation
  • Concurrent connection tests (multiple stdio connections)
  • Resource leak tests (verify process cleanup on disconnect)
  • UI component unit tests for env string parsing

🔐 Security Review

Approved Patterns

  • ✅ Input validation at API boundaries
  • ✅ No command injection vulnerabilities (using array args, not shell)
  • ✅ Proper subprocess stderr handling ('pipe')

Concerns

  • ⚠️ Environment variable merging (see Issue Add Claude Code GitHub Workflow #1 above)
  • ⚠️ No subprocess resource limits (CPU, memory, file descriptors)
  • ⚠️ Working directory (cwd) allows arbitrary filesystem access

Recommendation: Document security model for stdio connections:

  • Add SECURITY.md section about stdio transport risks
  • Consider sandboxing options for production deployments
  • Add warning in UI when connecting to stdio servers

📝 Documentation

Present

  • ✅ Clear PR description with architecture overview
  • ✅ Inline code comments explaining key design decisions
  • ✅ TypeScript types serve as documentation

Missing (Suggested for follow-up)

  • User-facing docs on when to use stdio vs HTTP
  • Examples of common stdio server configurations
  • Security guidance for stdio connections
  • Migration guide from HTTP-only setups

✨ Best Practices Followed

  1. ✅ Strict TypeScript with discriminated unions
  2. ✅ Zod v4 schema validation
  3. ✅ No any types in production code
  4. ✅ Proper export contract through src/index.ts
  5. ✅ Comprehensive test coverage
  6. ✅ Backward compatibility maintained
  7. ✅ Single responsibility principle (transport logic isolated)
  8. ✅ Proper resource cleanup

🚀 Recommendations

Must Address (Before Merge)

  1. Environment variable security (Issue Add Claude Code GitHub Workflow #1) - either implement isolation or add prominent security warnings in code comments and documentation
  2. Error visibility for auto-restart failures (Issue Fix documentation misalignment in codebase #3) - at minimum, emit events that UI can consume

Should Fix (Follow-up PR)

  1. Add Zod validation at dashboard HTTP boundary (Issue Fix linting errors and warnings #4)
  2. Improve env string parsing with better error messages (Issue Add GitHub Actions workflow for tests and linting #5)
  3. Add security documentation for stdio transport
  4. Add subprocess resource cleanup tests

Nice to Have

  1. Add user-facing documentation
  2. Add telemetry for reconnection patterns
  3. Consider subprocess resource limits
  4. Add UI notifications for connection state changes

📊 Summary

Category Rating Notes
Code Quality ⭐⭐⭐⭐⭐ Excellent architecture, clean types, proper patterns
Test Coverage ⭐⭐⭐⭐☆ Comprehensive behavioral tests, missing some edge cases
Security ⭐⭐⭐☆☆ Good input validation, env merging needs attention
Performance ⭐⭐⭐⭐☆ Solid auto-restart logic, proper cleanup
Documentation ⭐⭐⭐☆☆ Good inline docs, missing user guides
Backward Compat ⭐⭐⭐⭐⭐ Perfect - legacy input handled gracefully

🎯 Final Verdict

APPROVE with minor security documentation required

This is a well-engineered feature with excellent architecture and comprehensive testing. The discriminated union pattern and transport-agnostic design are textbook examples of good TypeScript practices.

The main concern is the environment variable merging behavior in packages/testing/src/server/test-client.ts:49-54, which should either be:

  1. Changed to be more restrictive by default (recommended), OR
  2. Clearly documented with security warnings in code and user-facing docs

The code quality is excellent and follows all repository standards (strict TypeScript, Zod v4, no any, proper exports, comprehensive tests). With the environment variable security addressed (either via code or documentation), this is ready to merge.

Estimated Risk: MEDIUM → LOW (after addressing env security)


Files Reviewed: 54 changed files (2037 additions, 377 deletions)

  • Core implementation: packages/testing/src/, packages/inspector/src/
  • Tests: packages/inspector/tests/stdio-transport.test.ts + 28 updated test files
  • UI: packages/inspector/src/dashboard/react/components/ConnectionBar.tsx

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/inspector/src/connection.ts`:
- Around line 459-484: The auto-restart aborts because connect() calls
disconnect() which increments connectionGeneration and mismatches
generationAtStart; fix by adding a way to suppress bumping connectionGeneration
when disconnect() is invoked as part of an internal reconnect flow: update
disconnect() to accept an options flag (e.g. preserveGeneration or
suppressGenerationIncrement) and change connect() (and handleStdioProcessExit()
if it triggers reconnect) to call disconnect({preserveGeneration: true}) during
the auto-restart path so connectionGeneration remains equal to generationAtStart
and the post-connect generation check in the autoRestartTimer callback will not
incorrectly abort; ensure autoRestartAttempts reset logic remains unchanged.
🧹 Nitpick comments (1)
packages/inspector/src/dashboard/react/components/ConnectionBar.tsx (1)

409-426: Consider preserving quoted stdio args.
Whitespace splitting drops quoted/escaped args (e.g., --name "a b"). If you expect such cases, a minimal quoted-arg parser keeps intent.

♻️ Optional tweak
-      ...(stdioArgs.trim() ? { args: stdioArgs.trim().split(/\s+/) } : {}),
+      ...(stdioArgs.trim() ? { args: parseQuotedArgs(stdioArgs.trim()) } : {}),
function parseQuotedArgs(raw: string): string[] {
  const matches = raw.match(/(?:[^\s"]+|"[^"]*")+/g);
  return matches ? matches.map((arg) => arg.replace(/^"|"$/g, "")) : [];
}

Comment on lines +459 to +484
const generationAtStart = this.connectionGeneration;

this.autoRestartTimer = setTimeout(() => {
this.autoRestartTimer = null;

// Abort if disconnect() was called while we were waiting
if (this.connectionGeneration !== generationAtStart) {
if (this.debug) {
console.log(`[inspector] Auto-restart aborted: disconnect called during backoff`);
}
return;
}

this.connect(params, options)
.then(() => {
// Abort if disconnect() was called while connect() was in-flight
if (this.connectionGeneration !== generationAtStart) {
if (this.debug) {
console.log(`[inspector] Auto-restart aborted: disconnect called during reconnect`);
}
void this.disconnect();
return;
}
// Reset attempt counter on successful reconnect
this.autoRestartAttempts = 0;
})

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 | 🟠 Major

Auto-restart will always abort after a successful reconnect.
When stdio exits, handleStdioProcessExit() calls connect() while state.connected is still true. connect() then calls disconnect(), which bumps connectionGeneration; the subsequent generation check treats that as a user disconnect and immediately disconnects, so restarts never stick.

🛠️ Suggested fix (avoid generation bump during auto-restart)
     this.autoRestartTimer = setTimeout(() => {
       this.autoRestartTimer = null;

       // Abort if disconnect() was called while we were waiting
       if (this.connectionGeneration !== generationAtStart) {
         if (this.debug) {
           console.log(`[inspector] Auto-restart aborted: disconnect called during backoff`);
         }
         return;
       }

+      const previousClient = this.state.client;
+      this.state.connected = false;
+      this.state.client = null;
+      void previousClient?.disconnect().catch(() => {
+        /* best-effort cleanup */
+      });
+
       this.connect(params, options)
         .then(() => {
           // Abort if disconnect() was called while connect() was in-flight
           if (this.connectionGeneration !== generationAtStart) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const generationAtStart = this.connectionGeneration;
this.autoRestartTimer = setTimeout(() => {
this.autoRestartTimer = null;
// Abort if disconnect() was called while we were waiting
if (this.connectionGeneration !== generationAtStart) {
if (this.debug) {
console.log(`[inspector] Auto-restart aborted: disconnect called during backoff`);
}
return;
}
this.connect(params, options)
.then(() => {
// Abort if disconnect() was called while connect() was in-flight
if (this.connectionGeneration !== generationAtStart) {
if (this.debug) {
console.log(`[inspector] Auto-restart aborted: disconnect called during reconnect`);
}
void this.disconnect();
return;
}
// Reset attempt counter on successful reconnect
this.autoRestartAttempts = 0;
})
const generationAtStart = this.connectionGeneration;
this.autoRestartTimer = setTimeout(() => {
this.autoRestartTimer = null;
// Abort if disconnect() was called while we were waiting
if (this.connectionGeneration !== generationAtStart) {
if (this.debug) {
console.log(`[inspector] Auto-restart aborted: disconnect called during backoff`);
}
return;
}
const previousClient = this.state.client;
this.state.connected = false;
this.state.client = null;
void previousClient?.disconnect().catch(() => {
/* best-effort cleanup */
});
this.connect(params, options)
.then(() => {
// Abort if disconnect() was called while connect() was in-flight
if (this.connectionGeneration !== generationAtStart) {
if (this.debug) {
console.log(`[inspector] Auto-restart aborted: disconnect called during reconnect`);
}
void this.disconnect();
return;
}
// Reset attempt counter on successful reconnect
this.autoRestartAttempts = 0;
})
🤖 Prompt for AI Agents
In `@packages/inspector/src/connection.ts` around lines 459 - 484, The
auto-restart aborts because connect() calls disconnect() which increments
connectionGeneration and mismatches generationAtStart; fix by adding a way to
suppress bumping connectionGeneration when disconnect() is invoked as part of an
internal reconnect flow: update disconnect() to accept an options flag (e.g.
preserveGeneration or suppressGenerationIncrement) and change connect() (and
handleStdioProcessExit() if it triggers reconnect) to call
disconnect({preserveGeneration: true}) during the auto-restart path so
connectionGeneration remains equal to generationAtStart and the post-connect
generation check in the autoRestartTimer callback will not incorrectly abort;
ensure autoRestartAttempts reset logic remains unchanged.

Replace full process.env merge with explicit safe-var allowlist (PATH, HOME,
LANG, NODE_ENV, XDG dirs, etc.) when spawning stdio child processes.
Sensitive variables (API keys, credentials, tokens) are never inherited
automatically.

Add inheritEnv flag to ConnectionParams for explicit control:
- When env is provided: only safe parent vars + user overrides (default)
- When inheritEnv=false: only safe parent vars
- When neither env nor inheritEnv set: Node.js default (full process.env)

Addresses PR #132 security review comment on env var leakage.
@claude

claude Bot commented Jan 31, 2026

Copy link
Copy Markdown
Contributor

Code Review: stdio MCP Transport Support

Summary

This PR adds stdio transport support to the MCP Inspector, enabling connections to MCP servers via stdin/stdout in addition to HTTP. The implementation is well-architected with strong type safety, comprehensive testing, and thoughtful error handling.

✅ Strengths

1. Excellent Architecture & Type Safety

  • Clean discriminated union ConnectionParams type provides compile-time transport safety
  • Single branch point at createTestClient() keeps transport logic isolated
  • Backward compatibility maintained via Zod union schema accepting legacy { url } format
  • Type system prevents mixing stdio/HTTP parameters (e.g., can't pass command to HTTP transport)

2. Security-First Design

The environment variable filtering in test-client.ts:47-73 is exemplary:

const SAFE_ENV_VARS = new Set([
  "PATH", "HOME", "LANG", "NODE_ENV", ...
]);
const safeParentEnv = Object.fromEntries(
  Object.entries(process.env).filter(
    (entry): entry is [string, string] =>
      entry[1] !== undefined && SAFE_ENV_VARS.has(entry[0])
  )
);

This prevents accidental credential leakage to child processes. Clear documentation explains the security model.

3. Robust Auto-Restart Logic

  • Exponential backoff (1s, 2s, 4s) with max 3 retries prevents thundering herd
  • Generation counter prevents stale reconnects after disconnect() - sophisticated race condition handling
  • Only applies to stdio (HTTP doesn't need it)
  • Well-tested edge cases (see stdio-transport.test.ts:264-360)

4. Comprehensive Test Coverage

  • 18 behavioral verification tests covering all transport paths
  • Mock-based unit tests avoid spawning real processes during test runs
  • Playwright E2E confirmed against real MCP servers
  • Tests validate backward compatibility, validation, and auto-restart logic

5. User Experience

  • Display labels clearly show transport type: "stdio:node server.js" vs HTTP URL
  • Frontend transport dropdown with conditional inputs (URL for HTTP, command/args for stdio)
  • Server history deduplicates stdio entries by command+args
  • Advanced settings panel for env vars and working directory (stdio only)

🔍 Code Quality Observations

Positive

  • Follows repo conventions (Zod v4, strict TypeScript, no any types)
  • Proper error handling with helpful messages for spawn failures, timeouts
  • Resource cleanup in all code paths (timers cleared, connections closed)
  • Debug logging for troubleshooting without noise in production

Areas for Consideration

1. Environment Variable Inheritance Logic (Minor Complexity)

test-client.ts:75-79:

const mergedEnv = params.env
  ? { ...safeParentEnv, ...params.env }
  : params.inheritEnv === false
    ? safeParentEnv
    : undefined;

The ternary logic here is correct but dense. Consider extracting to a named function for clarity:

function buildChildEnv(params: StdioParams, safeParentEnv: Record<string, string>) {
  if (params.env) {
    // User provided explicit env: merge with safe parent vars
    return { ...safeParentEnv, ...params.env };
  }
  if (params.inheritEnv === false) {
    // Explicitly disabled: only safe parent vars
    return safeParentEnv;
  }
  // Default Node.js behavior: inherit full process.env
  return undefined;
}

Impact: Low - current code works correctly, just harder to parse at a glance.

2. Race Condition Edge Case in Auto-Restart (Theoretical)

connection.ts:461-493:

The generation check happens at two points:

  • After timeout (line 465)
  • After connect() completes (line 475)

There's a tiny window between lines 472-475 where connect() succeeds but disconnect() could be called before the generation check. The code handles this correctly (line 479 calls disconnect() again), but the double-disconnect could be more explicit.

Impact: Very Low - works correctly, just slightly non-obvious control flow.

3. Frontend Input Validation (Missing Client-Side Check)

ConnectionBar.tsx doesn't validate command is non-empty before calling onCreateConnection. This means validation only happens server-side, resulting in a round-trip for invalid input.

Recommendation: Add client-side validation:

const isValid = transport === 'http' 
  ? urlInput.trim() !== '' 
  : commandInput.trim() !== '';

Impact: Low - server-side validation catches it, but poor UX for empty input.

4. Error Message Consistency (Minor Polish)

connect.ts:182-184:

if (message.includes("ENOENT") || message.includes("spawn")) {
  throw new Error(`Failed to spawn process: ${message}`);
}

ENOENT typically means "command not found". Consider more specific message:

throw new Error(`Command not found or failed to spawn: ${message}`);

Impact: Very Low - nice-to-have for better user feedback.

🔒 Security Analysis

✅ No Concerns Identified

  • Environment filtering prevents credential leakage
  • No command injection vectors (args passed as array, not shell string)
  • Input validation at API boundaries (empty command rejected)
  • No arbitrary file system access beyond cwd parameter
  • CORS headers properly set for dashboard endpoints

🚀 Performance Considerations

✅ Good Practices

  • Exponential backoff prevents reconnect storms
  • Generation counter prevents unnecessary work for stale reconnects
  • History limited to maxHistorySize (1000) to prevent memory leaks
  • Timer cleanup prevents leak in callToolWithRetry (lines 107, 131, 169)

Potential Optimization

Frontend server history could grow unbounded. Consider adding a MAX_HISTORY_ENTRIES limit in useServerHistory.ts.

Impact: Very Low - unlikely to be a real-world issue.

📋 Test Coverage Assessment

Covered ✅

  • Transport parameter forwarding (C1-C2)
  • Connection state management (C3-C4)
  • Auto-restart logic with backoff (C5-C7)
  • ConnectionRegistry integration (C8)
  • Tool API backward compatibility (C9-C10)
  • Dashboard POST validation (C11-C13)
  • E2E with real MCP server (Playwright)

Missing (Optional Enhancements)

  • Error handling for malformed stdio server responses
  • Resource cleanup on forceful process termination (SIGKILL)
  • Browser history persistence (localStorage)

Verdict: Coverage is excellent for core functionality. Missing tests are edge cases.

🎯 Adherence to Repository Standards

✅ Compliant

  • Uses Zod v4 (not v3) ✓
  • No any types in production code ✓
  • Strict TypeScript settings ✓
  • Exports flow through src/index.ts
  • Tests added to owning package ✓
  • await next() pattern followed (N/A for this PR) ✓

⚠️ Build/Test Status Unknown

Per AGENTS.md:53, must run pnpm build && pnpm test && pnpm lint && pnpm typecheck before finishing. I cannot verify this passed due to permission constraints. Please confirm CI is green.

🐛 Potential Bugs

None Critical

Minor observation: connection.ts:271-283 uses type assertion to access getServerVersion() which may not exist on all SDK client versions. Code handles this gracefully with try-catch, so no actual bug.

📝 Documentation Quality

✅ Excellent

  • JSDoc comments explain complex logic (auto-restart, env filtering)
  • PR description includes architecture overview, testing details
  • Security model documented inline
  • Type definitions are self-documenting

Suggestion

Add example to @mcp-apps-kit/testing README showing stdio usage:

const client = await createTestClient({
  transport: 'stdio',
  command: 'node',
  args: ['server.js'],
  env: { DEBUG: 'true' }
});

✨ Final Recommendation

APPROVE with minor suggestions

This is high-quality work that follows best practices. The discriminated union architecture, security-first env handling, and comprehensive testing demonstrate strong engineering. The suggestions above are polish items, not blockers.

Before Merging

  1. ✅ Verify CI passes (build, test, lint, typecheck)
  2. ✅ Confirm Playwright E2E tests pass
  3. 🔧 Consider adding client-side validation for empty command (5-minute fix)
  4. 🔧 Consider extracting buildChildEnv() helper (5-minute refactor)

Great work on this feature! The stdio transport significantly improves the Inspector's usability for local development workflows.

@gabrypavanello
gabrypavanello merged commit 4072dae into main Jan 31, 2026
4 checks passed
@gabrypavanello
gabrypavanello deleted the feat/stdio-support branch January 31, 2026 11:48
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