Skip to content

Make reasonix mcp inspect errors actionable #101

@esengine

Description

@esengine

Background

reasonix mcp inspect <spec> connects to one MCP server and prints its capabilities + tools / resources / prompts. When the connection fails, the user sees a bare error string with no recovery hint:

$ reasonix mcp inspect npx-typo=npx -y @nonexistent/server
mcp inspect failed: spawn npx-typo ENOENT

spawn ENOENT reads as gibberish to anyone who hasn't debugged Node child processes before. The fix is the same shape as #16 (web tool errors): every error string should end with a short " — try: …" tail telling the user what action would unstick them.

This is the inspect-side parallel to #16; the patterns line up so the two PRs can borrow phrasing.

Current behaviour

  • CLI registration: src/cli/index.ts:404
  • The try/catch at src/cli/index.ts:411-416 re-emits the raw err.message:
} catch (err) {
  process.stderr.write(`mcp inspect failed: ${(err as Error).message}\n`);
  process.exit(1);
}
  • Failure paths that flow through here:
    • parseMcpSpec throws on empty / malformed input (src/mcp/spec.ts)
    • StdioTransport throws spawn <cmd> ENOENT when the command doesn't exist
    • SseTransport / StreamableHttpTransport throw on connection-refused, DNS, TLS
    • client.initialize() throws on handshake timeout or protocol-version mismatch
    • inspectMcpServer throws on internal protocol errors

Expected behaviour

Wrap the catch in a small classifier so each common cause gets a tailored hint. Examples:

  • spawn <cmd> ENOENTcommand "<cmd>" not found — try: install the package first (e.g. \npx --version`), or check the spec's command spelling`
  • ECONNREFUSEDcould not connect to <url> — try: confirm the server is running and the port matches
  • handshake timeout → MCP handshake timed out — try: confirm the target speaks MCP (some servers only emit JSON-RPC after stdin is closed); pass --json for raw output if you want to debug further
  • empty / malformed spec from parseMcpSpec → leave the existing message but suffix with — try: \name=command args` or an http(s):// URL`
  • everything else → unchanged tail (mcp inspect failed: <message>)

Files to touch

  • src/cli/index.ts:411-416 — branch the catch on error code / message shape
    • OR (cleaner) extract a describeMcpInspectFailure(err): string helper into src/cli/commands/mcp-inspect.ts and call it from the catch
  • tests/mcp-inspect.test.ts — add 3-4 tests for the classifier (don't test against real subprocess; build fake Error objects with .code = "ENOENT" etc. and assert the formatted output)

Acceptance criteria

  • spawn ENOENT, ECONNREFUSED, and handshake-timeout errors all surface a " — try: …" tail with concrete next steps
  • parseMcpSpec errors keep their existing message but gain a suffix pointing at valid spec forms
  • Unknown errors fall through to the current mcp inspect failed: <message> — no regression
  • Exit code 1 preserved for all failure cases
  • No new dependencies
  • npm run verify passes

Hints

  • Don't introduce an McpInspectError class. A switch on (err as NodeJS.ErrnoException).code covers ENOENT / ECONNREFUSED cleanly.
  • Handshake-timeout: search src/mcp/client.ts for the timeout error message it throws; classify by message-substring match.
  • Look at Make web_search / web_fetch errors actionable #16 once it lands for the phrasing convention — keep them consistent.
  • Test pattern: build minimal Error objects in tests, never spawn real processes.

Out of scope

  • Adding retry logic (that's the user's call)
  • Changing the JSON output path (--json) — only the human-readable error tail
  • Internationalising the messages

Difficulty

2-3 hours.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions