Skip to content

Add Server Card / server.json TypeScript types (SEP-2127)#2652

Closed
tadasant wants to merge 7 commits into
modelcontextprotocol:sep/mcp-server-cardsfrom
tadasant:sep/mcp-server-cards-types
Closed

Add Server Card / server.json TypeScript types (SEP-2127)#2652
tadasant wants to merge 7 commits into
modelcontextprotocol:sep/mcp-server-cardsfrom
tadasant:sep/mcp-server-cards-types

Conversation

@tadasant

@tadasant tadasant commented Apr 26, 2026

Copy link
Copy Markdown
Member

Summary

Adds the typed surface for SEP-2127 to schema/draft/schema.ts so the existing schema-generation pipeline emits a JSON Schema for the Server Card and server.json shapes alongside the protocol types.

  • ServerCard — the discovery-oriented document intended for /.well-known/mcp/server-card.
  • Server extends ServerCard — a strict superset adding packages? for the registry's server.json shape.
  • Supporting types: Repository, Remote, Package, PackageTransport (StdioTransport | StreamableHttpPackageTransport | SsePackageTransport), Argument (PositionalArgument | NamedArgument), Input, InputWithVariables, KeyValueInput.
  • Reuses the existing MetaObject and Icon types from schema.ts rather than redefining them.

Why

SEP-2127 introduces the Server Card as a static metadata document for HTTP-based MCP servers and re-frames the registry's server.json as ServerCard + packages. Today the SEP PR only contains the markdown spec — there are no machine-readable types, so consumers can't validate documents against the spec or generate clients/SDKs.

This PR adds the artifact-engineering side: TS types whose generated JSON Schema mirrors the registry's server.schema.json, with the SEP's intentional modifications applied.

Approach

Single-file (Option A). Adds the new types into the existing schema/draft/schema.ts rather than creating a separate server-card.schema.ts / server.schema.ts pair. This keeps the change minimal and follows the precedent of the existing pipeline (scripts/generate-schemas.ts) emitting one schema.json per draft. The SEP's "two distinct schema URLs" intent (server-card.schema.json and server.schema.json) is intentionally deferred — file/URL splitting can land in a follow-up PR if/when SEP-2127 lands. We acknowledge Server Cards aren't JSON-RPC, so co-locating them in the JSON-RPC schema file is conceptually a bit messy; the trade-off is a much smaller change footprint.

Intentional deviations from the registry's server.schema.json

Generated against registry/main/docs/reference/server-json/draft/server.schema.json:

# Deviation Why
1 Server._meta references the protocol's existing MetaObject (Record<string, unknown>) rather than the registry's structured _meta shape with io.modelcontextprotocol.registry/publisher-provided. The SEP explicitly points _meta semantics at the protocol's existing _meta definition. The registry's structured shape can still be expressed via MetaObject since it's an open record.
2 Remote.supportedProtocolVersions and Package.supportedProtocolVersions (string[], optional). Per SEP additions §8.1 and §packages.1.
3 Icon reuses the existing schema.ts type — mimeType is an open string and src has no maxLength: 255, where the registry's Icon constrains both. Reuses an existing protocol type rather than redefining. The existing Icon is already approved with these looser constraints; tightening it would be an out-of-scope protocol change.
4 Discriminator type fields emitted as const (e.g., "const": "stdio") rather than enum (e.g., "enum": ["stdio"]). Cosmetic artifact of typescript-json-schema lowering TS string literals. Validation is identical.
5 Registry uses allOf chains for shared types (InputInputWithVariablesKeyValueInput) and an allOf [{anyOf [StreamableHttpTransport, SseTransport]}, {variables}] chain for RemoteTransport; ours emits flat objects with all fields inlined, with Remote carrying the type discriminator directly. Cosmetic artifact of TS interface inheritance lowering. Validation is identical — registry's StreamableHttpTransport and SseTransport already share the same headers: KeyValueInput[] and templated url pattern that Remote uses.
6 Transport-type renames: registry's LocalTransportPackageTransport; StreamableHttpTransportStreamableHttpPackageTransport; SseTransportSsePackageTransport. Disambiguates from the inlined "streamable-http" | "sse" discriminators on Remote (Server-Card-only) so consumers can tell the package-side transport types apart from the remote-side ones. Downstream code generators that mirror registry naming will need a rename map.
7 Package.version does not enforce the registry's "no 'latest'" rule. The "no latest" constraint is a registry-level publishing policy (the registry catalogs fixed releases, so unpinned versions are rejected at publish time), not a property of the server.json wire format. Server Card / server.json consumers outside the registry context shouldn't have it forced on them at the schema level.
8 PositionalArgument does not enforce "at least one of valueHint or value is set" at the schema level. Registry expresses this as anyOf: [required: [valueHint], required: [value]]; typescript-json-schema cannot generate anyOf-required. Documented as a SHOULD on the JSDoc for valueHint. Concrete documents that include neither field will validate against the schema but are non-conformant per the SEP.
9 $schema is required on ServerCard / Server, with pattern restricting the value to https://static.modelcontextprotocol.io/schemas/v1/<name>.schema.json. Matches the SEP body's "Field Descriptions" §0. Departs from the registry, which treats $schema as optional and emits date-based (/schemas/2025-12-11/...) URLs. See the dedicated section below for rationale on switching to /v1/.

The diff was produced by a script (tmp/diff-schemas.mjs) that walks both schemas and reports per-property differences. After excluding the deviations above, the generated Server $def matches ServerDetail exactly.

$schema field — required, /v1/ URLs

$schema is required on ServerCard (and therefore on Server), matching the SEP body's "Field Descriptions" §0. The value is constrained by pattern to URLs of the form https://static.modelcontextprotocol.io/schemas/v1/<name>.schema.json — versioning the schema by vN segment rather than by date (the registry currently emits /schemas/2025-12-11/server.schema.json-style URLs) means minor additive revisions of the v1 shape won't bump every published document's $schema URL. Only a true breaking change to the wire format would warrant /v2/. This is a behavioral departure from the registry's published server.schema.json (registry treats $schema as optional and currently emits date-based URLs); flagging here so that the registry side can adopt the same convention if/when SEP-2127 lands.

Note for SEP finalization

  • .well-known URL — mcp-server-card vs mcp/server-card. The SEP body uses /.well-known/mcp-server-card consistently (including the IANA registration paragraph saying "URI suffix: mcp-server-card"), but the SEP PR description in SEP-2127: MCP Server Cards - HTTP Server Discovery via .well-known #2127 uses /.well-known/mcp/server-card.json. This PR uses /.well-known/mcp/server-card to match the PR description (and keep room for future siblings under /.well-known/mcp/), but the SEP markdown should be updated for consistency.

Verification

  • npm run check passes locally (schema-ts, schema-json, schema-examples, schema-md, docs-format, docs-js-comments, docs-broken-links, seps).

    > @modelcontextprotocol/specification@0.1.0 check:seps
    > tsx scripts/render-seps.ts --check
    Reading SEP files...
    Found 29 SEP(s)
    All SEP documentation is up to date.
    
  • schema/draft/schema.json regenerated and committed; new $defs: ServerCard, Server, Repository, Remote, Package, PackageTransport, StdioTransport, StreamableHttpPackageTransport, SsePackageTransport, Argument, Input, InputWithVariables, KeyValueInput, PositionalArgument, NamedArgument.

  • docs/specification/draft/schema.mdx regenerated to render the new ## Server Cards section.

  • Remote shape aligned with registry's RemoteTransport (commit 08b83b80): headers reuses KeyValueInput[], variables: { [k]: Input } map is restored, and url carries the registry's templated pattern (^(https?://[^\s]+|\{[a-zA-Z_][a-zA-Z0-9_]*\}[^\s]*)$) so https://{region}.api.example.com/mcp-style URLs validate. The bespoke RemoteHeader interface was removed.

  • SEP example documents validate against the generated schema (Ajv 2020-12 against #/$defs/ServerCard and #/$defs/Server). Both example documents from seps/2127-mcp-server-cards.md validate clean, including a templated remote URL with headers (as KeyValueInput), a variables map, and the new required $schema field set to a /v1/ URL.

  • Negative tests pass (9/9): missing name, missing version, missing description, malformed name (no slash), unknown remote type (e.g., "websocket"), description > 100 chars, missing $schema, date-based $schema URL (e.g., /schemas/2025-12-11/...), and off-host $schema URL are all correctly rejected.

  • JSON-Schema diff against registrytmp/diff-schemas.mjs walks both schemas (with allOf/anyOf flattening) and reports per-property differences. After excluding the deviations above, the generated Server matches the registry's ServerDetail exactly; Remote matches RemoteTransport exactly modulo the supportedProtocolVersions addition (deviation Some way to substitute resources into prompts #2).

  • Fresh-eyes subagent review completed; feedback addressed (PositionalArgument SHOULD JSDoc, naming deviation row, $schema SEP/registry note) in commit 3f0ed648. The @default JSDoc tags suggested in review were rolled back in 9eac472aschema.ts doesn't use @default anywhere else, so introducing them only here was inconsistent.

  • CI green on latest revision (commit ad1fa665): validate, format, handle, label all passing. Latest run.

What this PR does NOT do

  • Does not modify scripts/generate-schemas.ts or any generator.
  • Does not split into separate server-card.schema.json / server.schema.json files (deferred — see above).
  • Does not regenerate the legacy schemas (2024-11-05, 2025-03-26, 2025-06-18) or the released 2025-11-25 schema. New types are scoped to draft since SEP-2127 is Draft status.
  • Does not add example JSON files under schema/draft/examples/. The SEP's inline examples are validated locally during this PR's verification but not committed as repo fixtures (follow-up).

Related

tadasant and others added 7 commits April 26, 2026 20:10
Adds the typed surface for SEP-2127 (MCP Server Cards) to
schema/draft/schema.ts so the existing schema-generation pipeline
emits a JSON Schema for the Server Card and server.json shapes
alongside the protocol types.

ServerCard describes the discovery-oriented document published at
/.well-known/mcp-server-card. Server extends ServerCard with
packages? for the registry's server.json shape.

Field set mirrors the registry's
docs/reference/server-json/draft/server.schema.json with the SEP's
intentional modifications (structured headers on Remote in place of
variables, plus supportedProtocolVersions on Remote and Package).

Reuses existing MetaObject and Icon from schema.ts rather than
redefining them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…default tags

Per fresh-eyes review of modelcontextprotocol#2652:

- Add `^(?!latest$).+$` pattern on `Package.version` to match the registry's
  intent of rejecting the literal string "latest" (registry expresses this
  as `not: { const: "latest" }`; we use a negative-lookahead pattern since
  typescript-json-schema can't generate `not`).
- Add `@default` JSDoc tags on `Input.format` (`"string"`), `Input.isRequired`
  (`false`), `Input.isSecret` (`false`), `PositionalArgument.isRepeated`
  (`false`), and `NamedArgument.isRepeated` (`false`) to match registry
  default values.
- Add JSDoc note on `PositionalArgument.valueHint` describing the "set at
  least one of valueHint or value" requirement (registry expresses this
  as `anyOf: [required: [valueHint], required: [value]]`; typescript-json-schema
  cannot generate this, so we document it as a SHOULD in JSDoc).
Remove the bespoke `RemoteHeader` interface and reuse `KeyValueInput[]` for
`Remote.headers`, matching what the registry's `StreamableHttpTransport` and
`SseTransport` already use. Restore `Remote.variables: { [k]: Input }` and a
templated `url` pattern (`^(https?://[^\s]+|\{[a-zA-Z_]...$`) that allows
`{var}` placeholders, matching registry's URL template support.

This narrows the Server Card / registry deviation list considerably:
`Remote.headers`, `Remote.variables`, and `Remote.url` are now structurally
aligned with the registry; the only intentional Remote-side deviation that
remains is the `supportedProtocolVersions` addition (already called out as
its own deviation).

The shape difference vs registry — flat `Remote` with a `type` discriminator
field instead of `allOf [{anyOf [StreamableHttp, Sse]}, {variables}]` — is
preserved for ergonomic TypeScript usage; both produce equivalent validation.
Remove the `@pattern ^(?!latest$).+$` constraint and corresponding description
notes from `Package.version`. The "no `latest`" rule is a registry-level
publishing concern (the registry rejects unpinned versions because it serves
as a catalog of fixed releases), not a property of the wire format / shape
itself. Server Card / `server.json` consumers outside the registry context
shouldn't have this constraint forced on them at the schema level.

Validation tests updated to drop the corresponding negative case.
Was missed in the previous commit (Package.version no-latest removal).
Existing schema.ts has zero `@default` JSDoc tags anywhere in the file —
introducing them only on the Server Card section is inconsistent with the
file's conventions. The defaults that the registry asserts (`isRequired:
false`, `isSecret: false`, `isRepeated: false`, `format: "string"`) are
already implicit from the optional `?` and the standard "absent boolean is
falsy" reading of optional fields, and they appear in the registry only
because the registry's schema is auto-generated from an OpenAPI spec where
default values are conventional. We don't have that constraint here.
Make `ServerCard.$schema` required (matches the SEP body's "Field
Descriptions §0" entry which already lists it as required) and constrain
the value to a URL of the form
`https://static.modelcontextprotocol.io/schemas/v1/<name>.schema.json`.

The `/v1/` segment replaces the registry's current date-based path (e.g.
`/schemas/2025-12-11/`). Versioning by `vN` rather than by date means that
minor, additive revisions of the v1 shape don't bump every published
document's `$schema` URL — only a true breaking change to the wire format
would warrant `/v2/`.

The pattern intentionally accepts any filename under `/schemas/v1/` (rather
than enumerating `server-card.schema.json` and `server.schema.json`) so that
sibling files can be added later without a schema-source change here.
@tadasant

Copy link
Copy Markdown
Member Author

Closing this in favor of relaunching as an experimental extension while SEP-2127 is still under review:

➡️ modelcontextprotocol/experimental-ext-server-card#1modelcontextprotocol/experimental-ext-server-card#1

The TypeScript source-of-truth and generated JSON Schema move into the experimental-ext repo verbatim (with MetaObject and Icon inlined so the file is self-contained). When the SEP is accepted, the schema.ts lifts directly back into schema/draft/schema.ts here — only the inlined deps need to be dropped during migration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant