mcp-context-card 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,33 +6,55 @@
6
6
  },
7
7
  "entries": [
8
8
  {
9
- "identifier": "urn:air:mcp-context-card:context",
9
+ "identifier": "urn:air:faf.one:mcp:mcp-context-card",
10
+ "type": "application/mcp-server-card+json",
11
+ "data": {
12
+ "$schema": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
13
+ "name": "io.github.Wolfe-Jam/mcp-context-card",
14
+ "version": "1.5.0",
15
+ "title": "MCP Context Card",
16
+ "description": "MCP server for a project's context (AGENTS.md), memory, and identity — base or drop-in extension.",
17
+ "websiteUrl": "https://github.com/Wolfe-Jam/mcp-context-card",
18
+ "repository": {
19
+ "url": "https://github.com/Wolfe-Jam/mcp-context-card",
20
+ "source": "github"
21
+ },
22
+ "_meta": {
23
+ "io.github.Wolfe-Jam.mcp-context-card/context": {
24
+ "source": "AGENTS.md",
25
+ "mediaType": "text/markdown"
26
+ },
27
+ "io.github.Wolfe-Jam.mcp-context-card/memory": {
28
+ "source": "project.fafm",
29
+ "mediaType": "application/vnd.fafm+yaml",
30
+ "iana": "https://www.iana.org/assignments/media-types/application/vnd.fafm+yaml",
31
+ "note": "no de-facto standard for agent memory yet — this is one instantiation"
32
+ },
33
+ "io.github.Wolfe-Jam.mcp-context-card/identity": {
34
+ "source": ".well-known/fafa",
35
+ "mediaType": "application/vnd.fafa+yaml",
36
+ "iana": "https://www.iana.org/assignments/media-types/application/vnd.fafa+yaml"
37
+ }
38
+ }
39
+ }
40
+ },
41
+ {
42
+ "identifier": "urn:air:faf.one:context:mcp-context-card",
10
43
  "displayName": "mcp-context-card — project context (AGENTS.md)",
11
44
  "type": "text/markdown",
12
- "mediaType": "text/markdown",
13
45
  "description": "Agent instructions for this project — 9 section(s): Setup, Build, Test, Layout, Conventions, The invariant, ….",
14
46
  "url": "./AGENTS.md"
15
47
  },
16
48
  {
17
- "identifier": "urn:air:mcp-context-card:memory",
18
- "displayName": "mcp-context-card — persistent memory (.fafm)",
19
- "type": "application/vnd.fafm+yaml",
20
- "mediaType": "application/vnd.fafm+yaml",
21
- "description": "Cross-session memory — 4 fact(s), profile \"knowledge\". Recall survives a process restart. No de-facto standard for this concern yet.",
22
- "url": "./project.fafm",
23
- "_meta": {
24
- "io.github.Wolfe-Jam.mcp-context-card/iana": "https://www.iana.org/assignments/media-types/application/vnd.fafm+yaml"
25
- }
26
- },
27
- {
28
- "identifier": "urn:air:mcp-context-card:identity",
49
+ "identifier": "urn:air:faf.one:identity:mcp-context-card",
29
50
  "displayName": "mcp-context-card — agent identity (.fafa)",
30
51
  "type": "application/vnd.fafa+yaml",
31
- "mediaType": "application/vnd.fafa+yaml",
32
52
  "description": "The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity — a base MCP on its own, or a drop-in extension for any existing MCP server, discoverable through the Server Card _meta block and a self-published ai-catalog.json.",
33
53
  "url": "./.well-known/fafa",
34
- "_meta": {
35
- "io.github.Wolfe-Jam.mcp-context-card/iana": "https://www.iana.org/assignments/media-types/application/vnd.fafa+yaml"
54
+ "extensions": {
55
+ "io.github.Wolfe-Jam.mcp-context-card": {
56
+ "iana": "https://www.iana.org/assignments/media-types/application/vnd.fafa+yaml"
57
+ }
36
58
  }
37
59
  }
38
60
  ]
package/.well-known/fafa CHANGED
@@ -8,8 +8,12 @@ version: "1.0"
8
8
  agent:
9
9
  name: "mcp-context-card"
10
10
  displayName: "mcp-context-card"
11
+ # Publisher identity in AI Catalog form, as `faf card init` writes it:
12
+ # urn:air:{your domain}:agent:{short name}. This example publishes as faf.one;
13
+ # a deployment for your own project uses your domain.
14
+ id: "urn:air:faf.one:agent:mcp-context-card"
11
15
  vendor: "io.github.Wolfe-Jam"
12
- version: "1.4.0"
16
+ version: "1.5.0"
13
17
  description: >-
14
18
  The essential MCP components for a project's context (AGENTS.md),
15
19
  cross-session memory, and identity — a base MCP on its own, or a
package/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  All notable changes to this project. Adheres to [Semantic Versioning](https://semver.org).
4
4
 
5
+ ## 1.5.0
6
+
7
+ MCP Server Cards (SEP-2127, Final) and AI Catalog 1.0, and HTTP that is local by default.
8
+
9
+ **Breaking for exposed deploys:** HTTP mode now binds `127.0.0.1`. A container or
10
+ hosted deploy must set `HOST=0.0.0.0` (`examples/Dockerfile` does). Local use is
11
+ unchanged.
12
+
13
+ - **Server Card (SEP-2127, Final).** Served at `GET /mcp/server-card`
14
+ (`<streamable-http-url>/server-card`). The 1.x `/.well-known/mcp/server-card`
15
+ still answers with the same card. The card validates against the official v1
16
+ schema: `$schema`, a reverse-DNS `name` (`io.github.Wolfe-Jam/mcp-context-card`,
17
+ as in `server.json`), `title`, `description`, `websiteUrl`, `repository`, and a
18
+ `streamable-http` remote when served over HTTP. `serverInfo` reports the same
19
+ name, title and version.
20
+ - **Discovery hosting.** The card, catalog and `.well-known/fafa` carry the
21
+ spec's CORS (`Access-Control-Allow-Origin: *`, GET, `Content-Type` and
22
+ `If-None-Match` allowed, `ETag` exposed), `Cache-Control: public,
23
+ max-age=3600`, and an `ETag` answered with `304 Not Modified`.
24
+ - **AI Catalog 1.0.** The Server Card is the first entry. Entries use `type`,
25
+ custom data sits in `extensions`, and identifiers are
26
+ `urn:air:{domain}:{namespace}:{name}` with the domain taken from the project's
27
+ own `.fafa` (none is ever invented). `/AGENTS.md` is served, so every catalog
28
+ link resolves with its declared type.
29
+ - **Memory stays private over HTTP.** `project.fafm` is session data, so it is
30
+ not served or listed in the AI Catalog, and an exposed `/card` shows only how
31
+ many facts there are, unless `MCP_CONTEXT_CARD_PUBLISH_MEMORY=1`. Local
32
+ surfaces (the CLI card, the MCP App, the tools, a local `/card`) are unchanged.
33
+ - **Local by default (MCP transports spec).** A foreign browser `Origin` gets
34
+ 403 with a JSON-RPC error. A local server serves loopback `Host` names only,
35
+ so a DNS name rebound to this machine is refused. The server binds
36
+ `127.0.0.1` unless `HOST` is set. `MCP_CONTEXT_CARD_ALLOWED_HOSTS` and
37
+ `MCP_CONTEXT_CARD_ALLOWED_ORIGINS` admit a reverse proxy or a web app. The
38
+ startup line says whether the server is local or exposed, and now prints
39
+ once the socket is listening.
40
+ - **WJTTC suite (`npm run wjttc`).** Seven tiers: Protocol, Server Card,
41
+ Hosting, AI Catalog, Security, stdio/HTTP Parity, Ship. Tiers 2 to 4 and the
42
+ transport checks run `src/conformance/discovery.ts`, a self-contained checker
43
+ for any server URL that reports each requirement as MUST or SHOULD, worded
44
+ as the specs word it.
45
+ - Dependencies: `@modelcontextprotocol/sdk` 1.32.1 (GHSA-6qxp-vccf-f47h) and
46
+ `hono` 4.13.13. `npm audit` reports 0 vulnerabilities.
47
+
48
+ No tool change. 200 tests, all green.
49
+
5
50
  ## 1.4.0
6
51
 
7
52
  The card reads the `.fafa` that `faf card init` writes, and shows the agent's ID.
package/README.md CHANGED
@@ -167,6 +167,16 @@ reports which project it picked and how. To pin one project instead, set
167
167
  wins. Identity is optional. Over HTTP instead:
168
168
  `PORT=8080 npx mcp-context-card`. Requires Node ≥20.
169
169
 
170
+ HTTP mode is local by default, as the MCP transports spec asks: it binds
171
+ `127.0.0.1`, refuses foreign browser origins with 403, and refuses DNS names
172
+ rebound to this machine. `HOST=0.0.0.0` exposes it (a container or hosted
173
+ deploy). There is no authentication or rate limiting, so put an exposed server
174
+ behind your own.
175
+ Memory is session data: an exposed server shows only its count on `/card`, and
176
+ never serves or lists `project.fafm`, unless you set
177
+ `MCP_CONTEXT_CARD_PUBLISH_MEMORY=1`. Details:
178
+ [docs/TRANSPORT.md](./docs/TRANSPORT.md#security-local-by-default).
179
+
170
180
  If `command: "npx"` fails to spawn (`spawn npx ENOENT` — seen on Cursor, whose
171
181
  host process doesn't inherit a shell `PATH`), point `command` at `node` and
172
182
  the installed `dist/bin.js` instead — see
@@ -190,9 +200,10 @@ grows its own shape.
190
200
 
191
201
  1. **Server Card `_meta`** ([SEP‑2127](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127)) —
192
202
  one reverse‑DNS‑namespaced key per concern, readable in‑band as an MCP
193
- resource and at `GET /.well-known/mcp/server-card`.
194
- 2. **`ai-catalog.json`** — sibling entries keyed by media type, at
195
- `GET /.well-known/ai-catalog.json`.
203
+ resource and at `GET /mcp/server-card` (the 1.x `/.well-known/mcp/server-card`
204
+ still answers).
205
+ 2. **`ai-catalog.json`** — the Server Card plus sibling entries keyed by media
206
+ type, at `GET /.well-known/ai-catalog.json`.
196
207
 
197
208
  The context concern points at `AGENTS.md` (`text/markdown`). Memory and identity
198
209
  have no de‑facto standard yet, so the examples here use
@@ -235,11 +246,15 @@ over both transports:
235
246
  4. **Discovery** — `list_context_sources()`, then the same server over stateless
236
247
  HTTP with its `.well-known` routes and `GET /card`.
237
248
 
238
- 140 tests on Linux, macOS, and Windows, coverage‑gated in CI. Two spawn a real
249
+ 200 tests on Linux, macOS, and Windows, coverage‑gated in CI.
250
+ `npm run wjttc` runs the WJTTC certification suite (seven tiers, from protocol
251
+ and Server Card conformance to stdio/HTTP parity and the shipped package). Two spawn a real
239
252
  child process and check a remembered fact survives the restart — one against
240
253
  an existing `project.fafm`, one starting from a project that has never had
241
254
  one; another checks the stdio and HTTP tool surfaces match, and another checks
242
255
  every tool's title and behaviour hints against what it actually does.
256
+ `src/conformance/discovery.ts` checks any server's Server Card, AI Catalog and
257
+ transport security against the specs, one MUST or SHOULD at a time.
243
258
 
244
259
  ## Layout
245
260
 
@@ -252,8 +267,11 @@ every tool's title and behaviour hints against what it actually does.
252
267
  | `src/render-card.ts` | the card — identity + `AGENTS.md` + memory + discovery, as one HTML page |
253
268
  | `src/memory.ts` | file‑backed `remember` / `recall` / `forget` |
254
269
  | `src/identity.ts` | `whoami` (`.fafa` → `package.json` fallback) + the `_meta` block |
255
- | `src/catalog-gen.ts` | writes `ai-catalog.json` from the same three sources |
270
+ | `src/server-card.ts` | the MCP Server Card (SEP-2127, schema v1) |
271
+ | `src/catalog-gen.ts` | writes `ai-catalog.json`: the Server Card + its sibling entries |
256
272
  | `src/transport/http.ts` | the stateless Streamable HTTP app (Hono) |
273
+ | `src/transport/guard.ts` | Origin and Host checks (DNS-rebinding protection) |
274
+ | `src/conformance/discovery.ts` | a portable Server Card / AI Catalog / transport checker |
257
275
  | `src/bin.ts` | the entry point — `stdio` · `--http` · `card` · `--help` · `--version` |
258
276
 
259
277
  ## Related
package/dist/bin.d.ts CHANGED
@@ -8,7 +8,12 @@ export interface Launch {
8
8
  root: string;
9
9
  }
10
10
  /** what a bare `--help` / `help` prints. */
11
- export declare const HELP = "mcp-context-card 1.4.0\nServe a project's context (AGENTS.md), memory, and identity over MCP.\n\nUSAGE\n mcp-context-card stdio MCP server \u2014 what an MCP host spawns (default)\n mcp-context-card --http stateless Streamable HTTP on PORT (default 3000)\n mcp-context-card --stdio force stdio even when PORT is set\n mcp-context-card card this dir's context card \u2014 opens it in your browser\n at a terminal; HTML to stdout when piped ( > f.html )\n --theme light|dark --accent #hex\n --expanded (all sections open) --stdout\n mcp-context-card --help this text\n mcp-context-card --version print version\n\nENV\n MCP_CONTEXT_CARD_ROOT read AGENTS.md / project.fafm / .well-known/ from here\n PORT if set, run HTTP instead of stdio\n\nA bare run is an stdio server: it waits for a host to speak JSON-RPC on stdin,\nso it looks idle at a terminal. Try `card` (opens your context in a browser) or `--http`.\nhttps://github.com/Wolfe-Jam/mcp-context-card\n";
11
+ export declare const HELP = "mcp-context-card 1.5.0\nServe a project's context (AGENTS.md), memory, and identity over MCP.\n\nUSAGE\n mcp-context-card stdio MCP server \u2014 what an MCP host spawns (default)\n mcp-context-card --http stateless Streamable HTTP on PORT (default 3000)\n mcp-context-card --stdio force stdio even when PORT is set\n mcp-context-card card this dir's context card \u2014 opens it in your browser\n at a terminal; HTML to stdout when piped ( > f.html )\n --theme light|dark --accent #hex\n --expanded (all sections open) --stdout\n mcp-context-card --help this text\n mcp-context-card --version print version\n\nENV\n MCP_CONTEXT_CARD_ROOT read AGENTS.md / project.fafm / .well-known/ from here\n PORT if set, run HTTP instead of stdio\n HOST HTTP bind address (default 127.0.0.1, this machine\n only); 0.0.0.0 exposes it, e.g. in a container\n MCP_CONTEXT_CARD_ALLOWED_HOSTS / _ALLOWED_ORIGINS\n comma-separated Host names / browser origins to\n accept besides this machine's own (reverse proxy, web app)\n MCP_CONTEXT_CARD_PUBLISH_MEMORY=1\n HTTP: serve project.fafm, list it in the AI Catalog,\n and show its facts on an exposed /card (off: memory\n is session data). HTTP has no auth\n\nA bare run is an stdio server: it waits for a host to speak JSON-RPC on stdin,\nso it looks idle at a terminal. Try `card` (opens your context in a browser) or `--http`.\nhttps://github.com/Wolfe-Jam/mcp-context-card\n";
12
+ /**
13
+ * The address the HTTP server binds: `HOST`, else 127.0.0.1. Local by default,
14
+ * as the MCP transports spec recommends; a hosted deploy sets HOST=0.0.0.0.
15
+ */
16
+ export declare function bindHost(env?: NodeJS.ProcessEnv): string;
12
17
  /**
13
18
  * Decide how to launch, from argv + env. Pure — so the mode matrix is unit
14
19
  * tested without spawning a process.
package/dist/bin.js CHANGED
@@ -41,11 +41,27 @@ USAGE
41
41
  ENV
42
42
  MCP_CONTEXT_CARD_ROOT read AGENTS.md / project.fafm / .well-known/ from here
43
43
  PORT if set, run HTTP instead of stdio
44
+ HOST HTTP bind address (default 127.0.0.1, this machine
45
+ only); 0.0.0.0 exposes it, e.g. in a container
46
+ MCP_CONTEXT_CARD_ALLOWED_HOSTS / _ALLOWED_ORIGINS
47
+ comma-separated Host names / browser origins to
48
+ accept besides this machine's own (reverse proxy, web app)
49
+ MCP_CONTEXT_CARD_PUBLISH_MEMORY=1
50
+ HTTP: serve project.fafm, list it in the AI Catalog,
51
+ and show its facts on an exposed /card (off: memory
52
+ is session data). HTTP has no auth
44
53
 
45
54
  A bare run is an stdio server: it waits for a host to speak JSON-RPC on stdin,
46
55
  so it looks idle at a terminal. Try \`card\` (opens your context in a browser) or \`--http\`.
47
56
  https://github.com/Wolfe-Jam/mcp-context-card
48
57
  `;
58
+ /**
59
+ * The address the HTTP server binds: `HOST`, else 127.0.0.1. Local by default,
60
+ * as the MCP transports spec recommends; a hosted deploy sets HOST=0.0.0.0.
61
+ */
62
+ export function bindHost(env = process.env) {
63
+ return env.HOST?.trim() || "127.0.0.1";
64
+ }
49
65
  /**
50
66
  * Decide how to launch, from argv + env. Pure — so the mode matrix is unit
51
67
  * tested without spawning a process.
@@ -131,9 +147,17 @@ if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
131
147
  else if (mode === "http") {
132
148
  const { httpApp } = await import("./transport/http.js");
133
149
  const { serve: serveHttp } = await import("@hono/node-server");
134
- serveHttp({ fetch: httpApp(root).fetch, port });
150
+ const { isLoopbackBind } = await import("./transport/guard.js");
151
+ const host = bindHost();
152
+ const local = isLoopbackBind(host);
153
+ // The startup line prints once the socket is listening, so anything waiting
154
+ // for it (a test, a supervisor) can connect straight away.
155
+ serveHttp({ fetch: httpApp(root, { exposure: local ? "local" : "exposed" }).fetch, port, hostname: host }, () =>
135
156
  // stderr, not stdout — stdout is the MCP wire in stdio mode.
136
- console.error(`${NAME} · http · :${port} (POST /mcp · GET /card · GET /.well-known/*)`);
157
+ console.error(`${NAME} · http · ${host.includes(":") ? `[${host}]` : host}:${port} (POST /mcp · GET /card · GET /.well-known/*)` +
158
+ (local
159
+ ? " · local only (HOST=0.0.0.0 to expose)"
160
+ : " · EXPOSED beyond this machine, no authentication: put it behind your own")));
137
161
  }
138
162
  else {
139
163
  // stderr so it never touches the JSON-RPC wire on stdout; a bare run at a
@@ -1,26 +1,77 @@
1
- export declare function buildCatalog(root: string): {
1
+ export interface CatalogOptions {
2
+ /** Public origin the catalog is served from, e.g. `https://ctx.example.com`. */
3
+ origin?: string;
4
+ /** List the memory file (`project.fafm`). Default: off. */
5
+ publishMemory?: boolean;
6
+ }
7
+ export declare function buildCatalog(root: string, opts?: CatalogOptions): {
2
8
  specVersion: string;
3
9
  host: {
4
10
  displayName: string;
5
11
  identifier: string;
6
12
  };
7
13
  entries: ({
14
+ url: string;
15
+ identifier: string;
16
+ type: string;
17
+ displayName?: undefined;
18
+ description?: undefined;
19
+ } | {
20
+ data: {
21
+ _meta: {
22
+ readonly "io.github.Wolfe-Jam.mcp-context-card/context": {
23
+ readonly source: "AGENTS.md";
24
+ readonly mediaType: "text/markdown";
25
+ };
26
+ readonly "io.github.Wolfe-Jam.mcp-context-card/memory": {
27
+ readonly source: "project.fafm";
28
+ readonly mediaType: "application/vnd.fafm+yaml";
29
+ readonly iana: string;
30
+ readonly note: "no de-facto standard for agent memory yet — this is one instantiation";
31
+ };
32
+ readonly "io.github.Wolfe-Jam.mcp-context-card/identity": {
33
+ readonly source: ".well-known/fafa";
34
+ readonly mediaType: "application/vnd.fafa+yaml";
35
+ readonly iana: string;
36
+ };
37
+ };
38
+ remotes?: {
39
+ type: "streamable-http";
40
+ url: string;
41
+ supportedProtocolVersions: string[];
42
+ }[] | undefined;
43
+ $schema: string;
44
+ name: string;
45
+ version: string;
46
+ title: string;
47
+ description: string;
48
+ websiteUrl: string;
49
+ repository: {
50
+ url: string;
51
+ source: string;
52
+ };
53
+ };
54
+ identifier: string;
55
+ type: string;
56
+ displayName?: undefined;
57
+ description?: undefined;
58
+ url?: undefined;
59
+ } | {
8
60
  identifier: string;
9
61
  displayName: string;
10
62
  type: string;
11
- mediaType: string;
12
63
  description: string;
13
64
  url: string;
14
- _meta?: undefined;
15
65
  } | {
66
+ extensions: {
67
+ "io.github.Wolfe-Jam.mcp-context-card": {
68
+ iana: string;
69
+ };
70
+ };
16
71
  identifier: string;
17
72
  displayName: string;
18
73
  type: string;
19
- mediaType: string;
20
74
  description: string;
21
75
  url: string;
22
- _meta: {
23
- "io.github.Wolfe-Jam.mcp-context-card/iana": string;
24
- };
25
76
  })[];
26
77
  };
@@ -1,11 +1,25 @@
1
1
  /**
2
- * catalog-gen — write `.well-known/ai-catalog.json` FROM the three sources
3
- * (`AGENTS.md`, `project.fafm`, `.well-known/fafa`) that also back the Server
4
- * Card `_meta` block. Same three artifacts, second exposure mechanism.
2
+ * catalog-gen — the AI Catalog (`application/ai-catalog+json`, spec 1.0) for
3
+ * this server: its MCP Server Card, plus the sources that also back the Server
4
+ * Card `_meta` block: `AGENTS.md` and `.fafa`, and `project.fafm` only when the
5
+ * project opts in ({@link CatalogOptions.publishMemory}). Memory is written
6
+ * during sessions, and SEP-2127 rules user- or session-specific data out of
7
+ * public discovery documents, so it is left out by default.
8
+ *
9
+ * Identifiers follow `urn:air:{publisher}:{namespace}:{name}`, with the
10
+ * publisher domain and short name taken from the project's own `.fafa`
11
+ * ({@link catalogPublisher}). Served over HTTP without a `.fafa` domain, the
12
+ * request host is the publisher (whoever serves it controls that domain). The
13
+ * committed static file with no domain uses plain `{name}:{namespace}` ids:
14
+ * the spec treats unknown schemes as opaque, and no domain is ever invented.
15
+ *
16
+ * Served (an origin is known): entries point at absolute URLs and the card
17
+ * entry links `<origin>/mcp/server-card`. Static: relative URLs and the card
18
+ * inline as `data` (no remotes, since no origin is known).
5
19
  *
6
20
  * Descriptions are derived from real file content (section count, fact count,
7
- * the agent's own description) — not hand-written blurbs that drift. The CI
8
- * job `catalog:check` regenerates this and fails on any diff.
21
+ * the agent's own description), not hand-written blurbs that drift. The CI
22
+ * job `catalog:check` regenerates the static file and fails on any diff.
9
23
  */
10
24
  import { writeFileSync } from "node:fs";
11
25
  import { dirname, join } from "node:path";
@@ -13,14 +27,21 @@ import { fileURLToPath, pathToFileURL } from "node:url";
13
27
  import { parseAgentsMd } from "./agents-md.js";
14
28
  import { parseFafm } from "./faf/parse-fafm.js";
15
29
  import { parseFafa } from "./faf/parse-fafa.js";
16
- import { META_NS, fafaFile } from "./identity.js";
30
+ import { META_NS, catalogPublisher, fafaFile } from "./identity.js";
31
+ import { SERVER_CARD_MEDIA_TYPE, SERVER_CARD_PATH } from "./constants.js";
32
+ import { serverCard } from "./server-card.js";
17
33
  const iana = (t) => `https://www.iana.org/assignments/media-types/${t}`;
18
- export function buildCatalog(root) {
34
+ export function buildCatalog(root, opts = {}) {
19
35
  const agents = parseAgentsMd(join(root, "AGENTS.md"));
20
36
  const fafm = parseFafm(join(root, "project.fafm"));
21
37
  const file = fafaFile(root);
22
38
  const fafa = file ? parseFafa(file) : null;
23
- const host = fafa?.name ?? "mcp-context-card";
39
+ const { domain, handle } = catalogPublisher(root);
40
+ const publisher = domain ?? (opts.origin ? new URL(opts.origin).hostname.toLowerCase() : undefined);
41
+ const id = (namespace) => publisher ? `urn:air:${publisher}:${namespace}:${handle}` : `${handle}:${namespace}`;
42
+ const at = (path) => (opts.origin ? `${opts.origin}/${path}` : `./${path}`);
43
+ const ext = (type) => ({ extensions: { [META_NS]: { iana: iana(type) } } });
44
+ const host = fafa?.displayName ?? fafa?.name ?? "mcp-context-card";
24
45
  return {
25
46
  specVersion: "1.0",
26
47
  host: {
@@ -29,10 +50,16 @@ export function buildCatalog(root) {
29
50
  },
30
51
  entries: [
31
52
  {
32
- identifier: `urn:air:${host}:context`,
53
+ // The card carries its own title, description and version, so the
54
+ // entry repeats none of them (AI Catalog: avoid drift).
55
+ identifier: id("mcp"),
56
+ type: SERVER_CARD_MEDIA_TYPE,
57
+ ...(opts.origin ? { url: `${opts.origin}${SERVER_CARD_PATH}` } : { data: serverCard() }),
58
+ },
59
+ {
60
+ identifier: id("context"),
33
61
  displayName: `${host} — project context (AGENTS.md)`,
34
62
  type: "text/markdown",
35
- mediaType: "text/markdown",
36
63
  description: agents
37
64
  ? (() => {
38
65
  const h = agents.sections.filter((s) => s.level > 1).map((s) => s.heading);
@@ -41,33 +68,31 @@ export function buildCatalog(root) {
41
68
  .join(", ")}${h.length > 6 ? ", …" : ""}.`;
42
69
  })()
43
70
  : "Agent instructions for this project (AGENTS.md — not present).",
44
- url: "./AGENTS.md",
45
- },
46
- {
47
- identifier: `urn:air:${host}:memory`,
48
- displayName: `${host} — persistent memory (.fafm)`,
49
- type: "application/vnd.fafm+yaml",
50
- mediaType: "application/vnd.fafm+yaml",
51
- description: `Cross-session memory — ${fafm.facts.length} fact(s), profile "${fafm.profile ?? "?"}". Recall survives a process restart. No de-facto standard for this concern yet.`,
52
- url: "./project.fafm",
53
- _meta: { [`${META_NS}/iana`]: iana("application/vnd.fafm+yaml") },
71
+ url: at("AGENTS.md"),
54
72
  },
73
+ ...(opts.publishMemory ? [{
74
+ identifier: id("memory"),
75
+ displayName: `${host} — persistent memory (.fafm)`,
76
+ type: "application/vnd.fafm+yaml",
77
+ description: `Cross-session memory — ${fafm.facts.length} fact(s), profile "${fafm.profile ?? "?"}". Recall survives a process restart. No de-facto standard for this concern yet.`,
78
+ url: at("project.fafm"),
79
+ ...ext("application/vnd.fafm+yaml"),
80
+ }] : []),
55
81
  {
56
- identifier: `urn:air:${host}:identity`,
82
+ identifier: id("identity"),
57
83
  displayName: `${host} — agent identity (.fafa)`,
58
84
  type: "application/vnd.fafa+yaml",
59
- mediaType: "application/vnd.fafa+yaml",
60
85
  description: fafa?.description ??
61
86
  `Agent identity card (status: ${fafa?.status ?? "unknown"}).`,
62
- url: "./.well-known/fafa",
63
- _meta: { [`${META_NS}/iana`]: iana("application/vnd.fafa+yaml") },
87
+ url: at(".well-known/fafa"),
88
+ ...ext("application/vnd.fafa+yaml"),
64
89
  },
65
90
  ],
66
91
  };
67
92
  }
68
- // Direct run → write the file.
93
+ // Direct run → write the static file (no origin: relative URLs, card inline).
69
94
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
70
95
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
71
96
  writeFileSync(join(root, ".well-known/ai-catalog.json"), JSON.stringify(buildCatalog(root), null, 2) + "\n");
72
- console.log("wrote .well-known/ai-catalog.json — 3 sibling entries, derived from AGENTS.md / project.fafm / .well-known/fafa");
97
+ console.log("wrote .well-known/ai-catalog.json — Server Card + AGENTS.md + .fafa entries (memory is opt-in, never in the static file)");
73
98
  }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * conformance/discovery — a portable checker for MCP Server Card discovery
3
+ * (SEP-2127, Final), the AI Catalog (spec 1.0), and the Streamable HTTP
4
+ * transport's security rules, run against any server URL.
5
+ *
6
+ * Self-contained on purpose: no imports from this package, only the global
7
+ * `fetch` (and `node:http` for the one check that must forge a Host header,
8
+ * which `fetch` drops), so the file can be lifted into another tool as-is.
9
+ * The transport checks send one JSON-RPC `ping`, which changes nothing. Point it at a
10
+ * server's Streamable HTTP endpoint and it returns one result per requirement,
11
+ * each tagged MUST or SHOULD exactly as the spec words it. Requirements the
12
+ * spec leaves at MAY (where the card lives, whether a catalog exists) are
13
+ * never failures: a missing optional document turns its checks into skips.
14
+ *
15
+ * JSON Schema validation is optional: pass `validateCard` (for example Ajv
16
+ * against the official v1 schema) and it runs as one more card check.
17
+ */
18
+ export declare const SERVER_CARD_SCHEMA_URL = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json";
19
+ export declare const SERVER_CARD_TYPE = "application/mcp-server-card+json";
20
+ export declare const AI_CATALOG_TYPE = "application/ai-catalog+json";
21
+ export type Tier = "card" | "hosting" | "catalog" | "transport";
22
+ export type Level = "must" | "should";
23
+ export type Status = "pass" | "fail" | "skip";
24
+ export interface CheckResult {
25
+ id: string;
26
+ tier: Tier;
27
+ level: Level;
28
+ title: string;
29
+ status: Status;
30
+ detail?: string;
31
+ }
32
+ export interface DiscoveryTarget {
33
+ /** The server's Streamable HTTP endpoint, e.g. `https://ctx.example.com/mcp`. */
34
+ mcpUrl: string;
35
+ /** Where to read the card. Default: `<mcpUrl>/server-card`, then the catalog's card entry. */
36
+ cardUrl?: string;
37
+ /** Where to read the catalog. Default: `<origin>/.well-known/ai-catalog.json`. */
38
+ catalogUrl?: string;
39
+ }
40
+ export interface DiscoveryOptions {
41
+ fetch?: typeof fetch;
42
+ /** Returns null when the card is valid, else a short error. */
43
+ validateCard?: (card: unknown) => string | null;
44
+ /** Per-request timeout in milliseconds (default 10000). */
45
+ timeoutMs?: number;
46
+ /** POST with exact headers (Host included); returns the status, or null. Default: node:http. */
47
+ rawStatus?: (url: string, headers: Record<string, string>, body: string, timeoutMs: number) => Promise<number | null>;
48
+ }
49
+ export interface DiscoveryReport {
50
+ mcpUrl: string;
51
+ cardUrl: string | null;
52
+ catalogUrl: string;
53
+ results: CheckResult[];
54
+ passed: number;
55
+ failed: number;
56
+ skipped: number;
57
+ /** Failed MUST checks: zero means nothing the spec requires is broken. */
58
+ mustFailures: number;
59
+ }
60
+ export declare function checkDiscovery(target: DiscoveryTarget, opts?: DiscoveryOptions): Promise<DiscoveryReport>;