mcp-context-card 1.4.0 → 1.6.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.6.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.6.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
@@ -17,3 +21,8 @@ agent:
17
21
  Server Card _meta block and a self-published ai-catalog.json.
18
22
  status: "published"
19
23
  license: "MIT"
24
+ metadata:
25
+ cards:
26
+ packages:
27
+ - registryType: "npm"
28
+ identifier: "mcp-context-card"
package/CHANGELOG.md CHANGED
@@ -2,6 +2,92 @@
2
2
 
3
3
  All notable changes to this project. Adheres to [Semantic Versioning](https://semver.org).
4
4
 
5
+ ## 1.6.0
6
+
7
+ The card is a business card.
8
+
9
+ `npx mcp-context-card card` used to open a technical page. It now opens a
10
+ **business card**: what you'd hand someone on the front, and the full detail
11
+ one flip away.
12
+
13
+ - **Front:** a monogram logo (the first letters of the name: **MC**), the
14
+ name, a title, and a one-liner.
15
+ - The **title** says what it is and its version ("MCP server · v1.6.0",
16
+ "A2A agent · v0.3.0"), from where the project's `.fafa` says it runs
17
+ (`endpoints`, or `metadata.cards.packages`). With nothing declared, it
18
+ claims nothing.
19
+ - The **one-liner** is the start of the description, stopping at its first
20
+ natural pause rather than mid-phrase.
21
+ - Domain, publisher, status and licence sit along the bottom.
22
+ - **Back:** tabs for About (the identity in full), Skills (when the `.fafa`
23
+ lists capabilities), Context (AGENTS.md), Memory and Discovery. Long content
24
+ scrolls inside the card, including by mouse wheel or trackpad on macOS.
25
+ - **Controls:** the same two on both faces. **(i)** top-right explains
26
+ Business Cards for Agents; the outline flip button bottom-right (↻ on the
27
+ front, ↺ on the back) has an instant tooltip.
28
+ - **Views:** ▭ ▯ switches landscape or portrait (`--portrait`,
29
+ `?layout=portrait`, `layout` on the tools). ◐ switches the **card** between
30
+ light and dark. `--theme` / `?theme=` set the card's starting colour, and
31
+ the page around it follows your OS on its own.
32
+ - **Works with scripts off:** flip, tabs and both toggles are CSS. The one
33
+ small script adds Expand all, `#section` links on the back, and printing.
34
+ `--expanded` and print lay both faces out flat.
35
+ - **The `.fafa` reader** now reads `endpoints`, `capabilities` and
36
+ `metadata.cards.packages`, as `faf card init` writes them. This repo's own
37
+ `.fafa` declares its npm package.
38
+ - **Read and render are separate:** `readCard` turns a project's files into a
39
+ neutral business card; `renderBusinessCard` draws any card, so a person's
40
+ card or an A2A Agent Card needs no renderer change.
41
+ - The saved file is still `context-card.html`: underneath, it's the project's
42
+ context.
43
+
44
+ No tool or protocol change. 209 tests, all green.
45
+
46
+ ## 1.5.0
47
+
48
+ MCP Server Cards (SEP-2127, Final) and the AI Catalog (specVersion 1.0), and HTTP that is local by default.
49
+
50
+ **Breaking for exposed deploys:** HTTP mode now binds `127.0.0.1`. A container or
51
+ hosted deploy must set `HOST=0.0.0.0` (`examples/Dockerfile` does). Local use is
52
+ unchanged.
53
+
54
+ - **Server Card (SEP-2127, Final).** Served at `GET /mcp/server-card`
55
+ (`<streamable-http-url>/server-card`). The 1.x `/.well-known/mcp/server-card`
56
+ still answers with the same card. The card validates against the official v1
57
+ schema: `$schema`, a reverse-DNS `name` (`io.github.Wolfe-Jam/mcp-context-card`,
58
+ as in `server.json`), `title`, `description`, `websiteUrl`, `repository`, and a
59
+ `streamable-http` remote when served over HTTP. `serverInfo` reports the same
60
+ name, title and version.
61
+ - **Discovery hosting.** The card, catalog and `.well-known/fafa` carry the
62
+ spec's CORS (`Access-Control-Allow-Origin: *`, GET, `Content-Type` and
63
+ `If-None-Match` allowed, `ETag` exposed), `Cache-Control: public,
64
+ max-age=3600`, and an `ETag` answered with `304 Not Modified`.
65
+ - **AI Catalog (specVersion 1.0).** The Server Card is the first entry. Entries use `type`,
66
+ custom data sits in `extensions`, and identifiers are
67
+ `urn:air:{domain}:{namespace}:{name}` with the domain taken from the project's
68
+ own `.fafa` (none is ever invented). `/AGENTS.md` is served, so every catalog
69
+ link resolves with its declared type.
70
+ - **Memory stays private over HTTP.** `project.fafm` is session data, so it is
71
+ not served or listed in the AI Catalog, and an exposed `/card` shows only how
72
+ many facts there are, unless `MCP_CONTEXT_CARD_PUBLISH_MEMORY=1`. Local
73
+ surfaces (the CLI card, the MCP App, the tools, a local `/card`) are unchanged.
74
+ - **Local by default (MCP transports spec).** A foreign browser `Origin` gets
75
+ 403 with a JSON-RPC error. A local server serves loopback `Host` names only,
76
+ so a DNS name rebound to this machine is refused. The server binds
77
+ `127.0.0.1` unless `HOST` is set. `MCP_CONTEXT_CARD_ALLOWED_HOSTS` and
78
+ `MCP_CONTEXT_CARD_ALLOWED_ORIGINS` admit a reverse proxy or a web app. The
79
+ startup line says whether the server is local or exposed, and now prints
80
+ once the socket is listening.
81
+ - **WJTTC suite (`npm run wjttc`).** Seven tiers: Protocol, Server Card,
82
+ Hosting, AI Catalog, Security, stdio/HTTP Parity, Ship. Tiers 2 to 4 and the
83
+ transport checks run `src/conformance/discovery.ts`, a self-contained checker
84
+ for any server URL that reports each requirement as MUST or SHOULD, worded
85
+ as the specs word it.
86
+ - Dependencies: `@modelcontextprotocol/sdk` 1.32.1 (GHSA-6qxp-vccf-f47h) and
87
+ `hono` 4.13.13. `npm audit` reports 0 vulnerabilities.
88
+
89
+ No tool change. 200 tests, all green.
90
+
5
91
  ## 1.4.0
6
92
 
7
93
  The card reads the `.fafa` that `faf card init` writes, and shows the agent's ID.
package/README.md CHANGED
@@ -11,10 +11,11 @@ rendered as one card you can read.
11
11
 
12
12
  | Light | Dark |
13
13
  |---|---|
14
- | ![the context card, light theme](./docs/img/card-light.png) | ![the context card, dark theme](./docs/img/card-dark.png) |
14
+ | ![the business card front, light theme: name, title, one-liner](./docs/img/card-light.png) | ![the business card front, dark theme](./docs/img/card-dark.png) |
15
15
 
16
- **See your own project's card** — run this in its folder. It writes
17
- `context-card.html` and opens it in your browser:
16
+ **See your own project's business card** — run this in its folder. It writes
17
+ `context-card.html` and opens it in your browser. The front is the card: name,
18
+ title, one-liner. Flip it for the back: About, Context, Memory, Discovery.
18
19
 
19
20
  ```
20
21
  npx mcp-context-card card
@@ -22,15 +23,15 @@ npx mcp-context-card card
22
23
 
23
24
  **context** — the project's `AGENTS.md`, served whole or one section at a time.
24
25
 
25
- ![the card's context section — AGENTS.md, section nav, read_agents_md](./docs/img/card-context.png)
26
+ ![the back's Context tab — AGENTS.md, section nav, read_agents_md](./docs/img/card-context.png)
26
27
 
27
28
  **memory** — facts that persist across sessions, in a file.
28
29
 
29
- ![the card's memory section — a real, tagged, verified fact](./docs/img/card-memory.png)
30
+ ![the back's Memory tab — real, tagged, verified facts](./docs/img/card-memory.png)
30
31
 
31
32
  **identity** — what this server is, from its own `.fafa` (`agent.fafa`, else `.well-known/fafa`).
32
33
 
33
- ![the card's identity — name and pills](./docs/img/card-identity.png)
34
+ ![the back's About tab — the identity in full](./docs/img/card-identity.png)
34
35
 
35
36
  Discovery goes through two surfaces already in the ecosystem: the Server Card
36
37
  `_meta` block and `ai-catalog.json` sibling entries.
@@ -76,23 +77,34 @@ no search.
76
77
 
77
78
  ## The card
78
79
 
79
- The screenshot at the top of this page is exactly this — the same three
80
- sources rendered as one self‑contained HTML page: identity, `AGENTS.md`,
81
- memory, and how a machine fetches it. The view for people: read it, screenshot
82
- it, drop it in a PR, put it on a status page.
83
-
84
- `AGENTS.md` sections are collapsed by default, so the card scans in one screen;
85
- a sticky index jumps to any section, **Expand all** opens everything. Sections
86
- toggle natively — the one small inline script only adds the bulk button and the
87
- print handler. `--expanded` / `?expand=all` renders it fully open, script-free,
88
- for a screenshot.
80
+ The screenshot at the top of this page is exactly this: the same sources
81
+ rendered as a **business card** in one self‑contained HTML page. The view for
82
+ people: read it, screenshot it, drop it in a PR, put it on a status page.
83
+
84
+ - **Front:** the name, a title (what it is and its version: "MCP server", "A2A
85
+ agent", from where the project's `.fafa` says it runs) and a one-liner (the
86
+ start of its description).
87
+ - **Back (Flip):** tabs for **About** (the identity in full),
88
+ **Skills** (when the `.fafa` lists capabilities), **Context** (`AGENTS.md`),
89
+ **Memory** and **Discovery**. Long content scrolls inside the card.
90
+ - **Landscape or portrait:** the ▭ ▯ button switches the view; `--portrait` /
91
+ `?layout=portrait` sets it.
92
+ - **Light or dark card:** the card's colour is its own, like a printed card.
93
+ `--theme` / `?theme=` sets it (default: follow your OS), and ◐ switches it.
94
+ The page around it always follows your OS.
95
+
96
+ Flip, tabs and the view switch are plain CSS, so they work with scripts off.
97
+ The one small inline script adds Expand all, opens a `#section` link on the
98
+ back, and opens every section for printing. `--expanded` / `?expand=all`
99
+ renders both faces flat, every section open, for a screenshot or print.
89
100
 
90
101
  ```
91
102
  npx mcp-context-card card # at a terminal: writes context-card.html and opens it
92
- npx mcp-context-card card --expanded # every section open
103
+ npx mcp-context-card card --portrait # the tall view
104
+ npx mcp-context-card card --expanded # both faces flat, every section open
93
105
  npx mcp-context-card card > x.html # piped/redirected: raw HTML to stdout
94
106
  GET /card # live, on the HTTP transport
95
- GET /card?expand=all&theme=light&accent=%230066cc
107
+ GET /card?layout=portrait&theme=light&accent=%230066cc
96
108
  ```
97
109
 
98
110
  In a chat, hosts that support [MCP Apps](https://modelcontextprotocol.io/extensions/apps)
@@ -167,6 +179,16 @@ reports which project it picked and how. To pin one project instead, set
167
179
  wins. Identity is optional. Over HTTP instead:
168
180
  `PORT=8080 npx mcp-context-card`. Requires Node ≥20.
169
181
 
182
+ HTTP mode is local by default, as the MCP transports spec asks: it binds
183
+ `127.0.0.1`, refuses foreign browser origins with 403, and refuses DNS names
184
+ rebound to this machine. `HOST=0.0.0.0` exposes it (a container or hosted
185
+ deploy). There is no authentication or rate limiting, so put an exposed server
186
+ behind your own.
187
+ Memory is session data: an exposed server shows only its count on `/card`, and
188
+ never serves or lists `project.fafm`, unless you set
189
+ `MCP_CONTEXT_CARD_PUBLISH_MEMORY=1`. Details:
190
+ [docs/TRANSPORT.md](./docs/TRANSPORT.md#security-local-by-default).
191
+
170
192
  If `command: "npx"` fails to spawn (`spawn npx ENOENT` — seen on Cursor, whose
171
193
  host process doesn't inherit a shell `PATH`), point `command` at `node` and
172
194
  the installed `dist/bin.js` instead — see
@@ -178,6 +200,19 @@ Extending an MCP you already run: most hosts accept more than one
178
200
  `server-git`, or your own, and every agent in that host gains context,
179
201
  memory, and identity discovery without anything else changing.
180
202
 
203
+ ### Your identifiers
204
+
205
+ The AI Catalog names each entry `urn:air:{domain}:{namespace}:{name}`, and
206
+ the domain is yours. It comes from your project's `.fafa`: run
207
+ `npx faf-cli card init` (or `--domain example.com`), and it writes
208
+ `agent.id: urn:air:example.com:agent:<short-name>`. Your catalog then publishes
209
+ `urn:air:example.com:context:<short-name>` and so on, with no code to change.
210
+ This repo's own `.fafa` says `faf.one`; that's our example, and yours says yours.
211
+
212
+ With no `.fafa`, no domain is invented: the served catalog uses the host it's
213
+ served from (`localhost` when you run it locally), and the static
214
+ `.well-known/ai-catalog.json` uses plain `<short-name>:<namespace>` IDs.
215
+
181
216
  ## Why
182
217
 
183
218
  `AGENTS.md` is the de-facto standard for telling a coding agent how to work in a
@@ -190,9 +225,10 @@ grows its own shape.
190
225
 
191
226
  1. **Server Card `_meta`** ([SEP‑2127](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127)) —
192
227
  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`.
228
+ resource and at `GET /mcp/server-card` (the 1.x `/.well-known/mcp/server-card`
229
+ still answers).
230
+ 2. **`ai-catalog.json`** — the Server Card plus sibling entries keyed by media
231
+ type, at `GET /.well-known/ai-catalog.json`.
196
232
 
197
233
  The context concern points at `AGENTS.md` (`text/markdown`). Memory and identity
198
234
  have no de‑facto standard yet, so the examples here use
@@ -235,11 +271,15 @@ over both transports:
235
271
  4. **Discovery** — `list_context_sources()`, then the same server over stateless
236
272
  HTTP with its `.well-known` routes and `GET /card`.
237
273
 
238
- 140 tests on Linux, macOS, and Windows, coverage‑gated in CI. Two spawn a real
274
+ 209 tests on Linux, macOS, and Windows, coverage‑gated in CI.
275
+ `npm run wjttc` runs the WJTTC certification suite (seven tiers, from protocol
276
+ and Server Card conformance to stdio/HTTP parity and the shipped package). Two spawn a real
239
277
  child process and check a remembered fact survives the restart — one against
240
278
  an existing `project.fafm`, one starting from a project that has never had
241
279
  one; another checks the stdio and HTTP tool surfaces match, and another checks
242
280
  every tool's title and behaviour hints against what it actually does.
281
+ `src/conformance/discovery.ts` checks any server's Server Card, AI Catalog and
282
+ transport security against the specs, one MUST or SHOULD at a time.
243
283
 
244
284
  ## Layout
245
285
 
@@ -252,8 +292,11 @@ every tool's title and behaviour hints against what it actually does.
252
292
  | `src/render-card.ts` | the card — identity + `AGENTS.md` + memory + discovery, as one HTML page |
253
293
  | `src/memory.ts` | file‑backed `remember` / `recall` / `forget` |
254
294
  | `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 |
295
+ | `src/server-card.ts` | the MCP Server Card (SEP-2127, schema v1) |
296
+ | `src/catalog-gen.ts` | writes `ai-catalog.json`: the Server Card + its sibling entries |
256
297
  | `src/transport/http.ts` | the stateless Streamable HTTP app (Hono) |
298
+ | `src/transport/guard.ts` | Origin and Host checks (DNS-rebinding protection) |
299
+ | `src/conformance/discovery.ts` | a portable Server Card / AI Catalog / transport checker |
257
300
  | `src/bin.ts` | the entry point — `stdio` · `--http` · `card` · `--help` · `--version` |
258
301
 
259
302
  ## 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.6.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) --portrait --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
@@ -10,7 +10,7 @@
10
10
  * writes context-card.html and opens it. Piped
11
11
  * or redirected: HTML to stdout ( > card.html ).
12
12
  * --theme light|dark · --accent #hex
13
- * --expanded (all sections open) · --stdout
13
+ * --expanded (all sections open) · --portrait · --stdout
14
14
  * mcp-context-card --help → usage
15
15
  * mcp-context-card --version → version
16
16
  *
@@ -34,18 +34,34 @@ USAGE
34
34
  mcp-context-card card this dir's context card — opens it in your browser
35
35
  at a terminal; HTML to stdout when piped ( > f.html )
36
36
  --theme light|dark --accent #hex
37
- --expanded (all sections open) --stdout
37
+ --expanded (all sections open) --portrait --stdout
38
38
  mcp-context-card --help this text
39
39
  mcp-context-card --version print version
40
40
 
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.
@@ -112,6 +128,7 @@ if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
112
128
  theme: theme === "light" || theme === "dark" ? theme : "auto",
113
129
  accent: safeAccent(flagValue(argv, "--accent")),
114
130
  expanded: argv.includes("--expanded"),
131
+ layout: argv.includes("--portrait") ? "portrait" : "landscape",
115
132
  });
116
133
  // Piped / redirected (or --stdout) → raw HTML on stdout, unchanged.
117
134
  // A bare run at a terminal → the HTML is noise; write a file and open it.
@@ -131,9 +148,17 @@ if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
131
148
  else if (mode === "http") {
132
149
  const { httpApp } = await import("./transport/http.js");
133
150
  const { serve: serveHttp } = await import("@hono/node-server");
134
- serveHttp({ fetch: httpApp(root).fetch, port });
151
+ const { isLoopbackBind } = await import("./transport/guard.js");
152
+ const host = bindHost();
153
+ const local = isLoopbackBind(host);
154
+ // The startup line prints once the socket is listening, so anything waiting
155
+ // for it (a test, a supervisor) can connect straight away.
156
+ serveHttp({ fetch: httpApp(root, { exposure: local ? "local" : "exposed" }).fetch, port, hostname: host }, () =>
135
157
  // stderr, not stdout — stdout is the MCP wire in stdio mode.
136
- console.error(`${NAME} · http · :${port} (POST /mcp · GET /card · GET /.well-known/*)`);
158
+ console.error(`${NAME} · http · ${host.includes(":") ? `[${host}]` : host}:${port} (POST /mcp · GET /card · GET /.well-known/*)` +
159
+ (local
160
+ ? " · local only (HOST=0.0.0.0 to expose)"
161
+ : " · EXPOSED beyond this machine, no authentication: put it behind your own")));
137
162
  }
138
163
  else {
139
164
  // 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
  }