Add OpenMCP: an open catalog of MCP relays (#153)

A relay serves /.well-known/openmcp.json; a catalog probes it (the
descriptor from the relay's own origin, then initialize and tools/list)
and lists only what it found; a client reaches every relay through the
catalog's REST, its own MCP endpoint, or signed webhooks. Landing page at
/openmcp, the document at /docs/openmcp, registered in the same four
places as the other specs. Reference implementation at
github.com/logicsrc/openmcp.


Claude-Session: https://claude.ai/code/session_01FMT2v1YxmgcDuionrfT719

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-09-12 09:55:14 -07:00 • committed by GitHub
parent 1a2d143a23
commit 30facd71ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 393 additions and 0 deletions

170
docs/openmcp.md Normal file
View file

@ -0,0 +1,170 @@
# OpenMCP
OpenMCP is an open catalog protocol for MCP relays: MCP servers you can reach over HTTP. It says how a relay describes itself in one file, how a catalog lists relays it has actually reached, how a client finds a tool and calls it through a catalog or direct, and how a catalog tells subscribers what changed. It is maintained by Profullstack, Inc. as part of the LogicSRC open-standards surface, with a reference implementation at [github.com/logicsrc/openmcp](https://github.com/logicsrc/openmcp).
Status: **0.1**. A description of a catalog already running, published so others can run one, list in one, or be listed.
Slug: `openmcp`
## The problem
Every product that speaks MCP is a relay: an endpoint an agent can call. There are thousands, and each one is found by hand, from a README, and wired into a client's configuration by a person. There is no file a relay serves to say what it is, no way for a directory to say "this one answered yesterday", and no single door an agent can open to reach relays it has not been told about.
The pieces exist. MCP defines the handshake and `tools/list`. `/.well-known/` is where a host says things about itself. Webhooks are how a service tells another service something happened. What is missing is the one convention that puts them together for MCP, so a relay can be discovered instead of configured.
## Terms
- A **relay** is an MCP server reachable over HTTP (Streamable HTTP transport). Its **descriptor** is the file it serves about itself.
- A **catalog** is a server that keeps relay records, probes each relay, and serves the records for discovery. A catalog is also a relay.
- A **record** is a relay as a catalog holds it: the descriptor, the tools the relay reported, whether it is online, whether it is verified, and when it was last reached.
- A **client** is anything that reads a catalog: a person's terminal, a program, an agent.
## The descriptor
A relay serves a JSON document at `/.well-known/openmcp.json` on its own origin.
```json
{
"openmcp": "0.1",
"mcp": "https://agenticjobs.work/api/mcp",
"name": "Agentic Jobs",
"description": "A job board where agents apply to agents, with a person at both ends.",
"url": "https://agenticjobs.work",
"auth": { "kind": "bearer", "url": "https://agenticjobs.work/me/tokens", "open": ["search_jobs", "get_job"] },
"tags": ["jobs", "hiring", "agents"],
"operator": "https://profullstack.com/.well-known/openprofile.md",
"webhooks": "https://agenticjobs.work/api/v1/webhooks",
"tools": ["search_jobs", "get_job", "apply_to_job", "post_update"],
"catalogs": ["https://openmcp.logicsrc.com"]
}
```
The rules, and every one degrades:
1. **`mcp` is required and is the only required key.** Absolute, or relative to the descriptor's own URL. A descriptor with `mcp` alone is valid.
2. **`openmcp`** is the version of this document the descriptor follows. Absent means the current one.
3. **`name`, `description`, `url`** are for a listing: one line, a paragraph, the site behind the relay.
4. **`auth.kind`** is `none`, `bearer`, `oauth` or `api-key`. `auth.url` is where a person gets a credential. `auth.open` names the tools that work with no credential. Absent `auth` means unstated, which a reader reports rather than assumes.
5. **`tags`** are free-form and lowercase. A catalog groups by them.
6. **`operator`** is the person or organisation answerable for the relay, as an [OpenProfile.md](/openprofile) URL. An agent that meets a relay with no operator should say so.
7. **`webhooks`** is where a caller subscribes to the relay's own events, if it has any. Its shape is the relay's business; this document only defines the catalog's webhooks.
8. **`tools`** names the tools, so a catalog can index a relay it cannot reach right now. What `tools/list` says wins over this list whenever both exist.
9. **`catalogs`** names catalogs the relay is listed in, so a reader that found the relay can find more.
Unknown keys are kept. Serve it as `application/json`. The descriptor is a claim; the probe is the verification.
## The probe
A catalog builds a record by asking two questions, in order, of any URL it is handed (the descriptor, the MCP endpoint, or just the site):
1. **Does the origin serve a descriptor?** Fetch `/.well-known/openmcp.json` on the origin of the given URL. If it parses, the record is **verified**: the descriptor came from the relay itself, not from whoever registered it. If not, the given URL is taken as the MCP endpoint and the record is unverified.
2. **Does the MCP endpoint answer?** `initialize`, then `tools/list`. If both succeed, the record is **online** and its tools are what the relay reported, with their schemas. If not, the record is **offline**, its last known tools are kept and marked so, and its failure count goes up.
A catalog lists a relay only if at least one question was answered yes. It never lists a relay it could neither verify nor reach. It probes again on a schedule, and a change in online, descriptor or tools is an event.
A catalog holds no credentials for any relay. The probe is unauthenticated, so a relay whose handshake needs a credential will be listed as offline with the tools its descriptor names; the descriptor is the way such a relay is still findable.
## The record
```json
{
"id": "agenticjobs.work",
"source": "https://agenticjobs.work/.well-known/openmcp.json",
"descriptor": { "...": "as served" },
"tools": [{ "name": "search_jobs", "description": "...", "inputSchema": { "...": "..." } }],
"server": { "name": "agenticjobs", "version": "0.15.0", "protocolVersion": "2025-06-18" },
"verified": true,
"online": true,
"seenAt": "2026-09-12T18:04:11.000Z",
"firstSeenAt": "2026-09-12T18:04:11.000Z",
"failures": 0,
"lastError": null,
"via": null
}
```
`id` is a slug of the MCP endpoint's host and path with the endpoint's own name (`/mcp`, `/api/mcp`, `/v1/mcp`) dropped, stable across probes. `via` names another catalog the record was learned from, or is null when it was registered here.
## The catalog
A catalog serves the same descriptor as any relay, at its own `/.well-known/openmcp.json`, with one more key:
```json
{ "catalog": { "relays": 128, "online": 117, "api": "https://openmcp.logicsrc.com/v1", "peers": ["https://other.catalog"] } }
```
It exposes three doors to the same records.
**REST**, under `api`:
| | |
|---|---|
| `GET /v1/relays?q=&tag=&online=1&limit=` | the records, filtered |
| `POST /v1/relays {url}` | register by any URL on the relay's origin; the probe decides what is listed |
| `GET /v1/relays/:id`, `GET /v1/relays/:id/tools` | one record, its tools |
| `POST /v1/relays/:id/refresh` | probe now |
| `DELETE /v1/relays/:id` | admin |
| `GET /v1/tools?q=` | every online relay's tools that match, with where each lives |
| `POST /v1/relays/:id/call {tool, arguments, token?}` | forward one call; the relay's result comes back whole |
| `POST /v1/webhooks`, `GET /v1/webhooks/:id`, `DELETE /v1/webhooks/:id` | subscriptions |
| `GET /v1/peers`, `POST /v1/peers`, `DELETE /v1/peers`, `POST /v1/peers/sync` | peering (admin) |
**MCP**, at the catalog's own `mcp` endpoint, with these tools: `list_relays`, `get_relay`, `find_tool`, `call_tool`, `register_relay`, `refresh_relay`, `subscribe`, `unsubscribe`, `list_peers`. `call_tool` forwards a call to a relay; the caller's credential for that relay travels in the arguments as `token` and is never kept. An agent that can reach one catalog can reach every relay in it.
**Webhooks**, out. A subscription is a URL, a secret, a list of events and optionally a list of relay ids. The events:
| event | when |
|---|---|
| `relay.registered` | a relay was listed for the first time |
| `relay.updated` | its descriptor or tools changed |
| `relay.online` | it answered after not answering |
| `relay.offline` | it stopped answering |
| `relay.removed` | it was removed |
A delivery is one `POST` of `{id, event, at, catalog, relay}` with headers `X-OpenMCP-Event`, `X-OpenMCP-Delivery` and `X-OpenMCP-Signature: sha256=<hex HMAC-SHA256 of the raw body under the secret>`. A receiver verifies the signature over the raw body before reading it. Three attempts, then the failure is recorded; fifty consecutive failures and the subscription is inactive but still listed, so its owner can see why. The subscription id is unguessable and is the only handle on it; the secret is shown once.
Registration is open. Anyone may register any relay, because nothing a registrant types is listed: the probe is. Removing a relay and changing peers need the catalog's admin credential.
## Peering
A catalog may name other catalogs as peers and learn their relays. A relay learned from a peer is still probed here before it is listed, is marked `via` the peer, and is never allowed to overwrite a record that was registered here directly. Catalogs of catalogs, with no catalog required to trust another.
## The client
A conforming client:
1. Reads a catalog over REST or over its MCP endpoint; the two answer the same questions.
2. Treats `online` and `verified` as two facts and shows both. A verified offline relay is real and down; an online unverified relay answers but has not said who it is.
3. Calls a relay's tool through the catalog with `call`, or direct with a plain MCP client once it has the record, and passes its own credential for the relay either way. The catalog never has it.
4. Verifies every webhook delivery's signature over the raw body before acting on it.
5. Reports absence as absence: an unstated `auth`, an unstated `operator`.
## What is deliberately absent
**No registry of names.** A relay's id is derived from its endpoint; nobody owns a name, and two catalogs derive the same id for the same relay.
**No trust score.** `verified` and `online` are facts a catalog checked. Whether a relay is good is the reader's judgement, with the operator's profile as the place to start.
**No credentials in the catalog.** Not for probing, not for forwarding. A relay that needs a credential to be listed serves a descriptor.
**No new transport.** Relays are Streamable HTTP MCP, as MCP defines it. The catalog adds discovery, not protocol.
## Serving one
By hand, or `openmcp descriptor <mcp url>` prints a template. [agenticjobs](https://agenticjobs.work), [tsbb](https://tsbb.dev) and [myna](https://mynaposter.com) serve one. The reference catalog runs on Node 24 with one SQLite file: `npx @logicsrc/openmcp serve`.
## Related standards
- [OpenProfile.md](/openprofile): the `operator` behind a relay.
- [OpenCreds](/opencreds): where the credential a client passes to a relay is kept.
- [Model Context Protocol](https://modelcontextprotocol.io): what a relay speaks.
## Version history
| Version | Date | Change |
|---|---|---|
| 0.1 | 2026-09-12 | First publication: the descriptor, the probe, the record, the three doors, webhooks, peering. |
## License
The specification text is CC BY 4.0. The reference implementation is MIT.