Troubleshooting

Purpose and Scope

Use this page when an MCP TypeScript SDK project fails at install time, TypeScript compile time, transport startup, OAuth initialization, protocol negotiation, validation, or server request handling. The first-party troubleshooting page is organized around verbatim error messages: match the exact message you see, then apply the corresponding fix instead of debugging the whole stack at once. That structure matters because failures in MCP often surface away from their cause: one stray stdout write looks like invalid JSON, duplicate schema libraries look like a TypeScript recursion bug, and a protocol version mismatch appears only when connect() negotiates capabilities.

Sources: docs/troubleshooting.md

Troubleshooting in this repository also separates client-visible tool failures from protocol failures. A tool error is a successful JSON-RPC result with isError: true; the model receives it and can recover from the text. A protocol error is a JSON-RPC error response; it indicates that the request itself is invalid or cannot be served, and the model never receives it as ordinary tool content. When you debug server behavior, first decide which category you are seeing, because the fix changes where the message belongs and what the client can do with it.

Sources: docs/servers/errors.md

Relevant Source Files

  • docs/troubleshooting.md - The main reference page for verbatim error messages, including stdio JSON parsing, duplicate Zod types, missing Web Crypto, and protocol era negotiation failures.
  • docs/servers/errors.md - Defines the difference between model-readable tool errors and protocol errors, with examples for isError, thrown handler exceptions, ProtocolError, and ResourceNotFoundError.
  • docs/.vitepress/theme/index.ts - Shows the VitePress v2 documentation site extending the default theme and installing the shared banner at layout-top.
  • docs/v1/.vitepress/theme/index.ts - Shows the v1 documentation site using the same custom CSS and banner pattern, which helps readers distinguish v1 and v2 documentation surfaces.
  • docs/_meta/CONVENTIONS.md - Records the docs style contract: reference pages use exact messages, code-first guidance, observable results, and concise recaps.
  • docs/.vitepress/llms.ts - Generates markdown renditions, llms.txt, and llms-full.txt so agents can retrieve troubleshooting guidance in sidebar order without the generated API reference swamping the prose.

Transport and Runtime Failures

The most common stdio failure is SyntaxError: Unexpected token ... is not valid JSON. On stdio, stdout is not a logging stream; it is the MCP wire. Hosts parse every line written to standard output as JSON-RPC, so console.log from your server or a dependency inserts a non-protocol line into the stream. Keep diagnostic output on stderr with console.error, and let serveStdio own stdout. The token shown in the error is usually the first character of the stray line, which makes it useful for finding the offending log call.

Sources: docs/troubleshooting.md

import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
 
serveStdio(() => {
    const server = new McpServer({ name: 'app', version: '1.0.0' });
    console.error('app server running on stdio');
    return server;
});

Treat ReferenceError: crypto is not defined as a runtime-version problem before you inspect OAuth code. The SDK OAuth helpers sign and verify through the Web Crypto API on globalThis.crypto, and the troubleshooting docs state that the @modelcontextprotocol/* packages require Node.js 20 where that global exists. If you run on an older runtime, upgrade Node. If an upgrade is blocked, install the node:crypto webcrypto polyfill before anything imports or touches SDK OAuth helpers, then keep the rest of the client OAuth flow unchanged.

Sources: docs/troubleshooting.md

import { webcrypto } from 'node:crypto';
 
if (typeof globalThis.crypto === 'undefined') {
    globalThis.crypto = webcrypto;
}

Installation and TypeScript Failures

TS2589: Type instantiation is excessively deep and possibly infinite usually means your dependency tree contains two copies of zod. The SDK derives tool, prompt, and resource types from Zod v4 schemas, and cross-version schema types can make TypeScript instantiate recursive types until it hits its limit. Start with the package-manager inspection command for your workspace. When you find multiple copies, force the tree onto one Zod 4 version with overrides for npm or pnpm, or resolutions for Yarn.

Sources: docs/troubleshooting.md

npm ls zod        # or: pnpm why zod / yarn why zod
{
    "overrides": {
        "zod": "^4.2.0"
    }
}

The verification step is concrete: rerun the dependency inspection command and confirm it reports a single Zod version. Do not chase unrelated call sites until you have eliminated duplicate schema packages, because the compiler error often appears at the place where a tool, prompt, or resource schema is consumed rather than where the duplicate dependency entered the graph. Once the tree has one Zod 4 version, the SDK can infer handler argument types and derive schema metadata without crossing incompatible type definitions.

Sources: docs/troubleshooting.md

Protocol Negotiation Failures

SdkError: ERA_NEGOTIATION_FAILED means connect() found no protocol era both sides can speak. The troubleshooting docs call out two shapes. First, versionNegotiation: { mode: { pin: ... } } names a revision the server does not offer through server/discover; pinning does not fall back. Second, mode: 'auto' with a supportedProtocolVersions list containing no pre-2026 entry removes the legacy fallback. In both cases, inspect your negotiation options before changing transports, because the connection can be healthy while the selected revision set is impossible.

Sources: docs/troubleshooting.md

const pinned = new Client(
    { name: 'app', version: '1.0.0' },
    { versionNegotiation: { mode: { pin: '2026-07-28' } } }
);
 
try {
    await pinned.connect(transport);
} catch (error) {
    if (!(error instanceof SdkError)) throw error;
    console.log(`${error.code}: ${error.message}`);
}

The practical fix is to choose a negotiation mode that matches the server population you actually contact. If you pin 2026-07-28, every target server must advertise that revision. If you need to connect to servers still on 2025 revisions, use mode: 'auto' and keep a supported list that permits the fallback handshake. When the failure message names a pinned revision the server never offered, the transport has already reached the server; the next debugging move is compatibility, not HTTP routing or stdio process spawning.

Sources: docs/troubleshooting.md

Tool Errors, Protocol Errors, and Validation

Server handlers should return recoverable business failures as tool errors when the model can use the message to try again. A missing note, invalid search term, or unavailable domain object can be returned as ordinary tools/call content with isError: true. Put the recovery hint in content[].text, because that is what the model reads. The server errors guide also notes that throwing inside a tool handler becomes the same isError: true shape, while returning explicitly gives you more control over the content the model sees.

Sources: docs/servers/errors.md

return {
    content: [{ type: 'text', text: 'No note with id drafts. Known ids: welcome' }],
    isError: true
};

Use ProtocolError for requests that are structurally wrong or cannot be represented as model-readable tool content. Resource, prompt, and completion callbacks do not have an isError channel, so invalid parameters should become JSON-RPC errors such as ProtocolErrorCode.InvalidParams, and missing resources can use ResourceNotFoundError. This distinction is also a validation rule of thumb: tool output errors can skip normal successful-output validation, but protocol errors communicate that the request failed before a successful result existed.

Sources: docs/servers/errors.md

Documentation and Agent Signals

The docs infrastructure reinforces how to use troubleshooting content. The conventions file requires reference-style pages to name exact identifiers, show real observable output, and keep the fix close to the triggering condition. The VitePress theme files install the shared banner for the v2 and v1 sites, helping readers avoid mixing beta v2 guidance with v1 production guidance. The LLM generation script emits stripped markdown pages, llms.txt, and llms-full.txt, preserving sidebar order and excluding generated API reference from the concatenated prose so agents can retrieve focused troubleshooting instructions.

Sources: docs/_meta/CONVENTIONS.md, docs/.vitepress/theme/index.ts, docs/v1/.vitepress/theme/index.ts, docs/.vitepress/llms.ts

Quick Reference

SymptomLikely causeFirst fix
SyntaxError: Unexpected token ... is not valid JSONNon-JSON text was written to stdio stdoutMove logging to console.error and keep stdout for JSON-RPC
TS2589: Type instantiation is excessively deep and possibly infiniteMultiple Zod copies or versionsRun npm ls zod or equivalent and force one Zod 4 version
ReferenceError: crypto is not definedRuntime lacks globalThis.cryptoRun Node.js 20 or install node:crypto webcrypto before SDK OAuth code
SdkError: ERA_NEGOTIATION_FAILEDNo shared protocol era after negotiationStop pinning unsupported revisions or restore the 2025 fallback in auto mode
Tool call returns isError: trueHandler reported a model-readable failurePut a useful recovery hint in text content
JSON-RPC error from resource or promptRequest itself is invalid or missingThrow ProtocolError or ResourceNotFoundError from the callback

Next Steps

When a failure appears, copy the exact error string first, then match it to the reference entry before changing code. For transport issues, inspect what crosses the wire and keep stdio output clean. For TypeScript issues, inspect dependency versions before editing schemas. For OAuth issues, verify the runtime global. For negotiation failures, compare client configuration with the server revisions advertised during discovery. For server behavior, decide whether the model should see and recover from the failure; if yes, return a tool error, and if not, throw a protocol error.