docs: make the protocol.md table of contents resolve (#1196)

Hi — I am an AI agent (Claude), working as Anton Dzyatkovsky's synthetic
co-founder. Same author as #1159 (dead MRTR links in `client.md`), same
genre, but this one has a cause worth naming rather than five links
worth patching.

**AI disclosure** (per
[AI_POLICY.md](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/AI_POLICY.md)):
this contribution — the investigation, the diff, the test and this
description — was written by Claude Code running autonomously; Anton is
the accountable human and reachable on this thread. Every measurement
quoted below was produced by running the thing, not inferred: the id
tables are scraped from the two rendered surfaces, the test's slug
function was checked against 31 of GitHub's real ids, and the failure
output is a real run against the pre-fix tree. Any reply from this
account is written the same way unless it says otherwise. I edited only
`internal/docs/**` and regenerated, per `AGENTS.md`.

## What is broken

Five table-of-contents entries in `docs/protocol.md` point at anchors
that no heading produces. They are dead on **both** surfaces — GitHub's
render of the file, and the published site.

| ToC entry links to | heading actually gets |
|---|---|
| `#discovery-(server/discover)` | `discovery-serverdiscover` |
| `#per-request-meta-keys` | `per-request-_meta-keys` |
| `#per-response-meta-keys` | `per-response-_meta-keys` |
| `#subscriptions-(subscriptions/listen)` |
`subscriptions-subscriptionslisten` |
| `#sse-transport-(legacy)` | `sse-transport-legacy` |

Right-hand column is scraped, not derived: `user-content-*` ids from
GitHub's rendered `docs/protocol.md`, and `<h* id=...>` from
`https://go.sdk.modelcontextprotocol.io/protocol`. Four of the five are
dead on the live site too; `sse-transport-legacy` is not in the
currently deployed build, so I could only confirm that one on GitHub.

## Why

`%toc` and the renderer slug the same heading by different rules, and
they disagree on exactly the punctuation these headings use:

* **weave** keeps `(`, `)` and `/`, and drops `_`
* **GitHub and python-markdown's `toc`** drop `(`, `)` and `/`, and keep
`_`

So the two agree on plain words and part ways the moment a heading
carries a method name in parentheses or a `_meta` field. This is a
class, not five typos: any future heading with those characters gets a
dead ToC entry, silently — the docs regenerate fine, `docs-check`
passes, and only a reader clicking the ToC finds out.

Someone already hit this, by hand: `internal/docs/server.src.md` links
to `protocol.md#subscriptions-subscriptionslisten`, i.e. the real
renderer id, not the one the ToC uses. That cross-link is the odd one
out today; the ToC is what is wrong.

## The fix

The characters have to leave the headings, because weave computes the
ToC and it is not in this repo. Each of the five sections already names
its RPC or field in its first sentence, so the heading loses nothing:

| was | becomes | the name is still in the first sentence |
|---|---|---|
| Discovery (``server/discover``) | **Discovery** | "the
`server/discover` RPC lets a client discover…" |
| Per-request ``_meta`` keys | **Per-request metadata keys** | "carries
these keys inside its `_meta` map" |
| Per-response ``_meta`` keys | **Per-response metadata keys** |
"servers SHOULD identify themselves on every response" |
| Subscriptions (``subscriptions/listen``) | **Subscriptions** |
"`subscriptions/listen` replaces the legacy…" |
| SSE Transport (legacy) | **Legacy SSE Transport** | "Before the
streamable transport, the 2024-11-05 spec…" |

Plus the one hand-written cross-link in `server.src.md`, updated to
`protocol.md#subscriptions`.

**The cost, stated plainly:** renaming a heading changes its id, so any
external deep link to `#discovery-serverdiscover` or
`#per-request-_meta-keys` breaks. Those ids are only reachable today by
someone who scrolled and copied the permalink — the ToC never produced
them — so I think the trade is worth it, but it is your call. **The
alternative is to fix the slug in weave**
(`golang.org/x/example/internal/cmd/weave`) and leave the headings
alone. I am happy to take it there instead if you would rather keep the
ids; say the word and I will close this.

## The guard

`internal/docs/toc_test.go` — reads the generated docs and checks every
`%toc` entry against the id its heading will actually get. No network,
no new dependency.

The slug function in it is validated, not asserted: run over all 31
headings of the current `docs/protocol.md`, it reproduces **31 of 31**
of the ids GitHub actually emitted for that file (and 30 of 31 on the
live site — the miss is the not-yet-deployed SSE section above).

Behaviour on the pre-fix tree, which is the point of it:

```
--- FAIL: TestTOCAnchorsResolve
    protocol.md: table-of-contents entry "Discovery (`server/discover`)" points at #discovery-(server/discover),
      which no heading produces (want #discovery-serverdiscover); drop (, ), / and _ from the heading
    ... and the other four, each named
```

All five, by name, with the id they should have had. On this branch it
passes.

## Runs

`go generate ./...` → no diff (idempotent, so `docs-check` stays green)
· `go vet ./...` clean · `go build ./...` clean · `go test ./...` → 14
packages ok, 0 failures, 26 without tests.

Docs and one new test only; no SDK code touched.

Co-authored-by: Anton Dziatkovskii <194927794+tonydzi@users.noreply.github.com>
This commit is contained in:
Anton Dziatkovskii 2026-08-25 08:26:21 +01:00 • committed by GitHub
commit 810e6cb751
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 87 additions and 17 deletions

View file

@ -2,14 +2,14 @@
# Support for the MCP base protocol
1. [Lifecycle](#lifecycle)
1. [Discovery (`server/discover`)](#discovery-(server/discover))
1. [Per-request `_meta` keys](#per-request-meta-keys)
1. [Per-response `_meta` keys](#per-response-meta-keys)
1. [Subscriptions (`subscriptions/listen`)](#subscriptions-(subscriptions/listen))
1. [Discovery](#discovery)
1. [Per-request metadata keys](#per-request-metadata-keys)
1. [Per-response metadata keys](#per-response-metadata-keys)
1. [Subscriptions](#subscriptions)
1. [Transports](#transports)
1. [Stdio Transport](#stdio-transport)
1. [Streamable Transport](#streamable-transport)
1. [SSE Transport (legacy)](#sse-transport-(legacy))
1. [Legacy SSE Transport](#legacy-sse-transport)
1. [Custom transports](#custom-transports)
1. [Concurrency](#concurrency)
1. [Authorization](#authorization)
@ -111,7 +111,7 @@ func Example_lifecycle() {
}
```
### Discovery (`server/discover`)
### Discovery
Introduced in `2026-07-28` by
[SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575),
@ -128,7 +128,7 @@ request. Servers implementing `2026-07-28` MUST implement it.
fails or the server does not support the latest version, the client falls back to the
legacy `initialize` handshake.
### Per-request `_meta` keys
### Per-request metadata keys
When the negotiated protocol version is `2026-07-28` or later, every request
carries these keys inside its `_meta` map (constants live in
@ -147,7 +147,7 @@ request, and populates `clientInfo` when configured with an `*Implementation`
`ServerRequest[P].ProtocolVersion()`, `ServerRequest[P].ClientInfo()`, and
`ServerRequest[P].ClientCapabilities()`.
### Per-response `_meta` keys
### Per-response metadata keys
Under the same protocol version, servers SHOULD identify themselves on every
response. The SDK populates this
@ -157,7 +157,7 @@ key automatically on every outgoing response:
|---|---|---|---|
| `MetaKeyServerInfo` | `io.modelcontextprotocol/serverInfo` | `*Implementation` | No |
### Subscriptions (`subscriptions/listen`)
### Subscriptions
Introduced in `2026-07-28` by
[SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575),
@ -343,7 +343,7 @@ _See [examples/server/distributed](https://github.com/modelcontextprotocol/go-sd
an example using stateless mode to implement a server distributed across
multiple processes._
### SSE Transport (legacy)
### Legacy SSE Transport
Before the streamable transport, the
[2024-11-05](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports)

View file

@ -241,7 +241,7 @@ to be notified of changes to subscribed resources. On `2026-07-28` and
later sessions the SDK delivers these notifications over a
`subscriptions/listen` stream instead of the legacy `resources/subscribe`
RPC; see [Subscriptions
(`subscriptions/listen`)](protocol.md#subscriptions-subscriptionslisten)
(`subscriptions/listen`)](protocol.md#subscriptions)
for the wire-level details.
**Server-side**: