--- shape: reference --- # Troubleshooting Each heading on this page is the verbatim error message. Match yours, then apply that entry's fix. ## `SyntaxError: Unexpected token ... is not valid JSON` On stdio, standard output is the wire: the host parses every line your server writes to `stdout` as JSON-RPC. One `console.log` — yours or a dependency's — puts a stray line on it, and the host reports that line with this error. Log to `stderr` instead; `serveStdio` owns `stdout`, and `console.error` is safe anywhere in the process. ```ts source="../examples/guides/troubleshooting.stdio.examples.ts#serveStdio_stderr" 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'); // stderr — never console.log return server; }); ``` The host shows the `stderr` line in the server's log and keeps parsing `stdout` cleanly. [Serve over stdio](./serving/stdio.md) covers the entry point. ::: tip The quoted token is the first character of the stray line, which usually identifies the call that wrote it. ::: ## `TS2589: Type instantiation is excessively deep and possibly infinite` Two copies of `zod` in the dependency tree. The SDK derives its tool, prompt and resource types from Zod v4 schemas; a second `zod` copy makes TypeScript instantiate cross-version types until it hits its recursion limit and fails at an unrelated-looking call site. List every installed copy: ```sh npm ls zod # or: pnpm why zod / yarn why zod ``` Align everything on one Zod 4 version. When a transitive dependency pins another copy, force one with your package manager's override field (`overrides` for npm and pnpm, `resolutions` for Yarn): ```json { "overrides": { "zod": "^4.2.0" } } ``` `npm ls zod` reporting a single version means the duplicate is gone and the error with it. ## `ReferenceError: crypto is not defined` The OAuth client helpers sign and verify through the Web Crypto API at `globalThis.crypto`. Every `@modelcontextprotocol/*` package requires Node.js 20, where that global is always defined — this error means the process is running on an older runtime (Node.js 18 and earlier). Upgrade Node.js. Where you cannot, assign the polyfill from `node:crypto` before anything touches the SDK: ```ts source="../examples/guides/troubleshooting.examples.ts#webcrypto_polyfill" import { webcrypto } from 'node:crypto'; if (typeof globalThis.crypto === 'undefined') { globalThis.crypto = webcrypto; } ``` With the global in place the [client OAuth](./clients/oauth.md) flows run unchanged. ## `SdkError: ERA_NEGOTIATION_FAILED` `connect()` found no **protocol era** both sides speak. Two shapes produce it: `versionNegotiation: { mode: { pin: ... } }` names a revision the server does not offer over `server/discover`, and pinning never falls back; or `mode: 'auto'` with a `supportedProtocolVersions` list that has no pre-2026 entry, which removes the legacy fallback. The pinned shape — `transport` here reaches a server still on the 2025 revisions ([Test a server](./testing.md) shows the in-memory wiring these outputs come from): ```ts source="../examples/guides/troubleshooting.examples.ts#connect_pinRejected" 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 rejection names the pinned revision the server never offered: ``` ERA_NEGOTIATION_FAILED: Version negotiation failed: the server did not offer pinned protocol version 2026-07-28 via server/discover (no fallback in pin mode) ``` Change the mode to `'auto'`: the probe falls back to the 2025 `initialize` handshake on the same connection. ```ts source="../examples/guides/troubleshooting.examples.ts#connect_autoFallback" const negotiated = new Client({ name: 'app', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } }); await negotiated.connect(transport); console.log(negotiated.getProtocolEra()); ``` `connect()` resolves and the client reports the era it landed on: ``` legacy ``` Keep `{ pin }` where a legacy connection is unacceptable and a hard failure is the behavior you want. [Protocol versions](./protocol-versions.md) defines the eras and what each negotiation mode offers. ## `SdkError: METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION` You sent a spec method the negotiated protocol era does not define. The SDK raises this locally — nothing reached the transport — and the message names the method, the negotiated revision, and the era-appropriate replacement. `subscriptions/listen` exists only on a 2026-07-28 connection; this client negotiated a 2025 era, the default: ```ts source="../examples/guides/troubleshooting.examples.ts#listen_legacyConnection" const client = new Client({ name: 'app', version: '1.0.0' }); await client.connect(transport); try { await client.listen({ resourceSubscriptions: ['file:///logs/app.log'] }); } catch (error) { if (!(error instanceof SdkError)) throw error; console.log(`${error.code}: ${error.message}`); } ``` The message carries the fix: ``` METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION: subscriptions/listen requires a 2026-07-28-era connection (negotiated: 2025-11-25). On a 2025-era connection, change notifications are delivered unsolicited: use ClientOptions.listChanged and resources/subscribe instead. ``` Either negotiate the era that defines the method — `versionNegotiation: { mode: 'auto' }` against a server that serves 2026-07-28, as in the previous entry — or call the surface the negotiated era does define. [Subscriptions](./clients/subscriptions.md) covers both delivery models; [Protocol versions](./protocol-versions.md) lists which methods each era defines. ## `Module '"@modelcontextprotocol/server"' has no exported member 'SSEServerTransport'` `@modelcontextprotocol/server` no longer ships the server-side SSE transport, and the OAuth Authorization Server helpers (`mcpAuthRouter`, `ProxyOAuthServerProvider`) left with it. Both live on as a frozen v1 copy in `@modelcontextprotocol/server-legacy`. Rewrite the imports: ```diff - import { SSEServerTransport } from '@modelcontextprotocol/server'; + import { SSEServerTransport } from '@modelcontextprotocol/server-legacy/sse'; - import { mcpAuthRouter, ProxyOAuthServerProvider } from '@modelcontextprotocol/server'; + import { mcpAuthRouter, ProxyOAuthServerProvider } from '@modelcontextprotocol/server-legacy/auth'; ``` The Resource Server helpers did not move there: `requireBearerAuth`, `mcpAuthMetadataRouter` and `OAuthTokenVerifier` are first-class in `@modelcontextprotocol/express` — see [Authorization](./serving/authorization.md). `@modelcontextprotocol/server-legacy` is frozen and receives no new features; serve new code over [Streamable HTTP](./serving/http.md), which still reaches 2025-era clients through [legacy client support](./serving/legacy-clients.md). A client limited to the HTTP+SSE transport is the one case that still needs the frozen `@modelcontextprotocol/server-legacy/sse` import above. ## Recap - Every heading on this page is the exact message you searched for. - On stdio, `stdout` carries JSON-RPC; log with `console.error`. - `TS2589` means two `zod` copies in the dependency tree. - The SDK raises `ERA_NEGOTIATION_FAILED` and `METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION` locally — neither is a wire error. - Server SSE and the Authorization Server helpers live in `@modelcontextprotocol/server-legacy`.