Skip to content

Commit 4cb518e

Browse files
Peter (via Claude Code agent)claude
andcommitted
docs: evaluate a /server-card.json path-suffix discovery convention
Adds a draft proposal under docs/proposals/ evaluating the suggestion to recommend fetching Server Cards at <mcp_url>/server-card.json via a plain GET. The writeup corrects the motivating premise (discovery already uses a path suffix, /server-card, not content negotiation against /mcp), lays out the honest pros/cons of the .json spelling, and recommends keeping /server-card while adding a non-breaking bare-GET guarantee and a suffix-resolution rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 92999cb commit 4cb518e

1 file changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Proposal evaluation: a `/server-card.json` path-suffix discovery convention
2+
3+
**Status:** Draft for discussion. Not a settled change.
4+
**Relates to:** [SEP-2127](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127), [`docs/discovery.md`](../discovery.md), issue #12 / PR #22 (which introduced the current `/server-card` reservation).
5+
6+
## TL;DR
7+
8+
The suggestion is to recommend fetching a Server Card with a plain `GET` at a fixed
9+
path suffix — `<mcp_url>/server-card.json` — to spare simple scrapers from HTTP
10+
content-type negotiation.
11+
12+
The motivating premise is **largely already true in this spec, and stronger than the
13+
suggestion assumes**: discovery here does **not** hinge on content negotiation against
14+
the `/mcp` endpoint. The spec already reserves a path-suffix convention,
15+
`GET <streamable-http-url>/server-card`, and the "Alternatives considered" section of
16+
[`discovery.md`](../discovery.md) already _explicitly rejects_ serving the card off the
17+
bare `/mcp` endpoint **because** that would "force content negotiation to disambiguate."
18+
So the headline win the suggestion is reaching for — "a plain GET to a predictable URL,
19+
no content negotiation" — is the design the spec already landed on.
20+
21+
That reframes the decision to two narrow, genuine questions:
22+
23+
1. Should the reserved suffix carry a **`.json` extension** (`/server-card.json`) instead
24+
of the current extension-less `/server-card`?
25+
2. Should the spec **soften the `Accept: application/mcp-server-card+json` expectation**
26+
so that a naive scraper issuing a bare `GET` to the reserved path is a first-class,
27+
guaranteed-to-work client?
28+
29+
This document recommends: **(1) keep `/server-card` — do not adopt `.json` as the
30+
reserved suffix**, for the reasons below; and **(2) yes — clarify that the reserved
31+
endpoint MUST answer a plain `GET` regardless of `Accept`.** (2) delivers essentially all
32+
of the scraper-friendliness the suggestion wants, without a churn-y change to an in-flight
33+
convention. The contrary case for (1) is laid out honestly so a maintainer can overrule.
34+
35+
## What the spec actually says today
36+
37+
From [`docs/discovery.md`](../discovery.md):
38+
39+
- **The discovery entrypoint is the MCP Catalog**, a JSON document at the well-known URI
40+
`/.well-known/mcp/catalog.json`. Each catalog entry carries an explicit `url` for that
41+
server's card. **Clients following the catalog never guess a card URL — they follow the
42+
`url` the catalog hands them**, and a card "MAY be hosted at any unreserved URI (on any
43+
domain)."
44+
- **For servers that want a predictable default location**, the spec reserves exactly one:
45+
46+
> MCP Servers MAY host their Server Card at `GET <streamable-http-url>/server-card`,
47+
> which we reserve for this purpose, though any unreserved URI (on any domain) is valid.
48+
49+
A server at `https://host/mcp` therefore yields `https://host/mcp/server-card` — the
50+
doc's stated rationale is that you "get path-namespacing for free."
51+
52+
- **Content negotiation is layered on top as a SHOULD, not a load-bearing requirement.**
53+
Clients SHOULD send `Accept: application/mcp-server-card+json`; the card has the media
54+
type `application/mcp-server-card+json`. But because the path is _already dedicated to
55+
the card_, the `Accept` header is a politeness/robustness hint, not the disambiguator.
56+
- **The bare-endpoint, content-negotiated approach was considered and rejected.** The
57+
"Alternatives considered" list rejects `GET <streamable-http-url>` (no suffix) precisely
58+
because in Streamable HTTP a `GET` on the MCP endpoint already opens the SSE stream, so
59+
serving the card there "overloads that endpoint and forces content negotiation to
60+
disambiguate 'give me the card' from 'open the stream.'" `.well-known` for the
61+
single-server card and a domain-root `/mcp/` metadata namespace were also considered and
62+
rejected.
63+
64+
**Correction to the framing:** the suggestion is pitched as "switch _away from_ content
65+
negotiation _to_ a path suffix." The spec is already on the path-suffix side of that line.
66+
The live decision is not _whether_ to use a path suffix, but _which spelling_ of the
67+
suffix, and _how hard_ to lean on `Accept`.
68+
69+
## The crux the "no drawback" claim skips: appending a suffix to an arbitrary URL is not free
70+
71+
The suggestion frames `/server-card.json` as "just a convention tacked onto the end of an
72+
`/mcp` URL." Tacking a segment onto an _arbitrary_ URL by string concatenation is **not**
73+
unambiguous. These edge cases apply, and note they apply **equally to the current
74+
`/server-card`** — they are a property of "suffix on a server URL," not of the `.json`
75+
spelling:
76+
77+
- **Query strings.** A streamable-HTTP URL may carry query parameters
78+
(`https://host/mcp?tenant=acme`). Naive concatenation produces
79+
`https://host/mcp?tenant=acme/server-card.json`, which puts the "suffix" _inside the
80+
query string_ — wrong. Correct resolution requires manipulating the URL **path**
81+
component (RFC 3986), stripping/handling the query and fragment.
82+
- **Trailing slash.** `https://host/mcp/` vs `https://host/mcp` yields `…/mcp//server-card`
83+
vs `…/mcp/server-card`. The convention needs a defined normalization rule.
84+
- **Templated URLs.** Server Cards already ship templated remote URLs in this repo —
85+
`examples/ServerCard/valid/templated-remote.json` has
86+
`"url": "https://{tenant}.example.com/mcp"`. The suffix must be appended to the
87+
_resolved_ URL, after variable expansion, which a scraper that has only the template
88+
cannot do on its own.
89+
- **Non-root / nested paths.** `https://host/server/mcp`
90+
`https://host/server/mcp/server-card.json`. Fine, but only if "append to the path" is
91+
the defined operation, not "append to the string."
92+
93+
**Implication:** the simplest scrapers — the population the suggestion is trying to help —
94+
are exactly the ones least likely to do correct RFC 3986 path resolution. For those
95+
clients the **catalog `url` is the robust answer**: it is an already-resolved, absolute URL
96+
that needs no suffix arithmetic. The path-suffix convention (either spelling) is best
97+
understood as a _convenience for servers/operators_ and for clients that already hold a
98+
concrete `/mcp` URL — not as the primary scraper interface. If we adopt or keep _any_
99+
suffix convention, the spec should state the resolution rule explicitly (operate on the
100+
path component; strip query and fragment; collapse a single trailing slash). This is a
101+
real gap in the current text regardless of the `.json` question.
102+
103+
## `/server-card.json` vs `/server-card`: the honest ledger
104+
105+
### Arguments for `.json`
106+
107+
- **Static file hosts.** A dumb static server (S3/GCS bucket, `nginx autoindex`, GitHub
108+
Pages) infers `Content-Type: application/json` from a `.json` extension and serves the
109+
bytes with zero configuration. An extension-less `/server-card` on such a host often
110+
defaults to `application/octet-stream` or `text/plain`.
111+
- **Human/tooling legibility.** `.json` signals "this is a JSON file" to a developer
112+
eyeballing a URL, and to generic tooling (browsers, `curl`-to-file, link scanners) that
113+
keys off extensions.
114+
- **CDN / cache key cleanliness.** A fixed `.json` URL with no reliance on `Vary: Accept`
115+
is the friendliest possible CDN object — one URL, one representation, one cache key.
116+
117+
### Arguments against `.json` (why this doc leans "keep `/server-card`")
118+
119+
- **The benefit is weakest exactly where the convention applies.** The reserved suffix
120+
hangs off a server's **streamable-HTTP endpoint** — a _live MCP server_, not a dumb
121+
static host. That server is, by definition, capable of setting
122+
`Content-Type: application/mcp-server-card+json` on the response. The headline `.json`
123+
win ("static host guesses the content type for you") mostly evaporates when the host is a
124+
dynamic application endpoint. A server that genuinely _wants_ static-host hosting can
125+
already put the card at a `.json` URL anywhere and advertise it via the catalog `url`
126+
the spec already permits "any unreserved URI." So `.json` buys little _at the reserved
127+
location_ specifically.
128+
- **`.json` advertises `application/json`, but the card's media type is
129+
`application/mcp-server-card+json`.** A `.json` extension nudges static hosts toward the
130+
_generic_ `application/json`, which is technically a less specific content type than the
131+
registered card media type the spec asks servers to serve. So the extension can pull the
132+
served `Content-Type` in the _wrong_ direction on the very hosts where it has effect.
133+
- **Couples the discovery path to a serialization.** `/server-card` names the _resource_;
134+
`/server-card.json` bakes the _encoding_ into the path. If a future revision ever serves
135+
an alternate representation (e.g. a signed/JWT-wrapped card, or a CBOR variant for
136+
constrained clients), an extension-less resource path negotiates that cleanly while a
137+
`.json` path has pre-committed. This is a small risk, but it cuts against an _in-flight,
138+
experimental_ convention.
139+
- **Churn on a convention that was just set.** `/server-card` landed deliberately in PR #22
140+
(resolving #12) with written rationale. Re-spelling it now, while SEP-2127 is under
141+
review, spends maintainer/reviewer attention and any nascent implementer goodwill for a
142+
marginal, host-dependent gain. Principle 3 of this repo's `AGENTS.md` ("respect the
143+
experimental, spec-tracked nature") and the "stay conservative" guidance both point
144+
toward _not_ re-litigating a fresh decision without a strong forcing function.
145+
- **It does not fix the real ambiguity.** As shown above, query strings / trailing slashes
146+
/ templated URLs are problems for `/server-card.json` just as much as for `/server-card`.
147+
`.json` changes the spelling, not the hard part.
148+
149+
### Net
150+
151+
The `.json` extension is a **legitimate option with a coherent rationale**, but its main
152+
benefit (static-host content-type inference) is structurally weakest at the reserved
153+
location it would govern (a dynamic MCP endpoint), it risks mis-advertising the card's
154+
specific media type as generic `application/json`, and it couples the path to a
155+
serialization on an experimental convention that was only just decided. On balance this
156+
doc recommends **keeping `/server-card`** and instead making the cheaper, higher-leverage
157+
change below.
158+
159+
## The change that actually delivers the suggestion's goal
160+
161+
The suggestion's real target is "**a simple scraper can do a plain `GET` and get the
162+
card, without dealing with content negotiation.**" That is achievable today with a one-line
163+
clarification, no path change and no breaking effect on consumers:
164+
165+
> A server that hosts its Server Card at the reserved `<streamable-http-url>/server-card`
166+
> location **MUST** serve the card in response to a plain `GET`, i.e. it MUST NOT _require_
167+
> an `Accept: application/mcp-server-card+json` request header to return the card at that
168+
> reserved path. Clients SHOULD still send the `Accept` header; servers SHOULD still
169+
> respond with `Content-Type: application/mcp-server-card+json`. (For cards hosted at an
170+
> arbitrary `url` advertised by a catalog, content negotiation at that URL remains at the
171+
> host's discretion.)
172+
173+
This:
174+
175+
- makes the bare-`GET` scraper a **first-class, guaranteed** client at the reserved path —
176+
the exact ergonomics the suggestion wants;
177+
- keeps `Accept`/`Content-Type` as the correct, recommended behavior for well-behaved
178+
clients and caches;
179+
- is **non-breaking** for existing consumers (it only constrains servers _toward_ being
180+
more permissive); and
181+
- does not pre-empt any unresolved SEP-2127 question about media types or alternate
182+
representations.
183+
184+
If, after discussion, maintainers still want the static-host ergonomics of an extension,
185+
the **least-disruptive** form is to keep `/server-card` as the reserved suffix and add a
186+
note that servers/operators **MAY** additionally expose the card at a `.json` URL and point
187+
the catalog `url` at it — rather than re-spelling the single reserved location.
188+
189+
## Recommendation
190+
191+
1. **Do not adopt `/server-card.json` as the reserved suffix.** Keep `/server-card`. The
192+
premise that this escapes content negotiation is already satisfied by the current path
193+
convention; the `.json` benefit is weakest at the dynamic endpoint it would govern, and
194+
re-spelling a just-decided experimental convention is not worth the churn. _(Presented as
195+
an available option above, with the case argued against.)_
196+
2. **Adopt the bare-`GET` clarification** (servers MUST serve the card at the reserved path
197+
without requiring `Accept`). This is the cheap, non-breaking change that delivers the
198+
scraper-friendliness the suggestion is after.
199+
3. **Specify the suffix-resolution rule** in `discovery.md` (operate on the URL _path_
200+
component per RFC 3986; strip query and fragment; collapse a single trailing slash;
201+
append after template expansion) so "append `/server-card`" is unambiguous. This gap
202+
exists today independent of the `.json` question.
203+
4. **Keep steering scrapers to the catalog `url`** as the robust path: it is an
204+
already-resolved absolute URL and sidesteps all suffix arithmetic.
205+
206+
## Open questions for maintainers
207+
208+
- Is there a concrete deployment scenario where MCP servers genuinely sit behind a
209+
pure static host that cannot set `Content-Type` for the reserved path? If that population
210+
is real and large, the case for `.json` strengthens and item (1) should be reconsidered.
211+
- Should the bare-`GET`-MUST guarantee (item 2) be scoped strictly to the _reserved_
212+
`/server-card` path (as proposed here), or generalized? Generalizing would intrude on the
213+
"any unreserved URI" freedom and is probably out of scope.
214+
- Does SEP-2127 anticipate any non-JSON or wrapped (signed) card representation? If yes,
215+
that is a further argument for an extension-less resource path (item 1) and worth
216+
recording in the SEP.
217+
- Is `application/json` (what a `.json` extension tends to advertise on static hosts)
218+
acceptable as a fallback `Content-Type`, or must the registered
219+
`application/mcp-server-card+json` always be served? The answer affects how damaging the
220+
`.json` media-type-genericization point is.

0 commit comments

Comments
 (0)