Two competing SEPs define MCP server discovery and neither is merged into core spec. Here is the runnable code to serve both .well-known endpoints today, plus exactly why DNS and URI drafts are not yet usable.
What is MCP server discovery, and which files do I ship today?
MCP server discovery is how an AI client figures out that a URL hosts a Model Context Protocol server — and what transports, auth, capabilities, and protocol version it speaks — before it ever opens a connection. As of mid-2026 the answer to “what do I ship today” is concrete: serve two static-ish JSON files over HTTPS, /.well-known/mcp/server-card.json (per SEP-1649) and /.well-known/mcp (per SEP-1960), and register your server in the official MCP Registry.
That recommendation comes with an important caveat that most builders miss: neither discovery mechanism is merged into the core MCP specification yet. SEP-1649 and SEP-1960 are both active Specification Enhancement Proposals, and the lead pull request advancing server cards, PR #2127 by Anthropic’s dsp-ant, is still open and under discussion. The 2026-07-28 specification release candidate touches .well-known only for OAuth authorization discovery (SEP-2351), not for capability discovery. So you are implementing against living drafts.
Why ship anyway? Because clients are converging on reading these files, and the cost of serving both is an afternoon of work that breaks nothing. This guide gives you runnable FastAPI and Express handlers for both endpoints, the exact JSON payload shapes, a curl-based validation script, and a plain verdict on the DNS and URI drafts you can safely ignore for now. If you’ve already read our explainers on what MCP is and what an AI agent registry is, this is the missing discovery-layer piece that sits between them.

How do MCP clients discover servers in 2026?
MCP clients discover servers through three layers that answer different questions: a centralized index (the official MCP Registry) for “which servers exist,” per-server .well-known endpoints for “what does this specific URL offer,” and manual configuration for everything not yet wired up. The .well-known layer is the one a server author controls directly, and it is the focus of this tutorial.
The discovery flow is a probe-before-connect handshake. A client takes a base URL — say https://mcp.example.com — and issues a GET to the well-known path. If it gets back a valid server card, it learns the transport endpoint, the supported protocol version, whether authentication is required and which schemes, and a summary of tools, resources, and prompts. Only then does it decide how (or whether) to open the MCP session. This is the value proposition the SEP-1649 summary states outright: detect capabilities, transports, auth requirements, and protocol versions before connecting.
Client support is uneven and worth being honest about. Claude Desktop is the most aggressive: when you hand it a server URL, it probes the well-known endpoint to determine transport and capabilities before connecting. ChatGPT and Cursor still lean on manual configuration — you paste a URL or edit a config file — though they consume registry and discovery metadata to populate server pickers. The trajectory is clear, which is exactly why serving the files now is a cheap hedge.
Crucially, the registry and the well-known endpoints are not competitors. The official MCP Registry (registry.modelcontextprotocol.io), backed by Anthropic, GitHub, PulseMCP, and Microsoft, is a metadata index that lists servers by reverse-DNS name tied to a verified GitHub account or domain. Your .well-known files are the authoritative, self-hosted truth for one server. Registry says “this server exists at this URL”; the well-known card says “here is exactly what I am, right now.”
| Layer | What it answers | Who hosts it | Status |
|---|---|---|---|
| Official MCP Registry | Which servers exist? (index) | registry.modelcontextprotocol.io (Anthropic/GitHub/Microsoft) | Preview, live |
| .well-known/mcp/server-card.json (SEP-1649) | What is this server? (rich card) | You, on your own domain | Draft SEP, not in core spec |
| /.well-known/mcp (SEP-1960) | How do I connect/authenticate? (manifest) | You, on your own domain | Open SEP, not in core spec |
| Manual config | Everything not auto-discovered | The user / client config file | Always works, no standard needed |
SEP-1649 vs SEP-1960: which .well-known endpoint do I need?
You need both, because they target different consumers. SEP-1649’s /.well-known/mcp/server-card.json is the rich “server card” — human-readable description, homepage, and full tool/resource/prompt listings aimed at catalogs and humans browsing. SEP-1960’s /.well-known/mcp is the machine-readable manifest — terse endpoint enumeration, transport options, and detailed auth/security policy aimed at clients deciding how to connect.
SEP-1649 was opened on 14 October 2025 by Anthropic’s dsp-ant and nickcoai, and is the proposal PR #2127 advances. Its card carries serverInfo (name, title, version), protocolVersion, a human description, transport, a structured capabilities block, authentication with required schemes, and either static or dynamic listings of tools, resources, and prompts. It is explicitly designed to also be fetchable as an MCP resource at mcp://server-card.json, so the same document works in-band and out-of-band.
SEP-1960 was filed on 11 December 2025 by the contributor hyperpolymath. Its manifest leans operational: an endpoints object enumerating streamable_http, sse, and websocket URLs; capabilities as simple booleans; an authentication block listing methods (oauth2, api_key, mtls) and OAuth2 config; plus richer security (TLS, DANE, certificate transparency, security contact), rate_limits, dynamic registration, and a jwks_uri. It also specifies response headers: Content-Type: application/json, X-Content-Type-Options: nosniff, and Cache-Control: max-age=3600.
The overlap (both describe capabilities and auth) is real but harmless. Where they disagree, follow each spec’s own field names rather than trying to unify them — clients that read one expect that one’s shape. The table below is the reconciliation that the existing guides gloss over.
Implement both well-known endpoints. They are cheap, additive, and clients are converging on reading them. Skip the IETF DNS and mcp:// URI drafts for now — they have no formal standing and may be abandoned. Register in the official MCP Registry as your discovery index. Re-validate the files after every deploy.
| Dimension | SEP-1649 (server card) | SEP-1960 (manifest) |
|---|---|---|
| Path | /.well-known/mcp/server-card.json | /.well-known/mcp |
| Filed | 14 Oct 2025 (dsp-ant, nickcoai) | 11 Dec 2025 (hyperpolymath) |
| Primary aim | Rich metadata + tool/resource listings | Endpoint enumeration + auth/security policy |
| Capabilities shape | Structured object (with dynamic flags) | Boolean flags (tools/resources/prompts) |
| Transports | transport object | endpoints: streamable_http / sse / websocket |
| Auth | authentication.schemes (bearer, oauth2) | authentication.methods (oauth2, api_key, mtls) + oauth2 config |
| Security extras | Minimal | TLS, DANE, cert transparency, security contact, rate_limits |
| Also an MCP resource? | Yes (mcp://server-card.json) | No |
| Advancing PR | PR #2127 (open, not merged) | Issue #1960 (open, not merged) |
Step 1: Serve server-card.json (SEP-1649) in FastAPI
Add one route that returns the SEP-1649 server card as JSON at /.well-known/mcp/server-card.json. The card should describe your server’s identity, supported protocol version, transport, capabilities, auth requirements, and a tool listing. Keep it static unless your tool set is genuinely dynamic.
Here is a complete, runnable FastAPI handler. The payload uses the exact field names from SEP-1649 (serverInfo, protocolVersion, transport, capabilities, authentication, tools). Adjust the URLs, names, and tool list to your server; the structure is what clients key off.
# app.py — SEP-1649 server card + SEP-1960 manifest in FastAPI
# pip install "fastapi>=0.115" "uvicorn[standard]"
# run: uvicorn app:app --host 0.0.0.0 --port 8000
from fastapi import FastAPI, Response
import json
app = FastAPI()
PUBLIC_BASE = "https://mcp.example.com" # your public origin
PROTOCOL_VERSION = "2025-06-18" # the MCP protocol version you implement
# ---- SEP-1649: rich server card ----
SERVER_CARD = {
"$schema": "https://modelcontextprotocol.io/schemas/server-card.json",
"version": "1.0",
"protocolVersion": PROTOCOL_VERSION,
"serverInfo": {
"name": "acme-weather",
"title": "Acme Weather MCP Server",
"version": "1.2.0",
},
"description": "Real-time weather, forecasts, and severe-weather alerts as MCP tools.",
"homepage": "https://acme.example.com/weather",
"transport": {
"type": "streamable-http",
"endpoint": f"{PUBLIC_BASE}/mcp",
},
"capabilities": {
"tools": {"listChanged": True},
"resources": {"subscribe": False, "listChanged": False},
"prompts": {"listChanged": False},
},
"authentication": {
"required": True,
"schemes": ["oauth2"],
},
"tools": [
{"name": "get_current_weather",
"description": "Current conditions for a city or lat/long."},
{"name": "get_forecast",
"description": "N-day forecast for a location."},
],
}
@app.get("/.well-known/mcp/server-card.json")
def server_card():
return Response(
content=json.dumps(SERVER_CARD, indent=2),
media_type="application/json",
headers={
"X-Content-Type-Options": "nosniff",
"Cache-Control": "max-age=3600",
# discovery is cross-origin: let any client probe it
"Access-Control-Allow-Origin": "*",
},
)
Step 2: Serve the /.well-known/mcp manifest (SEP-1960)
Add a second route at /.well-known/mcp returning the SEP-1960 manifest. This is the terse, machine-first document: it enumerates transport endpoints, lists capabilities as booleans, and declares your auth methods and security posture. Send the SEP-1960-specified headers — X-Content-Type-Options: nosniff and Cache-Control: max-age=3600 — on this one in particular.
Append this to the same app.py from Step 1. The field names follow SEP-1960 (mcp_version, endpoints, capabilities, authentication, security, rate_limits, jwks_uri), which deliberately differ from the SEP-1649 card. Do not try to merge the two shapes — serve each as its spec defines it.
SEP-1960’s path is /.well-known/mcp with NO trailing .json and NO file extension. SEP-1649’s path is /.well-known/mcp/server-card.json WITH the extension. Some early write-ups said /.well-known/mcp.json or /.well-known/mcp/manifest.json — those are stale. Match the spec exactly, because a client probing the canonical path will simply 404 and move on silently.
# append to app.py — SEP-1960 manifest
MANIFEST = {
"mcp_version": "1.0",
"server_version": "1.2.0",
"endpoints": {
"streamable_http": f"{PUBLIC_BASE}/mcp",
# include only the transports you actually serve:
# "sse": f"{PUBLIC_BASE}/sse",
# "websocket": "wss://mcp.example.com/ws",
},
"capabilities": {
"tools": True,
"resources": True,
"prompts": False,
"sampling": False,
"roots": False,
},
"authentication": {
"required": True,
"methods": ["oauth2"],
"oauth2": {
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["weather.read"],
},
},
"security": {
"tls_required": True,
"security_contact": "mailto:security@acme.example.com",
},
"rate_limits": {
"requests_per_minute": 600,
},
"registration": {"dynamic": False},
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"documentation": "https://acme.example.com/weather/docs",
}
@app.get("/.well-known/mcp")
def manifest():
return Response(
content=json.dumps(MANIFEST, indent=2),
media_type="application/json",
headers={
"X-Content-Type-Options": "nosniff",
"Cache-Control": "max-age=3600",
"Access-Control-Allow-Origin": "*",
},
)
Step 3: The same two endpoints in Express (Node)
If your server is Node-based, the logic is identical: two GET routes returning the same JSON shapes with the discovery headers and permissive CORS. Here is a drop-in Express implementation covering both SEP-1649 and SEP-1960.
This is intentionally framework-thin so you can paste it into an existing Express app, a Next.js custom server, or a serverless handler. The only behavioral requirement is that both paths return application/json with the nosniff and cache headers, and that they are reachable cross-origin so any client can probe them.
// discovery.js — SEP-1649 + SEP-1960 for Express
// npm i express
const express = require("express");
const app = express();
const PUBLIC_BASE = "https://mcp.example.com";
const PROTOCOL_VERSION = "2025-06-18";
const SERVER_CARD = {
$schema: "https://modelcontextprotocol.io/schemas/server-card.json",
version: "1.0",
protocolVersion: PROTOCOL_VERSION,
serverInfo: { name: "acme-weather", title: "Acme Weather MCP Server", version: "1.2.0" },
description: "Real-time weather, forecasts, and severe-weather alerts as MCP tools.",
homepage: "https://acme.example.com/weather",
transport: { type: "streamable-http", endpoint: `${PUBLIC_BASE}/mcp` },
capabilities: { tools: { listChanged: true }, resources: { subscribe: false } },
authentication: { required: true, schemes: ["oauth2"] },
tools: [
{ name: "get_current_weather", description: "Current conditions for a city or lat/long." },
{ name: "get_forecast", description: "N-day forecast for a location." },
],
};
const MANIFEST = {
mcp_version: "1.0",
server_version: "1.2.0",
endpoints: { streamable_http: `${PUBLIC_BASE}/mcp` },
capabilities: { tools: true, resources: true, prompts: false },
authentication: {
required: true,
methods: ["oauth2"],
oauth2: { authorization_servers: ["https://auth.example.com"], scopes_supported: ["weather.read"] },
},
security: { tls_required: true, security_contact: "mailto:security@acme.example.com" },
rate_limits: { requests_per_minute: 600 },
jwks_uri: "https://auth.example.com/.well-known/jwks.json",
};
function sendDiscovery(res, body) {
res.set({
"Content-Type": "application/json",
"X-Content-Type-Options": "nosniff",
"Cache-Control": "max-age=3600",
"Access-Control-Allow-Origin": "*",
});
res.send(JSON.stringify(body, null, 2));
}
app.get("/.well-known/mcp/server-card.json", (_req, res) => sendDiscovery(res, SERVER_CARD));
app.get("/.well-known/mcp", (_req, res) => sendDiscovery(res, MANIFEST));
app.listen(8000, () => console.log("discovery up on :8000"));
Step 4: Validate both endpoints with curl and jq
Validate three things before you call discovery done: each endpoint returns HTTP 200, the Content-Type is application/json with the nosniff header, and the JSON parses and contains the spec’s required keys. A 30-line shell script catches every common failure and belongs in CI so a deploy never silently drops you from auto-discovery.
Run the script below against your live origin. It checks status, headers, parseability, and presence of the load-bearing fields (serverInfo for the card, endpoints for the manifest). If anything fails it exits non-zero — wire that into your pipeline.
#!/usr/bin/env bash
# validate_discovery.sh — exits non-zero on any failure
set -euo pipefail
BASE="${1:-https://mcp.example.com}"
check () {
local url="$1" required_key="$2"
echo "==> $url"
# status + content-type
local hdr; hdr=$(curl -sS -D - -o /tmp/body.json "$url")
echo "$hdr" | grep -qi "^HTTP/.* 200" || { echo "FAIL: not 200"; exit 1; }
echo "$hdr" | grep -qi "content-type: application/json" || { echo "FAIL: bad content-type"; exit 1; }
echo "$hdr" | grep -qi "x-content-type-options: nosniff" || echo "WARN: missing nosniff"
# valid JSON + required key present
jq -e . /tmp/body.json > /dev/null || { echo "FAIL: invalid JSON"; exit 1; }
jq -e "has(\"$required_key\")" /tmp/body.json > /dev/null \
|| { echo "FAIL: missing .$required_key"; exit 1; }
echo "OK"
}
check "$BASE/.well-known/mcp/server-card.json" "serverInfo" # SEP-1649
check "$BASE/.well-known/mcp" "endpoints" # SEP-1960
echo "All discovery endpoints valid."




404 on /.well-known/mcp but server-card.json works
Almost always a routing/path issue: SEP-1960’s path has no extension. A static-file host or framework that auto-appends .html or expects a file extension will miss /.well-known/mcp. Add an explicit route (not a static-file glob) for the extensionless path. On Nginx, use a `location = /.well-known/mcp` exact-match block.Card loads in a browser but a client won’t auto-discover it
Check CORS. Discovery is cross-origin — the client’s origin differs from yours — so without Access-Control-Allow-Origin a browser-based client’s fetch is blocked even though curl (which ignores CORS) succeeds. Also confirm you’re serving over HTTPS; clients ignore plaintext discovery.Content-Type is text/plain or application/octet-stream
Static hosts often guess the MIME type from the (missing) extension and pick text/plain. Force Content-Type: application/json on both routes. The SEP-1960 manifest in particular mandates application/json plus X-Content-Type-Options: nosniff, and some clients will reject a mismatched type.Tools listed in the card don’t match the live server
A stale static card is worse than none — clients may surface tools you removed. Either generate the tools array from your actual tool registry at build time, or mark resources/tools/prompts as `dynamic` per SEP-1649 so clients know to enumerate them live via the MCP session rather than trusting the card.Why isn’t MCP server discovery in the core spec yet — and what about DNS/URI?
MCP server discovery isn’t in the core spec because the community hasn’t converged on one shape: SEP-1649 and SEP-1960 overlap, PR #2127 is still resolving design questions (dynamic primitives, relationship to the existing server.json, alignment with broader AI-card standards), and the 2026-07-28 release candidate scoped .well-known to OAuth discovery (SEP-2351) rather than capability discovery. The drafts are usable; they are just not yet blessed.
That is genuinely fine for a builder. The well-known files are additive metadata — serving them cannot break an existing client, and the worst case if a field name shifts is a one-line edit and a re-deploy. The asymmetry strongly favors shipping now: zero downside to serving both, real upside as Claude Desktop and others increasingly probe them.
The two IETF drafts are a different story, and the existing guides are vague about it. draft-morrison-mcp-dns-discovery-01 (DNS TXT records at _mcp., by Blake Morrison, April 2026) and draft-serra-mcp-discovery-uri-01 (the mcp:// URI scheme plus a /.well-known/mcp-server manifest, by Marco Serra, last updated 25 March 2026) are both individual Internet-Drafts. Each carries the standard IETF boilerplate: “not endorsed by the IETF” and “no formal standing in the IETF standards process.” Neither has working-group adoption; the serra draft expires 25 September 2026. They request IANA registrations that have not been granted.
My recommendation: do not implement the DNS or URI drafts in production yet. They could be adopted, revised, or abandoned. If you want to hedge, stub a single DNS TXT record in a non-production branch and move on — it is a one-liner you can publish in minutes if a draft suddenly gains traction. Spend your real effort on the two well-known files and a clean registry listing, because those are what clients read today.
Pros
Cons
“Registry says this server exists; the well-known card says here is exactly what I am, right now. You want both — they answer different questions.”
Surya Koritala, founder of Cyntr and Loomfeed
MCP server discovery checklist: ship it this week
Ship both .well-known endpoints today; ignore the IETF DNS/URI drafts
To make your MCP server auto-discoverable today: serve /.well-known/mcp/server-card.json (SEP-1649) and /.well-known/mcp (SEP-1960) over HTTPS with application/json, nosniff, cache, and permissive CORS headers; validate both in CI with curl and jq; and publish a listing in the official MCP Registry. Skip the IETF DNS and mcp:// URI drafts until they gain formal standing.
Treat the two well-known endpoints as the spec defines them — distinct field shapes, distinct paths, no merging. Keep the SEP-1649 card rich (description, homepage, tools) for catalogs and humans; keep the SEP-1960 manifest terse and operational (endpoints, auth methods, security) for clients deciding how to connect. If your tool set changes at runtime, mark listings as dynamic rather than letting a static card go stale.
The discovery layer is still mid-flight, but the builder’s move is unambiguous: ship both files now, validate them on every deploy, and register your server. You get most of the auto-discovery upside for an afternoon of work, and you are positioned for whichever SEP lands in the core spec after the 2026-07-28 release candidate settles.
Builder’s take
I wire discovery into every MCP server we run on Cyntr, and the honest answer is that the spec is mid-flight. Here is how I’d treat it as a builder shipping this week:
- Ship both endpoints. They cost an afternoon, they break nothing, and clients are converging on reading them. The downside of serving both is zero; the downside of guessing wrong is a re-deploy.
- Treat server-card.json (SEP-1649) as your marketing-and-capabilities card and /.well-known/mcp (SEP-1960) as your machine-readable transport-and-auth manifest. They overlap, but they are aimed at different consumers.
- Do not block on the IETF DNS or mcp:// URI drafts. They have no formal standing and can be abandoned. I keep a one-line TXT record stubbed in a branch and nothing more.
- Register in the official MCP Registry too. .well-known is per-server discovery; the registry is the index. You want both, and they answer different questions.
- Re-validate after every deploy. A stale or 404ing card silently drops you from auto-discovery, and you will not get an error — you will just get no clients. Curl it in CI.
Frequently asked questions
MCP server discovery is how an AI client determines that a URL hosts a Model Context Protocol server and learns its transport, authentication, capabilities, and protocol version before connecting. In 2026 it works through three layers: the official MCP Registry as an index, per-server .well-known endpoints (SEP-1649 and SEP-1960) as the authoritative self-hosted description, and manual configuration for anything not yet wired up.
SEP-1960’s manifest goes at the exact path /.well-known/mcp — no .json extension and no trailing slash. SEP-1649’s server card goes at /.well-known/mcp/server-card.json, with the extension. Both must be served over HTTPS as application/json with X-Content-Type-Options: nosniff and a Cache-Control header. Older write-ups citing /.well-known/mcp.json or /.well-known/mcp/manifest.json are stale.
SEP-1649 defines a rich server card (server-card.json) with human-readable description, homepage, and full tool/resource/prompt listings, aimed at catalogs and humans. SEP-1960 defines a terse machine-readable manifest at /.well-known/mcp focused on enumerating transport endpoints and declaring auth methods and security policy. They overlap on capabilities and auth, but you implement both because clients read different ones.
No. As of mid-2026 both SEP-1649 and SEP-1960 are active proposals, and PR #2127 advancing server cards is still open and unmerged. The 2026-07-28 specification release candidate uses .well-known only for OAuth authorization discovery (SEP-2351), not capability discovery. The drafts are usable and clients are converging on them, but they are not blessed in the core spec yet.
Support is uneven. Claude Desktop probes the well-known endpoint to determine transport and capabilities before connecting, making it the most native consumer of discovery. ChatGPT and Cursor still rely largely on manual configuration — you paste a URL or edit a config file — though they consume registry and discovery metadata to populate server pickers. Serving the well-known files now is a cheap hedge as support broadens.
Not in production yet. draft-morrison-mcp-dns-discovery (DNS TXT at _mcp.
Primary sources
- SEP-1649: MCP Server Cards — HTTP Server Discovery via .well-known — GitHub / modelcontextprotocol
- SEP-1960: .well-known/mcp Discovery Endpoint for Server Metadata — GitHub / modelcontextprotocol
- PR #2127: MCP Server Cards — HTTP Server Discovery via .well-known (dsp-ant) — GitHub / modelcontextprotocol
- draft-morrison-mcp-dns-discovery-01 — Discovery via DNS TXT Records — IETF Datatracker
- draft-serra-mcp-discovery-uri-01 — The ‘mcp’ URI Scheme — IETF Datatracker
- The Official MCP Registry — Model Context Protocol
- The 2026-07-28 MCP Specification Release Candidate — Model Context Protocol Blog
- MCP Server Discovery: Implement .well-known/mcp.json (2026) — Ekamoira
Last updated: June 3, 2026. Related: Agent Infrastructure.