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.
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 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:
npm ls zod # or: pnpm why zod / yarn why zodAlign 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):
{
"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:
import { webcrypto } from 'node:crypto';
if (typeof globalThis.crypto === 'undefined') {
globalThis.crypto = webcrypto;
}With the global in place the client OAuth 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 shows the in-memory wiring these outputs come from):
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.
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:
legacyKeep { pin } where a legacy connection is unacceptable and a hard failure is the behavior you want. Protocol versions 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:
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 covers both delivery models; Protocol versions 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:
- 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. @modelcontextprotocol/server-legacy is frozen and receives no new features; serve new code over Streamable HTTP, which still reaches 2025-era clients through legacy client support. 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,
stdoutcarries JSON-RPC; log withconsole.error. TS2589means twozodcopies in the dependency tree.- The SDK raises
ERA_NEGOTIATION_FAILEDandMETHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSIONlocally — neither is a wire error. - Server SSE and the Authorization Server helpers live in
@modelcontextprotocol/server-legacy.