mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-01 20:33:50 +00:00
Add OpenMCP: an open catalog of MCP relays
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. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMT2v1YxmgcDuionrfT719
This commit is contained in:
parent
1a2d143a23
commit
092f2a33d2
6 changed files with 393 additions and 0 deletions
|
|
@ -19,6 +19,7 @@ export function GET(): Response {
|
|||
|
||||
- [ASDLC](${SITE_URL}/asdlc): The Agentic Software Development Lifecycle: nine phases for building software when agents work in parallel and CI/CD is the only gate, with conformance levels and the ratchet rule.
|
||||
- [OpenProfile.md](${SITE_URL}/openprofile): One Markdown file that says who you are and where you are, for people and agents alike: identity block, accounts, topics, reshare terms and operator, discovered at /.well-known/openprofile.md or through rel="openprofile".
|
||||
- [OpenMCP](${SITE_URL}/openmcp): An open catalog of MCP relays: a relay serves /.well-known/openmcp.json, a catalog probes it and lists only what it found, and clients reach every relay through the catalog's REST, its own MCP endpoint, or signed webhooks.
|
||||
- [AgentSwarm](${SITE_URL}/agent-swarm): Provider-neutral agent orchestration, model routing, and cost controls.
|
||||
- [AgentByte](${SITE_URL}/agentbyte): Agent screening sessions, policy events, and APIs.
|
||||
- [Credential Sharing](${SITE_URL}/credential-sharing): End-to-end-encrypted team vaults, plus source/target credential diffs, approval, sync, rollback, and audit.
|
||||
|
|
|
|||
219
apps/logicsrc-web/src/app/openmcp/page.tsx
Normal file
219
apps/logicsrc-web/src/app/openmcp/page.tsx
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenMCP · LogicSRC",
|
||||
description:
|
||||
"OpenMCP is an open catalog protocol for MCP relays: a relay serves /.well-known/openmcp.json, a catalog probes it and lists only what it found, and a client reaches every relay through the catalog's REST, its own MCP endpoint, or signed webhooks. Reference implementation at github.com/logicsrc/openmcp.",
|
||||
alternates: { canonical: "/openmcp" }
|
||||
};
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"openmcp": "0.1",
|
||||
"mcp": "https://agenticjobs.work/api/mcp",
|
||||
"name": "Agentic Jobs",
|
||||
"description": "A job board where agents apply to agents.",
|
||||
"auth": { "kind": "bearer", "open": ["search_jobs", "get_job"] },
|
||||
"tags": ["jobs", "hiring", "agents"],
|
||||
"operator": "https://profullstack.com/.well-known/openprofile.md",
|
||||
"tools": ["search_jobs", "get_job", "apply_to_job", "post_update"]
|
||||
}`;
|
||||
|
||||
const CLI = `npx @logicsrc/openmcp serve --url https://catalog.example # run a catalog
|
||||
openmcp add https://agenticjobs.work # register by any URL on the relay
|
||||
openmcp find "post an update" # search every relay's tools
|
||||
openmcp call agenticjobs.work post_update '{"body":"Shipped."}' --relay-token <t>
|
||||
openmcp webhook add https://me.example/hooks --events relay.offline,relay.online
|
||||
openmcp --transport mcp relays # the same, over MCP`;
|
||||
|
||||
const PROBE: Array<[string, string, string]> = [
|
||||
["1. The descriptor", "GET /.well-known/openmcp.json on the relay's origin", "Parses: the record is verified. The descriptor is the relay's own word, not the registrant's."],
|
||||
["2. The handshake", "initialize, then tools/list at the MCP endpoint", "Answers: the record is online and the tools are what the relay reported, schemas included."],
|
||||
["Neither", "", "Not listed. A catalog never lists a relay it could neither verify nor reach."]
|
||||
];
|
||||
|
||||
const DOORS: Array<[string, string, string, string]> = [
|
||||
["List relays", "GET /v1/relays?q=&tag=&online=1", "list_relays", ""],
|
||||
["One relay, its tools", "GET /v1/relays/:id", "get_relay", ""],
|
||||
["Register, refresh", "POST /v1/relays {url}", "register_relay", "relay.registered, relay.updated"],
|
||||
["Find a tool anywhere", "GET /v1/tools?q=", "find_tool", ""],
|
||||
["Call a relay's tool", "POST /v1/relays/:id/call", "call_tool", ""],
|
||||
["Up, down", "POST /v1/relays/:id/refresh", "refresh_relay", "relay.online, relay.offline"],
|
||||
["Subscribe", "POST /v1/webhooks", "subscribe", "a signed POST per event"],
|
||||
["Peers", "GET/POST /v1/peers, /sync", "list_peers", ""]
|
||||
];
|
||||
|
||||
const ABSENT: Array<[string, string]> = [
|
||||
["No registry of names", "A relay's id is derived from its endpoint. Nobody owns a name; two catalogs derive the same id."],
|
||||
["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 caller's credential for a relay travels with the call and is never kept."],
|
||||
["No new transport", "Relays are Streamable HTTP MCP, as MCP defines it. The catalog adds discovery, not protocol."]
|
||||
];
|
||||
|
||||
export default function OpenMcpPage(): ReactNode {
|
||||
return (
|
||||
<SiteShell active="OpenMCP">
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<p className="eyebrow">LogicSRC standards surface</p>
|
||||
<h2>OpenMCP</h2>
|
||||
<p>
|
||||
An open catalog of MCP relays. A relay says what it is in one file, a catalog lists
|
||||
only what it has reached, and a client gets one door to all of them.
|
||||
</p>
|
||||
</div>
|
||||
<p style={{ color: "#41505d" }}>
|
||||
Every product that speaks MCP is a relay: an endpoint an agent can call. There are
|
||||
thousands, each found by hand from a README and wired into a client's configuration
|
||||
by a person. Nothing says what a relay is, nothing says whether it answered yesterday, and
|
||||
nothing lets an agent reach a relay it was not told about. OpenMCP puts the pieces that
|
||||
already exist, the MCP handshake, <code style={mono}>/.well-known/</code> and webhooks,
|
||||
together so a relay can be discovered instead of configured.
|
||||
</p>
|
||||
<p style={{ color: "#5b6b7a" }}>
|
||||
Status: 0.1. Reference implementation at{" "}
|
||||
<a href="https://github.com/logicsrc/openmcp">github.com/logicsrc/openmcp</a>: a catalog
|
||||
server on Node 24 with one SQLite file, and a client and CLI that speak REST, MCP and
|
||||
webhooks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>The descriptor</h2>
|
||||
<p>
|
||||
Served at <code style={mono}>/.well-known/openmcp.json</code>. Only{" "}
|
||||
<code style={mono}>mcp</code> is required.
|
||||
</p>
|
||||
</div>
|
||||
<pre style={pre}>{DESCRIPTOR}</pre>
|
||||
<p style={{ color: "#41505d" }}>
|
||||
<code style={mono}>auth</code> says how a caller authenticates and which tools are open
|
||||
without a credential. <code style={mono}>operator</code> is the person answerable, as an{" "}
|
||||
<Link href="/openprofile">OpenProfile.md</Link>. <code style={mono}>tools</code> names them
|
||||
so a catalog can index a relay it cannot reach; what <code style={mono}>tools/list</code>{" "}
|
||||
says wins whenever both exist. The descriptor is a claim; the probe is the verification.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>The probe</h2>
|
||||
<p>Two questions, in order, of any URL a catalog is handed.</p>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={th}>Question</th>
|
||||
<th style={th}>How</th>
|
||||
<th style={th}>Then</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{PROBE.map(([question, how, then]) => (
|
||||
<tr key={question}>
|
||||
<td style={td}>
|
||||
<strong>{question}</strong>
|
||||
</td>
|
||||
<td style={td}>{how ? <code style={mono}>{how}</code> : ""}</td>
|
||||
<td style={td}>{then}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<p style={{ color: "#41505d", marginTop: "1rem" }}>
|
||||
The probe is unauthenticated and repeats on a schedule. A change in online, descriptor or
|
||||
tools is an event. Registration is open, because nothing a registrant types is listed:
|
||||
the probe is.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Three doors to the same records</h2>
|
||||
<p>A catalog is itself a relay, and an agent that reaches one reaches everything in it.</p>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={th}></th>
|
||||
<th style={th}>REST</th>
|
||||
<th style={th}>MCP tool</th>
|
||||
<th style={th}>Webhook</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{DOORS.map(([what, rest, tool, hook]) => (
|
||||
<tr key={what}>
|
||||
<td style={td}>
|
||||
<strong>{what}</strong>
|
||||
</td>
|
||||
<td style={td}>
|
||||
<code style={mono}>{rest}</code>
|
||||
</td>
|
||||
<td style={td}>
|
||||
<code style={mono}>{tool}</code>
|
||||
</td>
|
||||
<td style={td}>{hook}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<p style={{ color: "#41505d", marginTop: "1rem" }}>
|
||||
<code style={mono}>call_tool</code> forwards a call to a relay with the caller's own
|
||||
credential, never kept. Every webhook delivery carries{" "}
|
||||
<code style={mono}>X-OpenMCP-Signature: sha256=<HMAC of the raw body></code>. Catalogs
|
||||
peer, and a relay learned from a peer is still probed here before it is listed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>From a terminal</h2>
|
||||
</div>
|
||||
<pre style={pre}>{CLI}</pre>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>What is deliberately absent</h2>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<tbody>
|
||||
{ABSENT.map(([what, why]) => (
|
||||
<tr key={what}>
|
||||
<td style={td}>
|
||||
<strong>{what}</strong>
|
||||
</td>
|
||||
<td style={td}>{why}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Where everything lives</h2>
|
||||
</div>
|
||||
<ul style={{ color: "#41505d", lineHeight: 1.9, paddingLeft: "1.1rem" }}>
|
||||
<li>
|
||||
<Link href="/docs/openmcp">Specification</Link>: the descriptor, the probe, the record,
|
||||
the three doors, webhooks, peering
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/logicsrc/openmcp">github.com/logicsrc/openmcp</a>: the
|
||||
reference catalog and client, <code style={mono}>npx @logicsrc/openmcp</code>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/openprofile">OpenProfile.md</Link>, the operator behind a relay;{" "}
|
||||
<Link href="/opencreds">OpenCreds</Link>, where a caller's credential is kept
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ const STATIC_ROUTES: Array<{
|
|||
{ path: "/openprd", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/asdlc", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openprofile", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openmcp", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openontology/explore", changeFrequency: "daily", priority: 0.7 },
|
||||
{ path: "/openspec", changeFrequency: "weekly", priority: 0.8 },
|
||||
{ path: "/agent-swarm", changeFrequency: "weekly", priority: 0.8 },
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const NAV: Array<{ href: string; label: string; external?: boolean }> = [
|
|||
{ href: "/openprd", label: "OpenPRD" },
|
||||
{ href: "/asdlc", label: "ASDLC" },
|
||||
{ href: "/openprofile", label: "OpenProfile" },
|
||||
{ href: "/openmcp", label: "OpenMCP" },
|
||||
{ href: "/#cli", label: "CLI" },
|
||||
{ href: "/docs", label: "Docs" },
|
||||
{ href: "/blog", label: "Blog" },
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export const DOC_SLUGS = [
|
|||
"openjob",
|
||||
"openresume",
|
||||
"openprofile",
|
||||
"openmcp",
|
||||
"openstream",
|
||||
"openspec-comparison",
|
||||
"data-model",
|
||||
|
|
|
|||
170
docs/openmcp.md
Normal file
170
docs/openmcp.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue