Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/clients/connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:30
await client.connect(transport);
```

`connect()` runs the `initialize` handshake and resolves once it completes. The client now holds the negotiated protocol version, the server's capabilities, and its instructions.
`connect()` runs the `initialize` handshake by default (legacy era) and resolves once it completes. The client now holds the negotiated protocol version, the server's capabilities, and its instructions. To probe for a 2026-07-28 server first, pass `versionNegotiation: { mode: 'auto' }` — see [Protocol versions](../protocol-versions.md).

::: info Coming from v1?
`Client` and the transport classes keep their names — only the import paths moved, to `@modelcontextprotocol/client` and its `/stdio` subpath. Run the codemod, then see the [upgrade guide](../migration/upgrade-to-v2.md).
Expand Down
6 changes: 5 additions & 1 deletion docs/protocol-versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ legacy

`mode` takes three values; the first is the default.

- Absent, or `mode: 'legacy'` — the 2025 `initialize` handshake, byte for byte. No probe.
- Absent, or `mode: 'legacy'` — the 2025 `initialize` handshake, byte for byte. No probe. This is intentional v2 default so existing 2025 clients stay byte-compatible; it is not a dual-era probe.
- `mode: 'auto'` — probe with `server/discover`; fall back to `initialize` against a 2025-only server.
- `mode: { pin: '2026-07-28' }` — that revision or nothing. A pin never falls back.

::: tip Non-MCP JSON-RPC endpoints
An endpoint that only accepts bare `tools/call` POSTs and never implements `initialize` or `server/discover` is not an MCP server. The default legacy client will fail on `connect()` with an HTTP/`SdkHttpError` (see [Troubleshooting](./troubleshooting.md#sdkhttperror-error-posting-to-endpoint-)). `mode: 'auto'` does not make such endpoints work either — both eras still require an MCP lifecycle. Fix the server or talk to it outside this SDK.
:::

Pin against the same 2025-only server and `connect()` rejects instead of falling back.

```ts source="../examples/guides/protocolVersions.examples.ts#versionNegotiation_pin"
Expand Down
33 changes: 33 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,38 @@ 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.

## `SdkHttpError: Error POSTing to endpoint: ...`

`connect()` POSTed the legacy `initialize` handshake (the default when `versionNegotiation` is absent or `mode: 'legacy'`) and the HTTP response was a non-2xx status. The transport surfaces that as `SdkHttpError` with the response body in the message and in `error.data.text`.

This is expected when:

- The URL is not an MCP server (or does not implement the MCP lifecycle at all). The default client always starts with `initialize`; endpoints that only accept ad-hoc `tools/call` JSON-RPC will reject it.
- A real MCP server rejected `initialize` with an HTTP error (auth misconfiguration, bad path, gateway error).

```ts
import { Client, StreamableHTTPClientTransport, SdkHttpError } from '@modelcontextprotocol/client';

const client = new Client({ name: 'app', version: '1.0.0' });
try {
await client.connect(new StreamableHTTPClientTransport(new URL('https://example.com/not-mcp')));
} catch (error) {
if (error instanceof SdkHttpError) {
console.log(error.status, error.message);
// JSON-RPC error bodies on 4xx/5xx arrive as text on the HTTP error;
// a 200 JSON-RPC error body surfaces as ProtocolError instead.
}
}
```

What to do next:

1. Confirm the URL is a real MCP Streamable HTTP endpoint (not a proprietary JSON-RPC path).
2. If the server speaks the 2026-07-28 era and you need the modern probe, opt in with `versionNegotiation: { mode: 'auto' }` — see [Protocol versions](./protocol-versions.md).
3. For a well-formed JSON-RPC error returned on HTTP 200, catch `ProtocolError` and read `error.code` / `error.data`. On non-2xx, parse `error.data.text` if you need the JSON-RPC fields from an `SdkHttpError`.

This path does **not** retry a failed `initialize` POST three times; if you see multiple attempts, an outer application retry loop is the usual cause.

## `Version negotiation failed: the server requires authorization (HTTP 401)`

`SdkHttpError` with code `CLIENT_HTTP_AUTHENTICATION` (`error.status === 401`): the negotiation probe hit an auth wall and no `authProvider` is configured. Auth status is never protocol-era evidence, so `connect()` fails typed instead of guessing an era. Pass an `authProvider` — `{ token: async () => myApiKey }` for bearer tokens, or an `OAuthClientProvider` for OAuth flows (the connect-time `UnauthorizedError` → `finishAuth()` → reconnect cycle then works in every negotiation mode).
Expand Down Expand Up @@ -173,6 +205,7 @@ HTTP SSE streams emit a `: keepalive` comment every 15 seconds by default so cli
- 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.
- Non-2xx `initialize` responses surface as `SdkHttpError: Error POSTing to endpoint: ...` (legacy default); non-MCP endpoints will not connect.
- Server SSE and the Authorization Server helpers live in `@modelcontextprotocol/server-legacy`.

<!-- maintainers: an entry on this page lives only as long as the surface that
Expand Down
Loading