
# Handshake

> `initialize`, `ping`, and the negotiated version.

Legacy revisions negotiate once, up front. `h3-mcp` implements the whole exchange from your handler options — there is nothing to wire up, and the same tools, resources, and prompts are served as on the modern era.

## `initialize`

```sh
curl -X POST http://localhost:3000/mcp \
  -H 'content-type: application/json' \
  -d '{
    "jsonrpc": "2.0", "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-06-18",
      "clientInfo": { "name": "my-client", "version": "1.0.0" },
      "capabilities": {}
    }
  }'
```

The response reports `2025-11-25` as the server's legacy version, along with `serverInfo` (name, version, `title`, `description`, `icons`, `websiteUrl`), `capabilities` derived from what you declared, and your `instructions`.

Accepted versions are `2025-11-25`, `2025-06-18`, and `2025-03-26`. Anything else is rejected with `400`. The negotiated version is available to every handler afterwards as `event.context.mcp.protocolVersion` — useful when behavior must differ, as it does for [input-validation errors](/security/validation).

Clients follow up with `notifications/initialized`, which is accepted and requires no handling.

## Protocol Version Header

Once negotiated, clients send `MCP-Protocol-Version` on subsequent requests. When present it is validated before the method is dispatched; an unsupported value is `400`. When absent, the request is still served — older clients did not send it.

## `ping`

`ping` returns an empty result and exists for liveness checks. It was removed in `2026-07-28`; modern clients use ordinary requests, or the [keep-alive comments](/guide/modern/subscriptions) on a listen stream.

## HTTP Surface

| Method   | Behavior                                                         |
| -------- | ---------------------------------------------------------------- |
| `POST`   | JSON-RPC requests and notifications                              |
| `GET`    | [SSE stream](/guide/legacy/sse), `405` when no `onStream` is set |
| `DELETE` | `200`, tearing down the session when sessions are enabled        |
| other    | `405` with `allow: POST, GET, DELETE`                            |

## What Changed in `2026-07-28`

The whole handshake is gone: no `initialize`, no `notifications/initialized`, no `ping`, no negotiated state at all. Version, identity, and capabilities travel in `_meta` on every request, and `server/discover` replaces the descriptive half of the response.

:read-more{to="/guide/modern/discovery" title="Discovery"}
