diff --git a/apps/logicsrc-web/src/app/llms.txt/route.ts b/apps/logicsrc-web/src/app/llms.txt/route.ts index 7ef5e0d..c0f1308 100644 --- a/apps/logicsrc-web/src/app/llms.txt/route.ts +++ b/apps/logicsrc-web/src/app/llms.txt/route.ts @@ -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. diff --git a/apps/logicsrc-web/src/app/openmcp/page.tsx b/apps/logicsrc-web/src/app/openmcp/page.tsx new file mode 100644 index 0000000..697877d --- /dev/null +++ b/apps/logicsrc-web/src/app/openmcp/page.tsx @@ -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 +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 ( + +
+
+

LogicSRC standards surface

+

OpenMCP

+

+ 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. +

+
+

+ 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, /.well-known/ and webhooks, + together so a relay can be discovered instead of configured. +

+

+ Status: 0.1. Reference implementation at{" "} + github.com/logicsrc/openmcp: a catalog + server on Node 24 with one SQLite file, and a client and CLI that speak REST, MCP and + webhooks. +

+
+ +
+
+

The descriptor

+

+ Served at /.well-known/openmcp.json. Only{" "} + mcp is required. +

+
+
{DESCRIPTOR}
+

+ auth says how a caller authenticates and which tools are open + without a credential. operator is the person answerable, as an{" "} + OpenProfile.md. tools names them + so a catalog can index a relay it cannot reach; what tools/list{" "} + says wins whenever both exist. The descriptor is a claim; the probe is the verification. +

+
+ +
+
+

The probe

+

Two questions, in order, of any URL a catalog is handed.

+
+ + + + + + + + + + {PROBE.map(([question, how, then]) => ( + + + + + + ))} + +
QuestionHowThen
+ {question} + {how ? {how} : ""}{then}
+

+ 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. +

+
+ +
+
+

Three doors to the same records

+

A catalog is itself a relay, and an agent that reaches one reaches everything in it.

+
+ + + + + + + + + + + {DOORS.map(([what, rest, tool, hook]) => ( + + + + + + + ))} + +
RESTMCP toolWebhook
+ {what} + + {rest} + + {tool} + {hook}
+

+ call_tool forwards a call to a relay with the caller's own + credential, never kept. Every webhook delivery carries{" "} + X-OpenMCP-Signature: sha256=<HMAC of the raw body>. Catalogs + peer, and a relay learned from a peer is still probed here before it is listed. +

+
+ +
+
+

From a terminal

+
+
{CLI}
+
+ +
+
+

What is deliberately absent

+
+ + + {ABSENT.map(([what, why]) => ( + + + + + ))} + +
+ {what} + {why}
+
+ +
+
+

Where everything lives

+
+
    +
  • + Specification: the descriptor, the probe, the record, + the three doors, webhooks, peering +
  • +
  • + github.com/logicsrc/openmcp: the + reference catalog and client, npx @logicsrc/openmcp +
  • +
  • + OpenProfile.md, the operator behind a relay;{" "} + OpenCreds, where a caller's credential is kept +
  • +
+
+
+ ); +} diff --git a/apps/logicsrc-web/src/app/sitemap.ts b/apps/logicsrc-web/src/app/sitemap.ts index 511115c..7790e50 100644 --- a/apps/logicsrc-web/src/app/sitemap.ts +++ b/apps/logicsrc-web/src/app/sitemap.ts @@ -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 }, diff --git a/apps/logicsrc-web/src/components/site-shell.tsx b/apps/logicsrc-web/src/components/site-shell.tsx index 8be7b4a..62789a3 100644 --- a/apps/logicsrc-web/src/components/site-shell.tsx +++ b/apps/logicsrc-web/src/components/site-shell.tsx @@ -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" }, diff --git a/apps/logicsrc-web/src/lib/docs.ts b/apps/logicsrc-web/src/lib/docs.ts index 092e04f..1fbb672 100644 --- a/apps/logicsrc-web/src/lib/docs.ts +++ b/apps/logicsrc-web/src/lib/docs.ts @@ -18,6 +18,7 @@ export const DOC_SLUGS = [ "openjob", "openresume", "openprofile", + "openmcp", "openstream", "openspec-comparison", "data-model", diff --git a/docs/openmcp.md b/docs/openmcp.md new file mode 100644 index 0000000..f9a81ee --- /dev/null +++ b/docs/openmcp.md @@ -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=`. 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 ` 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.