mcp-context-card 1.1.1 → 1.3.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.
- package/.well-known/fafa +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +67 -0
- package/README.md +40 -14
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +10 -10
- package/dist/constants.d.ts +7 -1
- package/dist/constants.js +7 -1
- package/dist/md.js +63 -1
- package/dist/open.d.ts +1 -0
- package/dist/open.js +17 -0
- package/dist/render-card.d.ts +21 -0
- package/dist/render-card.js +67 -6
- package/dist/server.d.ts +17 -8
- package/dist/server.js +170 -31
- package/docs/MECHANISMS.md +4 -2
- package/docs/WIRING.md +11 -5
- package/docs/card-dark.html +2 -2
- package/docs/card-light.html +2 -2
- package/docs/card.html +2 -2
- package/docs/img/card-dark.png +0 -0
- package/docs/img/card-identity.png +0 -0
- package/docs/img/card-light.png +0 -0
- package/docs/index.html +2 -2
- package/package.json +1 -1
- package/project.faf +1 -1
- package/server.json +2 -2
package/.well-known/fafa
CHANGED
|
@@ -9,7 +9,7 @@ agent:
|
|
|
9
9
|
name: "mcp-context-card"
|
|
10
10
|
displayName: "mcp-context-card"
|
|
11
11
|
vendor: "io.github.Wolfe-Jam"
|
|
12
|
-
version: "1.
|
|
12
|
+
version: "1.3.0"
|
|
13
13
|
description: >-
|
|
14
14
|
The essential MCP components for a project's context (AGENTS.md),
|
|
15
15
|
cross-session memory, and identity — a base MCP on its own, or a
|
package/AGENTS.md
CHANGED
|
@@ -45,7 +45,7 @@ code's shape moved without `project.faf`) on Linux.
|
|
|
45
45
|
|
|
46
46
|
| Path | What |
|
|
47
47
|
|---|---|
|
|
48
|
-
| `src/server.ts` | the MCP server — the
|
|
48
|
+
| `src/server.ts` | the MCP server — the ten tools + the Server Card and card resources |
|
|
49
49
|
| `src/agents-md.ts` | reads and section-splits this file |
|
|
50
50
|
| `src/author.ts` | `author_agents_md` — BETTER via `agents-md-facts`, BEST when `project.faf` exists |
|
|
51
51
|
| `src/md.ts` | a minimal dependency-free Markdown → HTML renderer |
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,73 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project. Adheres to [Semantic Versioning](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 1.3.0
|
|
6
|
+
|
|
7
|
+
Your project, with nothing to configure.
|
|
8
|
+
|
|
9
|
+
- **Finds your project from the host.** With no `MCP_CONTEXT_CARD_ROOT`, a
|
|
10
|
+
local (stdio) server asks the host which project you're in, through MCP
|
|
11
|
+
roots (goose sends its session's working directory), and follows it when
|
|
12
|
+
it changes. It falls back to the directory it was started in if that has
|
|
13
|
+
an `AGENTS.md`, then to its own folder, as before. An explicit
|
|
14
|
+
`MCP_CONTEXT_CARD_ROOT` always wins; HTTP is unchanged.
|
|
15
|
+
- **`list_context_sources`** reports `project: { path, from }`: which
|
|
16
|
+
project the server is reading, and how it found it.
|
|
17
|
+
- **`save_context_card`** opens its reply with what it did, as a plain fact:
|
|
18
|
+
`New file:` (with one line on what the file is) or `Updated:`, and the
|
|
19
|
+
path.
|
|
20
|
+
- **An empty project gets next steps, not dead ends,** on both the HTML and
|
|
21
|
+
the text card: no `AGENTS.md` yet → ask your agent to draft one
|
|
22
|
+
(`author_agents_md`, from the repo's real build and test commands); no
|
|
23
|
+
facts yet → ask it to remember something. The placeholder "MCP context
|
|
24
|
+
card" pill is gone.
|
|
25
|
+
- **A project with no identity file** (no `.well-known/fafa`, no
|
|
26
|
+
`package.json`) is named after its folder on the card, not after this
|
|
27
|
+
server.
|
|
28
|
+
|
|
29
|
+
- **HTML comments in `AGENTS.md` are hidden** on the card, as GitHub hides
|
|
30
|
+
them. `agents-md-facts` marks its managed block with comments, and the
|
|
31
|
+
card showed them as text. Comments inside code stay, as content.
|
|
32
|
+
|
|
33
|
+
No API change. 134 tests, all green.
|
|
34
|
+
|
|
35
|
+
## 1.2.0
|
|
36
|
+
|
|
37
|
+
The card reaches people in any host: inline where the host supports MCP
|
|
38
|
+
Apps, and as text in the chat plus the full card in the browser everywhere
|
|
39
|
+
else.
|
|
40
|
+
|
|
41
|
+
- **MCP App.** `render_context_card` links a `ui://mcp-context-card/card.html`
|
|
42
|
+
resource (`text/html;profile=mcp-app`). Hosts that support MCP Apps render
|
|
43
|
+
the card inline, and the model gets a one-line summary instead of the
|
|
44
|
+
whole HTML page. Hosts without MCP Apps get the full HTML, as before.
|
|
45
|
+
- **New tool: `save_context_card`.** Writes the card to `context-card.html`
|
|
46
|
+
in the project and replies with:
|
|
47
|
+
- the card as Markdown: identity, `AGENTS.md` section headings, memory,
|
|
48
|
+
discovery. About 1.2k characters for this repo, instead of about 17k of
|
|
49
|
+
HTML;
|
|
50
|
+
- a link to the full card, plus its `file://` address in a code block,
|
|
51
|
+
which hosts give a copy button (many won't follow a `file://` link).
|
|
52
|
+
|
|
53
|
+
When the server runs locally (stdio), it also opens the saved card in the
|
|
54
|
+
browser; `open: false` skips that, and over HTTP it never opens anything.
|
|
55
|
+
Same `theme`, `accent` and `expanded` inputs as `render_context_card`.
|
|
56
|
+
Marked `destructiveHint: true`, because it replaces an earlier
|
|
57
|
+
`context-card.html`.
|
|
58
|
+
- **A tl;dr by default.** The Markdown shows five facts, each cut to its
|
|
59
|
+
first sentence exactly as stored (or a word-boundary cut marked … when
|
|
60
|
+
that sentence is long), and counts the rest. It stays under about 3k
|
|
61
|
+
characters however much a project remembers. `detail: "full"` lists
|
|
62
|
+
every fact whole. The saved file always has everything.
|
|
63
|
+
- **Server instructions.** Sent at initialize: when the host can't display
|
|
64
|
+
the card, save it and show the user the Markdown card and its link,
|
|
65
|
+
not paste HTML into the chat.
|
|
66
|
+
- **`list_context_sources`** now lists the card resource under
|
|
67
|
+
`surfaces.mcp`.
|
|
68
|
+
|
|
69
|
+
Ten tools now. The nine existing tools keep their names, inputs and
|
|
70
|
+
behaviour. 117 tests, all green.
|
|
71
|
+
|
|
5
72
|
## 1.1.1
|
|
6
73
|
|
|
7
74
|
Every tool now says what it is and what it does to your project.
|
package/README.md
CHANGED
|
@@ -13,6 +13,13 @@ rendered as one card you can read.
|
|
|
13
13
|
|---|---|
|
|
14
14
|
|  |  |
|
|
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:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npx mcp-context-card card
|
|
21
|
+
```
|
|
22
|
+
|
|
16
23
|
**context** — the project's `AGENTS.md`, served whole or one section at a time.
|
|
17
24
|
|
|
18
25
|

|
|
@@ -52,7 +59,7 @@ say what it is. `mcp-context-card` is those three, done once:
|
|
|
52
59
|
gains context, memory, and identity discovery it didn't have. Nothing to
|
|
53
60
|
migrate; it composes.
|
|
54
61
|
|
|
55
|
-
|
|
62
|
+
Ten tools, two discovery surfaces already in the ecosystem (Server Card
|
|
56
63
|
`_meta`, `ai-catalog.json`), and a rendered [card](#the-card). MIT, on npm.
|
|
57
64
|
|
|
58
65
|
It composes:
|
|
@@ -63,8 +70,9 @@ It composes:
|
|
|
63
70
|
|
|
64
71
|
Vendor-free — context is plain Markdown (`AGENTS.md`); the memory and
|
|
65
72
|
identity formats are swappable examples. It reads and writes only its own
|
|
66
|
-
three files (`AGENTS.md`, `project.fafm`, `.well-known/fafa`)
|
|
67
|
-
file access, no shell,
|
|
73
|
+
three files (`AGENTS.md`, `project.fafm`, `.well-known/fafa`), plus the
|
|
74
|
+
`context-card.html` it saves on request — no general file access, no shell,
|
|
75
|
+
no search.
|
|
68
76
|
|
|
69
77
|
## The card
|
|
70
78
|
|
|
@@ -87,6 +95,18 @@ GET /card # live, on the HTTP transport
|
|
|
87
95
|
GET /card?expand=all&theme=light&accent=%230066cc
|
|
88
96
|
```
|
|
89
97
|
|
|
98
|
+
In a chat, hosts that support [MCP Apps](https://modelcontextprotocol.io/extensions/apps)
|
|
99
|
+
show the card inline when `render_context_card` runs; the model gets a short
|
|
100
|
+
summary instead of the page. Everywhere else, `save_context_card` writes
|
|
101
|
+
`context-card.html` into the project, opens it in your browser when the
|
|
102
|
+
server runs locally, and replies with the card as Markdown (identity, the
|
|
103
|
+
`AGENTS.md` sections, memory, discovery), a link to the full card, and its
|
|
104
|
+
address to copy. The Markdown is a tl;dr by default: five facts, each cut to
|
|
105
|
+
its first sentence as stored, so it stays small however much a project
|
|
106
|
+
remembers. `detail: "full"` lists every fact whole. The server's
|
|
107
|
+
instructions tell the model to show that rather than paste the HTML into
|
|
108
|
+
the chat.
|
|
109
|
+
|
|
90
110
|
Light, dark, or auto; the accent defaults to the AAIF palette and takes any hex.
|
|
91
111
|
This repo's own card, live: [auto](https://wolfe-jam.github.io/mcp-context-card/) ·
|
|
92
112
|
[light](https://wolfe-jam.github.io/mcp-context-card/card-light.html) ·
|
|
@@ -126,22 +146,25 @@ or redirected (`> card.html`, a script, CI) it writes raw HTML to stdout instead
|
|
|
126
146
|
|
|
127
147
|
### Wire it into a host
|
|
128
148
|
|
|
129
|
-
Claude Desktop, Cursor, or any stdio host:
|
|
149
|
+
goose, Claude Desktop, Cursor, or any stdio host:
|
|
130
150
|
|
|
131
151
|
```jsonc
|
|
132
152
|
{
|
|
133
153
|
"mcpServers": {
|
|
134
154
|
"context-card": {
|
|
135
155
|
"command": "npx",
|
|
136
|
-
"args": ["-y", "mcp-context-card"]
|
|
137
|
-
"env": { "MCP_CONTEXT_CARD_ROOT": "/abs/path/to/your/project" }
|
|
156
|
+
"args": ["-y", "mcp-context-card"]
|
|
138
157
|
}
|
|
139
158
|
}
|
|
140
159
|
}
|
|
141
160
|
```
|
|
142
161
|
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
No path to configure: the server asks the host which project you're in (MCP
|
|
163
|
+
roots — goose sends its session's working directory), then falls back to the
|
|
164
|
+
directory it was started in if that has an `AGENTS.md`. `list_context_sources`
|
|
165
|
+
reports which project it picked and how. To pin one project instead, set
|
|
166
|
+
`"env": { "MCP_CONTEXT_CARD_ROOT": "/abs/path/to/your/project" }`; that always
|
|
167
|
+
wins. Identity is optional. Over HTTP instead:
|
|
145
168
|
`PORT=8080 npx mcp-context-card`. Requires Node ≥20.
|
|
146
169
|
|
|
147
170
|
If `command: "npx"` fails to spawn (`spawn npx ENOENT` — seen on Cursor, whose
|
|
@@ -191,15 +214,18 @@ The wire‑level detail is in [docs/MECHANISMS.md](./docs/MECHANISMS.md).
|
|
|
191
214
|
| `forget` | drop or correct a stale fact |
|
|
192
215
|
| `whoami` | this server's name, vendor, version, status, license |
|
|
193
216
|
| `list_context_sources` | what this project publishes, in what media types, via which surface |
|
|
194
|
-
| `render_context_card` | the whole card as one self‑contained HTML page (also `GET /card`) |
|
|
217
|
+
| `render_context_card` | the whole card as one self‑contained HTML page (also `GET /card`); hosts that support MCP Apps show it inline |
|
|
218
|
+
| `save_context_card` | write the card to `context-card.html` in the project and open it in your browser; returns the card as Markdown plus a link to the full version |
|
|
195
219
|
|
|
196
|
-
Seven tools only read. `remember` and `forget` write the memory file,
|
|
197
|
-
|
|
220
|
+
Seven tools only read. `remember` and `forget` write the memory file, and
|
|
221
|
+
`save_context_card` writes `context-card.html`, so those three are marked
|
|
222
|
+
destructive and a host can ask before running them. Every tool carries a
|
|
198
223
|
title and MCP tool annotations.
|
|
199
224
|
|
|
200
225
|
## The demo
|
|
201
226
|
|
|
202
|
-
`npm run demo`
|
|
227
|
+
`npm run demo` walks through context, memory, identity and discovery, live,
|
|
228
|
+
over both transports:
|
|
203
229
|
|
|
204
230
|
1. **Context** — list the `AGENTS.md` sections, then pull just `## Test`.
|
|
205
231
|
2. **Memory** — `remember()` a fact, stop the server process, start a new one,
|
|
@@ -209,7 +235,7 @@ title and MCP tool annotations.
|
|
|
209
235
|
4. **Discovery** — `list_context_sources()`, then the same server over stateless
|
|
210
236
|
HTTP with its `.well-known` routes and `GET /card`.
|
|
211
237
|
|
|
212
|
-
|
|
238
|
+
134 tests on Linux, macOS, and Windows, coverage‑gated in CI. Two spawn a real
|
|
213
239
|
child process and check a remembered fact survives the restart — one against
|
|
214
240
|
an existing `project.fafm`, one starting from a project that has never had
|
|
215
241
|
one; another checks the stdio and HTTP tool surfaces match, and another checks
|
|
@@ -219,7 +245,7 @@ every tool's title and behaviour hints against what it actually does.
|
|
|
219
245
|
|
|
220
246
|
| Path | What |
|
|
221
247
|
|---|---|
|
|
222
|
-
| `src/server.ts` | the
|
|
248
|
+
| `src/server.ts` | the ten tools + the Server Card and card resources |
|
|
223
249
|
| `src/agents-md.ts` | reads and section‑splits `AGENTS.md` |
|
|
224
250
|
| `src/author.ts` | `author_agents_md` — BETTER via [`agents-md-facts`](https://github.com/Wolfe-Jam/agents-md-facts), BEST when `project.faf` exists |
|
|
225
251
|
| `src/md.ts` | a minimal dependency‑free Markdown → HTML renderer |
|
package/dist/bin.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ 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.
|
|
11
|
+
export declare const HELP = "mcp-context-card 1.3.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";
|
|
12
12
|
/**
|
|
13
13
|
* Decide how to launch, from argv + env. Pure — so the mode matrix is unit
|
|
14
14
|
* tested without spawning a process.
|
package/dist/bin.js
CHANGED
|
@@ -121,17 +121,10 @@ if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
|
|
|
121
121
|
else {
|
|
122
122
|
const { writeFileSync } = await import("node:fs");
|
|
123
123
|
const { join } = await import("node:path");
|
|
124
|
-
const {
|
|
124
|
+
const { openInBrowser } = await import("./open.js");
|
|
125
125
|
const out = join(process.cwd(), "context-card.html");
|
|
126
126
|
writeFileSync(out, html);
|
|
127
|
-
|
|
128
|
-
? ["open", [out]]
|
|
129
|
-
: process.platform === "win32"
|
|
130
|
-
? ["cmd", ["/c", "start", "", out]]
|
|
131
|
-
: ["xdg-open", [out]];
|
|
132
|
-
spawn(opener[0], opener[1], { stdio: "ignore", detached: true })
|
|
133
|
-
.on("error", () => { })
|
|
134
|
-
.unref();
|
|
127
|
+
openInBrowser(out);
|
|
135
128
|
process.stderr.write(`${NAME} · wrote ${out} — opening in your browser (--stdout for raw HTML)\n`);
|
|
136
129
|
}
|
|
137
130
|
}
|
|
@@ -146,6 +139,13 @@ if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
|
|
|
146
139
|
// stderr so it never touches the JSON-RPC wire on stdout; a bare run at a
|
|
147
140
|
// terminal otherwise looks hung.
|
|
148
141
|
console.error(`${NAME} · stdio · waiting for an MCP host on stdin (--help for usage · Ctrl-C to exit)`);
|
|
149
|
-
|
|
142
|
+
// stdio = a host on this machine, so save_context_card can open the card.
|
|
143
|
+
// With no MCP_CONTEXT_CARD_ROOT, find the user's project: the host's MCP
|
|
144
|
+
// roots, then the start directory. So a host needs no config to show yours.
|
|
145
|
+
const { openInBrowser } = await import("./open.js");
|
|
146
|
+
await serve(new StdioServerTransport(), root, {
|
|
147
|
+
openFile: openInBrowser,
|
|
148
|
+
detectRoot: !process.env.MCP_CONTEXT_CARD_ROOT,
|
|
149
|
+
});
|
|
150
150
|
}
|
|
151
151
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Server identity constants, in their own module so any file can import
|
|
2
2
|
* them without pulling in the whole server. */
|
|
3
3
|
export declare const NAME = "mcp-context-card";
|
|
4
|
-
export declare const VERSION = "1.
|
|
4
|
+
export declare const VERSION = "1.3.0";
|
|
5
5
|
export declare const SERVER_CARD_URI = "mcp-context-card://server-card";
|
|
6
|
+
/** MCP Apps (io.modelcontextprotocol/ui): the card as an inline UI resource.
|
|
7
|
+
* A host that supports MCP Apps fetches this resource and renders it in a
|
|
8
|
+
* sandboxed iframe next to the conversation. */
|
|
9
|
+
export declare const CARD_UI_URI = "ui://mcp-context-card/card.html";
|
|
10
|
+
export declare const MCP_APP_MIME = "text/html;profile=mcp-app";
|
|
11
|
+
export declare const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
package/dist/constants.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Server identity constants, in their own module so any file can import
|
|
2
2
|
* them without pulling in the whole server. */
|
|
3
3
|
export const NAME = "mcp-context-card";
|
|
4
|
-
export const VERSION = "1.
|
|
4
|
+
export const VERSION = "1.3.0";
|
|
5
5
|
export const SERVER_CARD_URI = "mcp-context-card://server-card";
|
|
6
|
+
/** MCP Apps (io.modelcontextprotocol/ui): the card as an inline UI resource.
|
|
7
|
+
* A host that supports MCP Apps fetches this resource and renders it in a
|
|
8
|
+
* sandboxed iframe next to the conversation. */
|
|
9
|
+
export const CARD_UI_URI = "ui://mcp-context-card/card.html";
|
|
10
|
+
export const MCP_APP_MIME = "text/html;profile=mcp-app";
|
|
11
|
+
export const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
package/dist/md.js
CHANGED
|
@@ -58,8 +58,70 @@ const TABLE_SEP = /^\s*\|?[\s:|-]*-[\s:|-]*\|?\s*$/;
|
|
|
58
58
|
function tableRow(line) {
|
|
59
59
|
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Drop HTML comments, as GitHub does when it renders Markdown: tools such as
|
|
63
|
+
* agents-md-facts mark their managed blocks with them. A comment may span
|
|
64
|
+
* lines. Inside a fenced block or an inline `code` span it's content, so kept.
|
|
65
|
+
*/
|
|
66
|
+
function stripComments(src) {
|
|
67
|
+
const out = [];
|
|
68
|
+
let inFence = null;
|
|
69
|
+
let inComment = false;
|
|
70
|
+
for (const line of src.split("\n")) {
|
|
71
|
+
if (inFence) {
|
|
72
|
+
out.push(line);
|
|
73
|
+
if (inFence.test(line))
|
|
74
|
+
inFence = null;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const fence = !inComment && line.match(FENCE);
|
|
78
|
+
if (fence) {
|
|
79
|
+
inFence = fence[2][0] === "`" ? /^\s*```+\s*$/ : /^\s*~~~+\s*$/;
|
|
80
|
+
out.push(line);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// outside code: split on inline code spans, strip comments only between them
|
|
84
|
+
let kept = "";
|
|
85
|
+
const parts = line.split(/(`+[^`]*`+)/);
|
|
86
|
+
for (let k = 0; k < parts.length; k++) {
|
|
87
|
+
let part = parts[k];
|
|
88
|
+
if (k % 2 === 1 && !inComment) {
|
|
89
|
+
kept += part;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
while (part.length) {
|
|
93
|
+
if (inComment) {
|
|
94
|
+
const end = part.indexOf("-->");
|
|
95
|
+
if (end < 0) {
|
|
96
|
+
part = "";
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
part = part.slice(end + 3);
|
|
100
|
+
inComment = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const start = part.indexOf("<!--");
|
|
105
|
+
if (start < 0) {
|
|
106
|
+
kept += part;
|
|
107
|
+
part = "";
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
kept += part.slice(0, start);
|
|
111
|
+
part = part.slice(start + 4);
|
|
112
|
+
inComment = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// a line that held only a comment disappears, rather than leaving a blank gap
|
|
118
|
+
if (kept.trim() || !line.includes("<!--") && !line.includes("-->"))
|
|
119
|
+
out.push(kept);
|
|
120
|
+
}
|
|
121
|
+
return out.join("\n");
|
|
122
|
+
}
|
|
61
123
|
export function renderMarkdown(src) {
|
|
62
|
-
const lines = src.replace(/\r\n/g, "\n").split("\n");
|
|
124
|
+
const lines = stripComments(src.replace(/\r\n/g, "\n")).split("\n");
|
|
63
125
|
const out = [];
|
|
64
126
|
let i = 0;
|
|
65
127
|
const flushPara = (buf) => {
|
package/dist/open.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function openInBrowser(path: string): void;
|
package/dist/open.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a local file in the default browser — what `mcp-context-card card`
|
|
3
|
+
* does at a terminal, and what save_context_card does when the server runs
|
|
4
|
+
* locally over stdio. Fire and forget: no display (SSH, CI) just means
|
|
5
|
+
* nothing opens, never an error.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
export function openInBrowser(path) {
|
|
9
|
+
const [cmd, args] = process.platform === "darwin"
|
|
10
|
+
? ["open", [path]]
|
|
11
|
+
: process.platform === "win32"
|
|
12
|
+
? ["cmd", ["/c", "start", "", path]]
|
|
13
|
+
: ["xdg-open", [path]];
|
|
14
|
+
spawn(cmd, args, { stdio: "ignore", detached: true })
|
|
15
|
+
.on("error", () => { })
|
|
16
|
+
.unref();
|
|
17
|
+
}
|
package/dist/render-card.d.ts
CHANGED
|
@@ -14,3 +14,24 @@ export interface CardOptions {
|
|
|
14
14
|
export declare const AAIF_ACCENT = "#FF702D";
|
|
15
15
|
export declare function safeAccent(a?: string): string;
|
|
16
16
|
export declare function renderCard(root: string, opts?: CardOptions): string;
|
|
17
|
+
/** tl;dr: the first few facts, each cut to about a line. `full`: every fact, whole. */
|
|
18
|
+
export type Detail = "tldr" | "full";
|
|
19
|
+
/**
|
|
20
|
+
* Shorten a fact for the tl;dr without rewording it. The whole first sentence
|
|
21
|
+
* when it fits in `max` (a stored sentence, verbatim, so a model has no ragged
|
|
22
|
+
* end to "tidy"); otherwise a word-boundary cut marked with …. A dot inside a
|
|
23
|
+
* word (AGENTS.md, v1.2) is not a sentence end: it must be followed by space.
|
|
24
|
+
*/
|
|
25
|
+
export declare function clip(s: string, max: number): string;
|
|
26
|
+
/**
|
|
27
|
+
* The same card as Markdown, for chats that can't display HTML: identity,
|
|
28
|
+
* the AGENTS.md section headings (bodies stay in the full card), memory,
|
|
29
|
+
* and the discovery table. Same sources as renderCard.
|
|
30
|
+
*
|
|
31
|
+
* The default tl;dr stays small however much memory a project holds: five
|
|
32
|
+
* facts, each cut short, and a count of the rest. `detail: "full"` lists
|
|
33
|
+
* every fact whole. The saved HTML card always has everything.
|
|
34
|
+
*/
|
|
35
|
+
export declare function renderCardText(root: string, opts?: {
|
|
36
|
+
detail?: Detail;
|
|
37
|
+
}): string;
|
package/dist/render-card.js
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
* is the expand-all / print helper (TOGGLE_SCRIPT) — a progressive enhancement;
|
|
11
11
|
* every section still opens on its own without it. Renders anywhere.
|
|
12
12
|
*/
|
|
13
|
-
import { join } from "node:path";
|
|
13
|
+
import { basename, join, resolve } from "node:path";
|
|
14
14
|
import { parseAgentsMd } from "./agents-md.js";
|
|
15
15
|
import { parseFafm } from "./memory.js";
|
|
16
16
|
import { resolveIdentity, serverCardMeta, META_NS } from "./identity.js";
|
|
17
|
-
import {
|
|
17
|
+
import { SERVER_CARD_URI } from "./constants.js";
|
|
18
18
|
import { escapeHtml, renderInline, renderMarkdown, slug } from "./md.js";
|
|
19
19
|
/** AAIF brand orange (aaif.io). The default accent. */
|
|
20
20
|
export const AAIF_ACCENT = "#FF702D";
|
|
@@ -121,7 +121,7 @@ export function renderCard(root, opts = {}) {
|
|
|
121
121
|
const mem = parseFafm(join(root, "project.fafm"));
|
|
122
122
|
const id = resolveIdentity(root);
|
|
123
123
|
const meta = serverCardMeta();
|
|
124
|
-
const name = id?.displayName ?? id?.name ??
|
|
124
|
+
const name = id?.displayName ?? id?.name ?? basename(resolve(root));
|
|
125
125
|
const pills = [
|
|
126
126
|
id?.vendor && id.vendor !== id.status && `<span class="pill">${escapeHtml(id.vendor)}</span>`,
|
|
127
127
|
id?.agentVersion && `<span class="pill">v${escapeHtml(id.agentVersion)}</span>`,
|
|
@@ -152,7 +152,7 @@ export function renderCard(root, opts = {}) {
|
|
|
152
152
|
: ""}</div>
|
|
153
153
|
${preamble ? `<div class="ctx-preamble md">${renderMarkdown(preamble)}</div>` : ""}
|
|
154
154
|
<div class="ctx-body">${sections}</div>`
|
|
155
|
-
: `<p class="none">No AGENTS.md
|
|
155
|
+
: `<p class="none">No AGENTS.md yet. Ask your agent to draft one: <code>author_agents_md</code> builds it from this repo's real build and test commands, nothing invented.</p>`;
|
|
156
156
|
// MEMORY
|
|
157
157
|
const memoryBody = mem.facts.length
|
|
158
158
|
? mem.facts
|
|
@@ -164,7 +164,7 @@ export function renderCard(root, opts = {}) {
|
|
|
164
164
|
return `<div class="fact"><p>${renderInline(f.text)}</p><div class="meta">${tags}<span class="dot${verified ? "" : " pending"}" title="${verified ? "verified" : f.verification_status ?? "unverified"}"></span></div></div>`;
|
|
165
165
|
})
|
|
166
166
|
.join("")
|
|
167
|
-
: `<p class="none">No facts yet.</p>`;
|
|
167
|
+
: `<p class="none">No facts yet. Ask your agent to remember something, and it lands here.</p>`;
|
|
168
168
|
// DISCOVERY
|
|
169
169
|
const rows = Object.entries(meta)
|
|
170
170
|
.map(([k, v]) => {
|
|
@@ -184,7 +184,7 @@ export function renderCard(root, opts = {}) {
|
|
|
184
184
|
<main class="card">
|
|
185
185
|
<div class="top">
|
|
186
186
|
<h1>${escapeHtml(name)}</h1>
|
|
187
|
-
|
|
187
|
+
${pills ? `<div class="pills">${pills}</div>` : ""}
|
|
188
188
|
</div>
|
|
189
189
|
<section>
|
|
190
190
|
<p class="label">Context — AGENTS.md</p>
|
|
@@ -236,3 +236,64 @@ const TOGGLE_SCRIPT = `<script>
|
|
|
236
236
|
sync();
|
|
237
237
|
})();
|
|
238
238
|
</script>`;
|
|
239
|
+
const TLDR_FACTS = 5;
|
|
240
|
+
const TLDR_CHARS = 160;
|
|
241
|
+
/**
|
|
242
|
+
* Shorten a fact for the tl;dr without rewording it. The whole first sentence
|
|
243
|
+
* when it fits in `max` (a stored sentence, verbatim, so a model has no ragged
|
|
244
|
+
* end to "tidy"); otherwise a word-boundary cut marked with …. A dot inside a
|
|
245
|
+
* word (AGENTS.md, v1.2) is not a sentence end: it must be followed by space.
|
|
246
|
+
*/
|
|
247
|
+
export function clip(s, max) {
|
|
248
|
+
if (s.length <= max)
|
|
249
|
+
return s;
|
|
250
|
+
const first = /^(.+?[.!?])(?=\s)/.exec(s)?.[1];
|
|
251
|
+
if (first && first.length <= max)
|
|
252
|
+
return first;
|
|
253
|
+
const cut = s.slice(0, max);
|
|
254
|
+
const space = cut.lastIndexOf(" ");
|
|
255
|
+
return `${(space > max / 2 ? cut.slice(0, space) : cut).replace(/[\s,;:.—-]+$/, "")}…`;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The same card as Markdown, for chats that can't display HTML: identity,
|
|
259
|
+
* the AGENTS.md section headings (bodies stay in the full card), memory,
|
|
260
|
+
* and the discovery table. Same sources as renderCard.
|
|
261
|
+
*
|
|
262
|
+
* The default tl;dr stays small however much memory a project holds: five
|
|
263
|
+
* facts, each cut short, and a count of the rest. `detail: "full"` lists
|
|
264
|
+
* every fact whole. The saved HTML card always has everything.
|
|
265
|
+
*/
|
|
266
|
+
export function renderCardText(root, opts = {}) {
|
|
267
|
+
const agents = parseAgentsMd(join(root, "AGENTS.md"));
|
|
268
|
+
const mem = parseFafm(join(root, "project.fafm"));
|
|
269
|
+
const id = resolveIdentity(root);
|
|
270
|
+
const meta = serverCardMeta();
|
|
271
|
+
const name = id?.displayName ?? id?.name ?? basename(resolve(root));
|
|
272
|
+
const plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
|
|
273
|
+
const out = [`### ${name} — context card`];
|
|
274
|
+
const pills = [
|
|
275
|
+
id?.vendor && id.vendor !== id.status ? id.vendor : null,
|
|
276
|
+
id?.agentVersion ? `v${id.agentVersion}` : null,
|
|
277
|
+
id?.status,
|
|
278
|
+
id?.license,
|
|
279
|
+
].filter(Boolean);
|
|
280
|
+
if (pills.length)
|
|
281
|
+
out.push(pills.join(" · "));
|
|
282
|
+
if (id?.description)
|
|
283
|
+
out.push(id.description);
|
|
284
|
+
const sections = agents?.sections.filter((s) => s.level > 1) ?? [];
|
|
285
|
+
out.push(agents
|
|
286
|
+
? `**Context — AGENTS.md** · ${plural(sections.length, "section")}\n${sections.map((s) => s.heading).join(" · ")}`
|
|
287
|
+
: "**Context — AGENTS.md** · none yet. `author_agents_md` drafts one from this repo's real build and test commands, nothing invented.");
|
|
288
|
+
const full = opts.detail === "full";
|
|
289
|
+
const shown = full ? mem.facts : mem.facts.slice(0, TLDR_FACTS);
|
|
290
|
+
const rest = mem.facts.length - shown.length;
|
|
291
|
+
out.push(mem.facts.length
|
|
292
|
+
? `**Memory** · ${plural(mem.facts.length, "fact")}\n${shown
|
|
293
|
+
.map((f) => `- ${full ? f.text : clip(f.text, TLDR_CHARS)}${f.verification_status === "verified" ? " ✓" : ""}`)
|
|
294
|
+
.join("\n")}${rest ? `\n\n…and ${plural(rest, "more fact")}, in the full card` : ""}`
|
|
295
|
+
: "**Memory** · no facts yet. Ask your agent to remember something, and it lands here.");
|
|
296
|
+
const rows = Object.entries(meta).map(([k, v]) => `| ${k.slice(META_NS.length + 1)} | \`${v.source}\` | \`${v.mediaType}\` |`);
|
|
297
|
+
out.push(["**Discovery**", "| concern | source | media type |", "|---|---|---|", ...rows].join("\n"));
|
|
298
|
+
return out.join("\n\n");
|
|
299
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* context — read_agents_md · list_agents_md_sections · author_agents_md (this project's AGENTS.md)
|
|
6
6
|
* memory — remember · recall · forget (a .fafm file)
|
|
7
7
|
* identity — whoami (this server's .fafa)
|
|
8
|
-
* discovery — list_context_sources · render_context_card
|
|
8
|
+
* discovery — list_context_sources · render_context_card · save_context_card (what's published, and how)
|
|
9
9
|
*
|
|
10
10
|
* ...exposed through the two mechanisms already in the ecosystem:
|
|
11
11
|
*
|
|
@@ -46,11 +46,20 @@ export declare function serverCard(): {
|
|
|
46
46
|
};
|
|
47
47
|
};
|
|
48
48
|
};
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
/** Sent to every client at initialize. Hosts that support MCP Apps show the
|
|
50
|
+
* card inline; for the rest, this steers the model to save the card as a
|
|
51
|
+
* file instead of pasting a whole HTML page into the chat. */
|
|
52
|
+
export declare const INSTRUCTIONS: string;
|
|
53
|
+
export interface ServerOptions {
|
|
54
|
+
/** Open a saved file for the person. Set only for a local (stdio) server;
|
|
55
|
+
* over HTTP the browser would open on the server, so it stays unset. */
|
|
56
|
+
openFile?: (path: string) => void;
|
|
57
|
+
/** No root was configured: find the user's project instead — the client's
|
|
58
|
+
* MCP roots, then `cwd` if it holds an AGENTS.md, then `root`. Local only. */
|
|
59
|
+
detectRoot?: boolean;
|
|
60
|
+
/** The directory the server was started in (default: process.cwd()). */
|
|
61
|
+
cwd?: string;
|
|
62
|
+
}
|
|
63
|
+
export declare function createServer(root?: string, opts?: ServerOptions): Server;
|
|
55
64
|
/** Connect a server instance to a transport (stdio or http). */
|
|
56
|
-
export declare function serve(transport: Transport, root?: string): Promise<Server>;
|
|
65
|
+
export declare function serve(transport: Transport, root?: string, opts?: ServerOptions): Promise<Server>;
|
package/dist/server.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* context — read_agents_md · list_agents_md_sections · author_agents_md (this project's AGENTS.md)
|
|
6
6
|
* memory — remember · recall · forget (a .fafm file)
|
|
7
7
|
* identity — whoami (this server's .fafa)
|
|
8
|
-
* discovery — list_context_sources · render_context_card
|
|
8
|
+
* discovery — list_context_sources · render_context_card · save_context_card (what's published, and how)
|
|
9
9
|
*
|
|
10
10
|
* ...exposed through the two mechanisms already in the ecosystem:
|
|
11
11
|
*
|
|
@@ -16,16 +16,17 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
-
import { CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
20
|
-
import {
|
|
19
|
+
import { CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, RootsListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
20
|
+
import { existsSync, statSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { basename, dirname, join } from "node:path";
|
|
21
22
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
22
23
|
import { findSection, parseAgentsMd } from "./agents-md.js";
|
|
23
24
|
import { authorAgentsMd } from "./author.js";
|
|
24
25
|
import { forget, parseFafm, recall, remember } from "./memory.js";
|
|
25
|
-
import { identity, serverCardMeta, whoami } from "./identity.js";
|
|
26
|
-
import { renderCard, safeAccent } from "./render-card.js";
|
|
26
|
+
import { identity, resolveIdentity, serverCardMeta, whoami } from "./identity.js";
|
|
27
|
+
import { renderCard, renderCardText, safeAccent } from "./render-card.js";
|
|
27
28
|
export { NAME, VERSION, SERVER_CARD_URI } from "./constants.js";
|
|
28
|
-
import { NAME, VERSION, SERVER_CARD_URI } from "./constants.js";
|
|
29
|
+
import { NAME, VERSION, SERVER_CARD_URI, CARD_UI_URI, MCP_APP_MIME, UI_EXTENSION } from "./constants.js";
|
|
29
30
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
30
31
|
/** Default package root — `dist/` at runtime, `src/` under tsx. Both are one up. */
|
|
31
32
|
export const ROOT = join(here, "..");
|
|
@@ -39,16 +40,95 @@ export function serverCard() {
|
|
|
39
40
|
return { name: NAME, version: VERSION, _meta: serverCardMeta() };
|
|
40
41
|
}
|
|
41
42
|
const text = (s) => ({ content: [{ type: "text", text: s }] });
|
|
43
|
+
/** Sent to every client at initialize. Hosts that support MCP Apps show the
|
|
44
|
+
* card inline; for the rest, this steers the model to save the card as a
|
|
45
|
+
* file instead of pasting a whole HTML page into the chat. */
|
|
46
|
+
export const INSTRUCTIONS = "This server publishes a project's context (AGENTS.md), memory (project.fafm) and identity (.well-known/fafa). " +
|
|
47
|
+
"Read them with read_agents_md, recall and whoami; list_context_sources says what is published and where. " +
|
|
48
|
+
"When the user wants to see the context card: hosts that support MCP Apps display it inline from render_context_card. " +
|
|
49
|
+
"Otherwise, don't paste the card's HTML into the conversation. Call save_context_card: it writes context-card.html " +
|
|
50
|
+
"into the project, opens it in the user's browser when this server runs locally, and returns the card as Markdown " +
|
|
51
|
+
"with a link to the saved file. Show the user that Markdown as returned, including the link, so they see the card " +
|
|
52
|
+
"in the chat and can open the full version in a browser.";
|
|
53
|
+
/** Theme / accent / expanded from tool arguments, shared by render and save. */
|
|
54
|
+
function cardOptions(args) {
|
|
55
|
+
const theme = args.theme;
|
|
56
|
+
return {
|
|
57
|
+
theme: (["light", "dark", "auto"].includes(theme) ? theme : "auto"),
|
|
58
|
+
accent: safeAccent(args.accent),
|
|
59
|
+
expanded: args.expanded === true || args.expanded === "true",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const CARD_ARGS = {
|
|
63
|
+
theme: { type: "string", enum: ["light", "dark", "auto"], description: "default: auto" },
|
|
64
|
+
accent: { type: "string", description: "CSS hex colour, e.g. #FF702D (default: the AAIF palette)" },
|
|
65
|
+
expanded: { type: "boolean", description: "render every AGENTS.md section open (default: collapsed)" },
|
|
66
|
+
};
|
|
42
67
|
/**
|
|
43
68
|
* @param root directory holding `AGENTS.md`, `project.fafm`, `.well-known/`.
|
|
44
69
|
* Defaults to the package root; a deploy points `MCP_CONTEXT_CARD_ROOT`
|
|
45
70
|
* at a real project, a test points it at a fixture.
|
|
46
71
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
72
|
+
/** True when the connected client declared MCP Apps support
|
|
73
|
+
* (capabilities.extensions["io.modelcontextprotocol/ui"].mimeTypes). */
|
|
74
|
+
function hostRendersApps(server) {
|
|
75
|
+
const ext = server.getClientCapabilities()?.extensions?.[UI_EXTENSION];
|
|
76
|
+
return Array.isArray(ext?.mimeTypes) && ext.mimeTypes.includes(MCP_APP_MIME);
|
|
77
|
+
}
|
|
78
|
+
export function createServer(root = ROOT, opts = {}) {
|
|
79
|
+
const server = new Server({ name: NAME, version: VERSION }, { capabilities: { tools: {}, resources: {} }, instructions: INSTRUCTIONS });
|
|
80
|
+
// ── Which project? ────────────────────────────────────────────────────
|
|
81
|
+
// Resolved per call, not at startup: a client's roots are only known once
|
|
82
|
+
// it has connected, and they can change (goose sends its session's working
|
|
83
|
+
// directory, and notifies when it moves).
|
|
84
|
+
let found;
|
|
85
|
+
server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
|
|
86
|
+
found = undefined;
|
|
87
|
+
});
|
|
88
|
+
const isDir = (p) => {
|
|
89
|
+
try {
|
|
90
|
+
return statSync(p).isDirectory();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
async function fromClientRoots() {
|
|
97
|
+
if (!server.getClientCapabilities()?.roots)
|
|
98
|
+
return undefined;
|
|
99
|
+
try {
|
|
100
|
+
const { roots } = await server.listRoots(undefined, { timeout: 5000 });
|
|
101
|
+
for (const r of roots) {
|
|
102
|
+
if (!r.uri.startsWith("file:"))
|
|
103
|
+
continue;
|
|
104
|
+
const dir = fileURLToPath(r.uri);
|
|
105
|
+
if (isDir(dir))
|
|
106
|
+
return dir;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// a client that declares roots but doesn't answer: fall through
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
async function project() {
|
|
115
|
+
if (!opts.detectRoot)
|
|
116
|
+
return { root, from: root === ROOT ? "package" : "configured" };
|
|
117
|
+
if (found)
|
|
118
|
+
return found;
|
|
119
|
+
const fromRoots = await fromClientRoots();
|
|
120
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
121
|
+
found = fromRoots
|
|
122
|
+
? { root: fromRoots, from: "client roots" }
|
|
123
|
+
: existsSync(join(cwd, "AGENTS.md"))
|
|
124
|
+
? { root: cwd, from: "start directory" }
|
|
125
|
+
: { root, from: "package" };
|
|
126
|
+
return found;
|
|
127
|
+
}
|
|
128
|
+
const files = (dir) => ({
|
|
129
|
+
AGENTS: join(dir, "AGENTS.md"),
|
|
130
|
+
FAFM: join(dir, "project.fafm"),
|
|
131
|
+
});
|
|
52
132
|
// ── Mechanism 1: the Server Card resource + its _meta context block ───
|
|
53
133
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
54
134
|
resources: [
|
|
@@ -58,9 +138,23 @@ export function createServer(root = ROOT) {
|
|
|
58
138
|
description: "This server's identity + the _meta context block.",
|
|
59
139
|
mimeType: "application/json",
|
|
60
140
|
},
|
|
141
|
+
{
|
|
142
|
+
uri: CARD_UI_URI,
|
|
143
|
+
name: "Context Card",
|
|
144
|
+
description: "The context card as an MCP App: identity, AGENTS.md, memory and discovery, rendered inline by hosts that support MCP Apps.",
|
|
145
|
+
mimeType: MCP_APP_MIME,
|
|
146
|
+
},
|
|
61
147
|
],
|
|
62
148
|
}));
|
|
63
149
|
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
150
|
+
if (req.params.uri === CARD_UI_URI) {
|
|
151
|
+
// Rendered at read time from the project's own files. The card is
|
|
152
|
+
// self-contained (inline CSS, no external requests), and its one script
|
|
153
|
+
// is progressive enhancement, so it still works in a strict sandbox.
|
|
154
|
+
return {
|
|
155
|
+
contents: [{ uri: CARD_UI_URI, mimeType: MCP_APP_MIME, text: renderCard((await project()).root, { theme: "auto" }) }],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
64
158
|
if (req.params.uri !== SERVER_CARD_URI) {
|
|
65
159
|
throw new Error(`unknown resource: ${req.params.uri}`);
|
|
66
160
|
}
|
|
@@ -178,14 +272,31 @@ export function createServer(root = ROOT) {
|
|
|
178
272
|
{
|
|
179
273
|
name: "render_context_card",
|
|
180
274
|
title: "Render Context Card",
|
|
275
|
+
// MCP Apps: hosts that support it render the card inline from this
|
|
276
|
+
// resource. Both key forms, as the official ext-apps helper writes them.
|
|
277
|
+
_meta: { ui: { resourceUri: CARD_UI_URI }, "ui/resourceUri": CARD_UI_URI },
|
|
181
278
|
annotations: { title: "Render Context Card", readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
182
279
|
description: "Render the whole card — identity, AGENTS.md, memory, discovery — as one self-contained HTML page a person can read or screenshot. AGENTS.md sections collapse by default; pass expanded:true for the full render. Also served at GET /card (?expand=all) over the HTTP transport.",
|
|
280
|
+
inputSchema: { type: "object", properties: CARD_ARGS },
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "save_context_card",
|
|
284
|
+
title: "Save Context Card",
|
|
285
|
+
annotations: { title: "Save Context Card", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
286
|
+
description: "Write the context card to context-card.html in the project and, when this server runs locally, open it in the person's browser. Returns the card as Markdown (identity, AGENTS.md sections, memory, discovery) plus a clickable link to the saved file. Use this instead of pasting render_context_card's HTML into a chat that can't display it. Replaces any earlier context-card.html.",
|
|
183
287
|
inputSchema: {
|
|
184
288
|
type: "object",
|
|
185
289
|
properties: {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
290
|
+
...CARD_ARGS,
|
|
291
|
+
open: {
|
|
292
|
+
type: "boolean",
|
|
293
|
+
description: "open the saved card in the person's browser when the server runs locally (default: true)",
|
|
294
|
+
},
|
|
295
|
+
detail: {
|
|
296
|
+
type: "string",
|
|
297
|
+
enum: ["tldr", "full"],
|
|
298
|
+
description: "the Markdown reply: tldr (default) shows five facts, each cut short; full shows every fact whole. The saved file always has everything.",
|
|
299
|
+
},
|
|
189
300
|
},
|
|
190
301
|
},
|
|
191
302
|
},
|
|
@@ -193,6 +304,8 @@ export function createServer(root = ROOT) {
|
|
|
193
304
|
}));
|
|
194
305
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
195
306
|
const args = (req.params.arguments ?? {});
|
|
307
|
+
const { root, from } = await project();
|
|
308
|
+
const { AGENTS, FAFM } = files(root);
|
|
196
309
|
switch (req.params.name) {
|
|
197
310
|
case "read_agents_md": {
|
|
198
311
|
const doc = parseAgentsMd(AGENTS);
|
|
@@ -235,24 +348,45 @@ export function createServer(root = ROOT) {
|
|
|
235
348
|
case "whoami":
|
|
236
349
|
return text(whoami(root));
|
|
237
350
|
case "render_context_card": {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
351
|
+
if (hostRendersApps(server)) {
|
|
352
|
+
// The host renders the card itself from CARD_UI_URI, so the model
|
|
353
|
+
// gets a short summary instead of a whole HTML page.
|
|
354
|
+
const doc = parseAgentsMd(AGENTS);
|
|
355
|
+
const mem = parseFafm(FAFM);
|
|
356
|
+
const sections = doc?.sections.length ?? 0;
|
|
357
|
+
const facts = mem.facts.length;
|
|
358
|
+
return text(`Showing the context card for ${resolveIdentity(root)?.name ?? basename(root)}: AGENTS.md with ${sections} section${sections === 1 ? "" : "s"}, ` +
|
|
359
|
+
`${facts} remembered fact${facts === 1 ? "" : "s"}, and this server's identity. ` +
|
|
360
|
+
"The card is displayed to the user; call read_agents_md or recall for the text itself.");
|
|
361
|
+
}
|
|
362
|
+
return text(renderCard(root, cardOptions(args)));
|
|
363
|
+
}
|
|
364
|
+
case "save_context_card": {
|
|
365
|
+
const out = join(root, "context-card.html");
|
|
366
|
+
const existed = existsSync(out);
|
|
367
|
+
writeFileSync(out, renderCard(root, cardOptions(args)));
|
|
368
|
+
// First line, plain fact: models tend to rewrite the end of a reply, not the start.
|
|
369
|
+
const saved = existed
|
|
370
|
+
? `**Updated:** \`context-card.html\` in your project, at ${out}.`
|
|
371
|
+
: `**New file:** \`context-card.html\` in your project, at ${out}. A snapshot of the context your agent reads.`;
|
|
372
|
+
// Many hosts won't follow a file:// link, so a local server opens it.
|
|
373
|
+
const raw = args.open;
|
|
374
|
+
const opened = !!opts.openFile && raw !== false && raw !== "false";
|
|
375
|
+
if (opened)
|
|
376
|
+
opts.openFile(out);
|
|
377
|
+
return text(`${saved}\n\n` +
|
|
378
|
+
`${renderCardText(root, { detail: args.detail === "full" ? "full" : "tldr" })}\n\n` +
|
|
379
|
+
"_In a host that supports MCP Apps, this card shows inline._\n\n---\n\n" +
|
|
380
|
+
(opened ? "Opened the full card in your browser.\n\n" : "") +
|
|
381
|
+
`**[Open the full card in your browser](${pathToFileURL(out).href})**\n\n` +
|
|
382
|
+
// Some hosts won't follow a file:// link; a code block gets a copy button.
|
|
383
|
+
`Or copy this into your browser's address bar:\n\n\`\`\`\n${pathToFileURL(out).href}\n\`\`\``);
|
|
251
384
|
}
|
|
252
385
|
case "list_context_sources": {
|
|
253
386
|
const doc = parseAgentsMd(AGENTS);
|
|
254
387
|
const mem = parseFafm(FAFM);
|
|
255
388
|
return text(JSON.stringify({
|
|
389
|
+
project: { path: root, from },
|
|
256
390
|
context: {
|
|
257
391
|
source: "AGENTS.md",
|
|
258
392
|
mediaType: "text/markdown",
|
|
@@ -271,7 +405,10 @@ export function createServer(root = ROOT) {
|
|
|
271
405
|
present: identity(root) !== null,
|
|
272
406
|
},
|
|
273
407
|
surfaces: {
|
|
274
|
-
mcp: {
|
|
408
|
+
mcp: {
|
|
409
|
+
serverCard: `resource ${SERVER_CARD_URI}`,
|
|
410
|
+
card: `resource ${CARD_UI_URI} (MCP App, ${MCP_APP_MIME})`,
|
|
411
|
+
},
|
|
275
412
|
http: {
|
|
276
413
|
serverCard: "GET /.well-known/mcp/server-card",
|
|
277
414
|
aiCatalog: "GET /.well-known/ai-catalog.json",
|
|
@@ -287,13 +424,15 @@ export function createServer(root = ROOT) {
|
|
|
287
424
|
return server;
|
|
288
425
|
}
|
|
289
426
|
/** Connect a server instance to a transport (stdio or http). */
|
|
290
|
-
export async function serve(transport, root = ROOT) {
|
|
291
|
-
const server = createServer(root);
|
|
427
|
+
export async function serve(transport, root = ROOT, opts = {}) {
|
|
428
|
+
const server = createServer(root, opts);
|
|
292
429
|
await server.connect(transport);
|
|
293
430
|
return server;
|
|
294
431
|
}
|
|
295
432
|
// Direct run (incl. the demo's spawned child) → stdio. pathToFileURL keeps
|
|
296
433
|
// this correct on Windows, where argv[1] is a `C:\...` path.
|
|
297
434
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
298
|
-
await
|
|
435
|
+
const { openInBrowser } = await import("./open.js");
|
|
436
|
+
const pinned = process.env.MCP_CONTEXT_CARD_ROOT;
|
|
437
|
+
await serve(new StdioServerTransport(), pinned ?? ROOT, { openFile: openInBrowser, detectRoot: !pinned });
|
|
299
438
|
}
|
package/docs/MECHANISMS.md
CHANGED
|
@@ -29,7 +29,7 @@ Both return:
|
|
|
29
29
|
```jsonc
|
|
30
30
|
{
|
|
31
31
|
"name": "mcp-context-card",
|
|
32
|
-
"version": "1.
|
|
32
|
+
"version": "1.3.0",
|
|
33
33
|
"_meta": {
|
|
34
34
|
"io.github.Wolfe-Jam.mcp-context-card/context": {
|
|
35
35
|
"source": "AGENTS.md",
|
|
@@ -118,7 +118,9 @@ GET /.well-known/ai-catalog.json
|
|
|
118
118
|
|
|
119
119
|
The same three sources also render as **the card** — `GET /card` /
|
|
120
120
|
`render_context_card` / `docs/card.html` — the human view of exactly what a
|
|
121
|
-
machine reads below.
|
|
121
|
+
machine reads below. Hosts that support MCP Apps show it inline from the
|
|
122
|
+
`ui://mcp-context-card/card.html` resource; elsewhere `save_context_card`
|
|
123
|
+
writes it to `context-card.html` and returns it as Markdown.
|
|
122
124
|
|
|
123
125
|
The same three sources back **both** discovery mechanisms:
|
|
124
126
|
|
package/docs/WIRING.md
CHANGED
|
@@ -16,15 +16,20 @@
|
|
|
16
16
|
"mcpServers": {
|
|
17
17
|
"context-card": {
|
|
18
18
|
"command": "npx",
|
|
19
|
-
"args": ["-y", "mcp-context-card"]
|
|
20
|
-
"env": { "MCP_CONTEXT_CARD_ROOT": "/abs/path/to/your/project" }
|
|
19
|
+
"args": ["-y", "mcp-context-card"]
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
```
|
|
25
24
|
|
|
26
|
-
-
|
|
27
|
-
|
|
25
|
+
- **Which project.** Unset, a stdio server finds it per call: the host's first
|
|
26
|
+
MCP root (a `file://` directory), then the start directory if it holds an
|
|
27
|
+
`AGENTS.md`, then the server's own bundled copies. A `roots/list_changed`
|
|
28
|
+
notification makes the next call look again. `list_context_sources` reports
|
|
29
|
+
`project: { path, from }`.
|
|
30
|
+
- **`MCP_CONTEXT_CARD_ROOT`** — pins the project: a directory holding
|
|
31
|
+
`AGENTS.md`, `project.fafm`, and `.well-known/fafa`. Always wins over roots.
|
|
32
|
+
Over HTTP there are no roots, so it's the only way to pick a project there.
|
|
28
33
|
- `stdout` is the JSON‑RPC wire; logging is on `stderr`.
|
|
29
34
|
- **`command: "npx"` fails to spawn on some hosts** (`spawn npx ENOENT`) — the
|
|
30
35
|
host's process spawn doesn't inherit a shell `PATH` that has `npx` on it,
|
|
@@ -83,7 +88,8 @@ And to discover what a server offers before committing to it:
|
|
|
83
88
|
await client.callTool({ name: "list_context_sources", arguments: {} });
|
|
84
89
|
// → { context: { source: "AGENTS.md", mediaType: "text/markdown", present: true, sections: 10 },
|
|
85
90
|
// memory: { … }, identity: { … },
|
|
86
|
-
// surfaces: { mcp: { serverCard: "resource mcp-context-card://server-card"
|
|
91
|
+
// surfaces: { mcp: { serverCard: "resource mcp-context-card://server-card",
|
|
92
|
+
// card: "resource ui://mcp-context-card/card.html (MCP App, text/html;profile=mcp-app)" },
|
|
87
93
|
// http: { serverCard: "GET /.well-known/mcp/server-card",
|
|
88
94
|
// aiCatalog: "GET /.well-known/ai-catalog.json",
|
|
89
95
|
// card: "GET /card" } } }
|
package/docs/card-dark.html
CHANGED
|
@@ -100,7 +100,7 @@ details.ctx-section>.md{padding:0 0 16px}
|
|
|
100
100
|
<main class="card">
|
|
101
101
|
<div class="top">
|
|
102
102
|
<h1>mcp-context-card</h1>
|
|
103
|
-
<div class="pills"><span class="pill">io.github.Wolfe-Jam</span><span class="pill">v1.
|
|
103
|
+
<div class="pills"><span class="pill">io.github.Wolfe-Jam</span><span class="pill">v1.3.0</span><span class="pill accent">published</span><span class="pill">MIT</span></div>
|
|
104
104
|
</div>
|
|
105
105
|
<section>
|
|
106
106
|
<p class="label">Context — AGENTS.md</p>
|
|
@@ -114,7 +114,7 @@ npm run test:coverage # + the coverage gate (lines 90 / funcs 85 / branches 80,
|
|
|
114
114
|
npm run demo # end to end: all tools over stdio, then over stateless HTTP
|
|
115
115
|
npm run version:check # every version-bearing spot agrees with package.json
|
|
116
116
|
npm run faf:check # project.faf / project.fafm / .well-known/fafa still describe the project</code></pre>
|
|
117
|
-
<p>CI runs <code>version:check → faf:check → typecheck → build → test:coverage → demo</code> on Linux, macOS, and Windows for every push and PR to <code>main</code> (<code>.github/workflows/ci.yml</code>), plus <code>catalog:check</code> / <code>card:check</code>, <code>faf-cli check project.faf --strict</code> (the repo dogfoods a <code>project.faf</code> — this keeps it Trophy), and <code>faf:nudge</code> (PR-only, non-blocking — warns if the code's shape moved without <code>project.faf</code>) on Linux.</p></div></details><details class="ctx-section" id="layout"><summary>Layout</summary><div class="md"><table><thead><tr><th>Path</th><th>What</th></tr></thead><tbody><tr><td><code>src/server.ts</code></td><td>the MCP server — the
|
|
117
|
+
<p>CI runs <code>version:check → faf:check → typecheck → build → test:coverage → demo</code> on Linux, macOS, and Windows for every push and PR to <code>main</code> (<code>.github/workflows/ci.yml</code>), plus <code>catalog:check</code> / <code>card:check</code>, <code>faf-cli check project.faf --strict</code> (the repo dogfoods a <code>project.faf</code> — this keeps it Trophy), and <code>faf:nudge</code> (PR-only, non-blocking — warns if the code's shape moved without <code>project.faf</code>) on Linux.</p></div></details><details class="ctx-section" id="layout"><summary>Layout</summary><div class="md"><table><thead><tr><th>Path</th><th>What</th></tr></thead><tbody><tr><td><code>src/server.ts</code></td><td>the MCP server — the ten tools + the Server Card and card resources</td></tr><tr><td><code>src/agents-md.ts</code></td><td>reads and section-splits this file</td></tr><tr><td><code>src/author.ts</code></td><td><code>author_agents_md</code> — BETTER via <code>agents-md-facts</code>, BEST when <code>project.faf</code> exists</td></tr><tr><td><code>src/md.ts</code></td><td>a minimal dependency-free Markdown → HTML renderer</td></tr><tr><td><code>src/render-card.ts</code></td><td>the card — identity + this file + memory + discovery, as one HTML page</td></tr><tr><td><code>src/memory.ts</code> → <code>src/faf/parse-fafm.ts</code></td><td>file-backed <code>remember</code> / <code>recall</code> / <code>forget</code></td></tr><tr><td><code>src/identity.ts</code></td><td><code>whoami</code> (<code>.fafa</code> → <code>package.json</code> fallback) + the <code>_meta</code> context block</td></tr><tr><td><code>src/catalog-gen.ts</code></td><td>writes <code>.well-known/ai-catalog.json</code> from the same three sources</td></tr><tr><td><code>src/transport/http.ts</code></td><td>the stateless Streamable HTTP app (Hono)</td></tr><tr><td><code>src/bin.ts</code></td><td>the entry point (<code>resolveLaunch</code>) — <code>stdio</code> · <code>--http</code> · <code>card</code> · <code>--help</code> · <code>--version</code></td></tr><tr><td><code>src/faf/parse-fafm.ts</code> · <code>parse-fafa.ts</code></td><td>the <code>.fafm</code> / <code>.fafa</code> parsers</td></tr></tbody></table></div></details><details class="ctx-section" id="conventions"><summary>Conventions</summary><div class="md"><ul><li>TypeScript strict, ESM only (<code>"type": "module"</code>, <code>.js</code> import specifiers).</li><li>Tests use <code>node:test</code> + <code>node:assert/strict</code> — no test framework.</li><li>Every source file opens with a comment stating what it is and why.</li><li>Conventional Commit messages (<code>feat:</code>, <code>fix:</code>, <code>chore:</code>, <code>test:</code>, <code>docs:</code>).</li></ul></div></details><details class="ctx-section" id="the-invariant"><summary>The invariant</summary><div class="md"><p><code>src/identity.ts</code>, <code>src/catalog-gen.ts</code>, and <code>src/render-card.ts</code> all describe the <strong>same three sources</strong>: this file, <code>project.fafm</code>, <code>.well-known/fafa</code>. Change what one exposes and you must change the others. <code>npm run catalog:check</code> and <code>npm run card:check</code> enforce it in CI — each regenerates its surface and fails on any diff.</p></div></details><details class="ctx-section" id="safety"><summary>Safety</summary><div class="md"><ul><li>Branch off <code>main</code>; CI must be green before merge.</li><li><code>npm run demo</code> writes a fact to <code>project.fafm</code> and restores the file on exit — don't kill it mid-run.</li><li>No secrets live in this repo; never add any.</li></ul></div></details><details class="ctx-section" id="definition-of-done"><summary>Definition of done</summary><div class="md"><p><code>npm run typecheck && npm run build && npm test && npm run demo</code> all green, plus <code>npm run catalog:check</code> and <code>npm run card:check</code> clean if you touched <code>AGENTS.md</code>, <code>project.fafm</code>, or <code>.well-known/fafa</code>, and <code>npm run faf:check</code> green if you changed the layout, a dependency, or the identity. On a version bump, <code>npm run version:check</code> green (it lists every spot that must move together) and <code>project.faf</code> still Trophy (<code>faf-cli check project.faf --strict</code>). If CI's <code>faf:nudge</code> warns on your PR, reconcile <code>project.faf</code> (and re-check <code>project.fafm</code> facts if <code>AGENTS.md</code> moved) or say why it's fine.</p></div></details><details class="ctx-section" id="authoring-this-file"><summary>Authoring this file</summary><div class="md"><p><code>AGENTS.md</code> here is maintained by hand. The <code>author_agents_md</code> tool (or <code>faf export --agents</code>) would draft a BEST version straight from this repo's own <code>project.faf</code> plus its detected facts — the server doesn't care how the file was authored, only that it's valid Markdown.</p></div></details></div>
|
|
118
118
|
</section>
|
|
119
119
|
<section>
|
|
120
120
|
<p class="label">Memory — 4 facts</p>
|
package/docs/card-light.html
CHANGED
|
@@ -100,7 +100,7 @@ details.ctx-section>.md{padding:0 0 16px}
|
|
|
100
100
|
<main class="card">
|
|
101
101
|
<div class="top">
|
|
102
102
|
<h1>mcp-context-card</h1>
|
|
103
|
-
<div class="pills"><span class="pill">io.github.Wolfe-Jam</span><span class="pill">v1.
|
|
103
|
+
<div class="pills"><span class="pill">io.github.Wolfe-Jam</span><span class="pill">v1.3.0</span><span class="pill accent">published</span><span class="pill">MIT</span></div>
|
|
104
104
|
</div>
|
|
105
105
|
<section>
|
|
106
106
|
<p class="label">Context — AGENTS.md</p>
|
|
@@ -114,7 +114,7 @@ npm run test:coverage # + the coverage gate (lines 90 / funcs 85 / branches 80,
|
|
|
114
114
|
npm run demo # end to end: all tools over stdio, then over stateless HTTP
|
|
115
115
|
npm run version:check # every version-bearing spot agrees with package.json
|
|
116
116
|
npm run faf:check # project.faf / project.fafm / .well-known/fafa still describe the project</code></pre>
|
|
117
|
-
<p>CI runs <code>version:check → faf:check → typecheck → build → test:coverage → demo</code> on Linux, macOS, and Windows for every push and PR to <code>main</code> (<code>.github/workflows/ci.yml</code>), plus <code>catalog:check</code> / <code>card:check</code>, <code>faf-cli check project.faf --strict</code> (the repo dogfoods a <code>project.faf</code> — this keeps it Trophy), and <code>faf:nudge</code> (PR-only, non-blocking — warns if the code's shape moved without <code>project.faf</code>) on Linux.</p></div></details><details class="ctx-section" id="layout"><summary>Layout</summary><div class="md"><table><thead><tr><th>Path</th><th>What</th></tr></thead><tbody><tr><td><code>src/server.ts</code></td><td>the MCP server — the
|
|
117
|
+
<p>CI runs <code>version:check → faf:check → typecheck → build → test:coverage → demo</code> on Linux, macOS, and Windows for every push and PR to <code>main</code> (<code>.github/workflows/ci.yml</code>), plus <code>catalog:check</code> / <code>card:check</code>, <code>faf-cli check project.faf --strict</code> (the repo dogfoods a <code>project.faf</code> — this keeps it Trophy), and <code>faf:nudge</code> (PR-only, non-blocking — warns if the code's shape moved without <code>project.faf</code>) on Linux.</p></div></details><details class="ctx-section" id="layout"><summary>Layout</summary><div class="md"><table><thead><tr><th>Path</th><th>What</th></tr></thead><tbody><tr><td><code>src/server.ts</code></td><td>the MCP server — the ten tools + the Server Card and card resources</td></tr><tr><td><code>src/agents-md.ts</code></td><td>reads and section-splits this file</td></tr><tr><td><code>src/author.ts</code></td><td><code>author_agents_md</code> — BETTER via <code>agents-md-facts</code>, BEST when <code>project.faf</code> exists</td></tr><tr><td><code>src/md.ts</code></td><td>a minimal dependency-free Markdown → HTML renderer</td></tr><tr><td><code>src/render-card.ts</code></td><td>the card — identity + this file + memory + discovery, as one HTML page</td></tr><tr><td><code>src/memory.ts</code> → <code>src/faf/parse-fafm.ts</code></td><td>file-backed <code>remember</code> / <code>recall</code> / <code>forget</code></td></tr><tr><td><code>src/identity.ts</code></td><td><code>whoami</code> (<code>.fafa</code> → <code>package.json</code> fallback) + the <code>_meta</code> context block</td></tr><tr><td><code>src/catalog-gen.ts</code></td><td>writes <code>.well-known/ai-catalog.json</code> from the same three sources</td></tr><tr><td><code>src/transport/http.ts</code></td><td>the stateless Streamable HTTP app (Hono)</td></tr><tr><td><code>src/bin.ts</code></td><td>the entry point (<code>resolveLaunch</code>) — <code>stdio</code> · <code>--http</code> · <code>card</code> · <code>--help</code> · <code>--version</code></td></tr><tr><td><code>src/faf/parse-fafm.ts</code> · <code>parse-fafa.ts</code></td><td>the <code>.fafm</code> / <code>.fafa</code> parsers</td></tr></tbody></table></div></details><details class="ctx-section" id="conventions"><summary>Conventions</summary><div class="md"><ul><li>TypeScript strict, ESM only (<code>"type": "module"</code>, <code>.js</code> import specifiers).</li><li>Tests use <code>node:test</code> + <code>node:assert/strict</code> — no test framework.</li><li>Every source file opens with a comment stating what it is and why.</li><li>Conventional Commit messages (<code>feat:</code>, <code>fix:</code>, <code>chore:</code>, <code>test:</code>, <code>docs:</code>).</li></ul></div></details><details class="ctx-section" id="the-invariant"><summary>The invariant</summary><div class="md"><p><code>src/identity.ts</code>, <code>src/catalog-gen.ts</code>, and <code>src/render-card.ts</code> all describe the <strong>same three sources</strong>: this file, <code>project.fafm</code>, <code>.well-known/fafa</code>. Change what one exposes and you must change the others. <code>npm run catalog:check</code> and <code>npm run card:check</code> enforce it in CI — each regenerates its surface and fails on any diff.</p></div></details><details class="ctx-section" id="safety"><summary>Safety</summary><div class="md"><ul><li>Branch off <code>main</code>; CI must be green before merge.</li><li><code>npm run demo</code> writes a fact to <code>project.fafm</code> and restores the file on exit — don't kill it mid-run.</li><li>No secrets live in this repo; never add any.</li></ul></div></details><details class="ctx-section" id="definition-of-done"><summary>Definition of done</summary><div class="md"><p><code>npm run typecheck && npm run build && npm test && npm run demo</code> all green, plus <code>npm run catalog:check</code> and <code>npm run card:check</code> clean if you touched <code>AGENTS.md</code>, <code>project.fafm</code>, or <code>.well-known/fafa</code>, and <code>npm run faf:check</code> green if you changed the layout, a dependency, or the identity. On a version bump, <code>npm run version:check</code> green (it lists every spot that must move together) and <code>project.faf</code> still Trophy (<code>faf-cli check project.faf --strict</code>). If CI's <code>faf:nudge</code> warns on your PR, reconcile <code>project.faf</code> (and re-check <code>project.fafm</code> facts if <code>AGENTS.md</code> moved) or say why it's fine.</p></div></details><details class="ctx-section" id="authoring-this-file"><summary>Authoring this file</summary><div class="md"><p><code>AGENTS.md</code> here is maintained by hand. The <code>author_agents_md</code> tool (or <code>faf export --agents</code>) would draft a BEST version straight from this repo's own <code>project.faf</code> plus its detected facts — the server doesn't care how the file was authored, only that it's valid Markdown.</p></div></details></div>
|
|
118
118
|
</section>
|
|
119
119
|
<section>
|
|
120
120
|
<p class="label">Memory — 4 facts</p>
|
package/docs/card.html
CHANGED
|
@@ -100,7 +100,7 @@ details.ctx-section>.md{padding:0 0 16px}
|
|
|
100
100
|
<main class="card">
|
|
101
101
|
<div class="top">
|
|
102
102
|
<h1>mcp-context-card</h1>
|
|
103
|
-
<div class="pills"><span class="pill">io.github.Wolfe-Jam</span><span class="pill">v1.
|
|
103
|
+
<div class="pills"><span class="pill">io.github.Wolfe-Jam</span><span class="pill">v1.3.0</span><span class="pill accent">published</span><span class="pill">MIT</span></div>
|
|
104
104
|
</div>
|
|
105
105
|
<section>
|
|
106
106
|
<p class="label">Context — AGENTS.md</p>
|
|
@@ -114,7 +114,7 @@ npm run test:coverage # + the coverage gate (lines 90 / funcs 85 / branches 80,
|
|
|
114
114
|
npm run demo # end to end: all tools over stdio, then over stateless HTTP
|
|
115
115
|
npm run version:check # every version-bearing spot agrees with package.json
|
|
116
116
|
npm run faf:check # project.faf / project.fafm / .well-known/fafa still describe the project</code></pre>
|
|
117
|
-
<p>CI runs <code>version:check → faf:check → typecheck → build → test:coverage → demo</code> on Linux, macOS, and Windows for every push and PR to <code>main</code> (<code>.github/workflows/ci.yml</code>), plus <code>catalog:check</code> / <code>card:check</code>, <code>faf-cli check project.faf --strict</code> (the repo dogfoods a <code>project.faf</code> — this keeps it Trophy), and <code>faf:nudge</code> (PR-only, non-blocking — warns if the code's shape moved without <code>project.faf</code>) on Linux.</p></div></details><details class="ctx-section" id="layout"><summary>Layout</summary><div class="md"><table><thead><tr><th>Path</th><th>What</th></tr></thead><tbody><tr><td><code>src/server.ts</code></td><td>the MCP server — the
|
|
117
|
+
<p>CI runs <code>version:check → faf:check → typecheck → build → test:coverage → demo</code> on Linux, macOS, and Windows for every push and PR to <code>main</code> (<code>.github/workflows/ci.yml</code>), plus <code>catalog:check</code> / <code>card:check</code>, <code>faf-cli check project.faf --strict</code> (the repo dogfoods a <code>project.faf</code> — this keeps it Trophy), and <code>faf:nudge</code> (PR-only, non-blocking — warns if the code's shape moved without <code>project.faf</code>) on Linux.</p></div></details><details class="ctx-section" id="layout"><summary>Layout</summary><div class="md"><table><thead><tr><th>Path</th><th>What</th></tr></thead><tbody><tr><td><code>src/server.ts</code></td><td>the MCP server — the ten tools + the Server Card and card resources</td></tr><tr><td><code>src/agents-md.ts</code></td><td>reads and section-splits this file</td></tr><tr><td><code>src/author.ts</code></td><td><code>author_agents_md</code> — BETTER via <code>agents-md-facts</code>, BEST when <code>project.faf</code> exists</td></tr><tr><td><code>src/md.ts</code></td><td>a minimal dependency-free Markdown → HTML renderer</td></tr><tr><td><code>src/render-card.ts</code></td><td>the card — identity + this file + memory + discovery, as one HTML page</td></tr><tr><td><code>src/memory.ts</code> → <code>src/faf/parse-fafm.ts</code></td><td>file-backed <code>remember</code> / <code>recall</code> / <code>forget</code></td></tr><tr><td><code>src/identity.ts</code></td><td><code>whoami</code> (<code>.fafa</code> → <code>package.json</code> fallback) + the <code>_meta</code> context block</td></tr><tr><td><code>src/catalog-gen.ts</code></td><td>writes <code>.well-known/ai-catalog.json</code> from the same three sources</td></tr><tr><td><code>src/transport/http.ts</code></td><td>the stateless Streamable HTTP app (Hono)</td></tr><tr><td><code>src/bin.ts</code></td><td>the entry point (<code>resolveLaunch</code>) — <code>stdio</code> · <code>--http</code> · <code>card</code> · <code>--help</code> · <code>--version</code></td></tr><tr><td><code>src/faf/parse-fafm.ts</code> · <code>parse-fafa.ts</code></td><td>the <code>.fafm</code> / <code>.fafa</code> parsers</td></tr></tbody></table></div></details><details class="ctx-section" id="conventions"><summary>Conventions</summary><div class="md"><ul><li>TypeScript strict, ESM only (<code>"type": "module"</code>, <code>.js</code> import specifiers).</li><li>Tests use <code>node:test</code> + <code>node:assert/strict</code> — no test framework.</li><li>Every source file opens with a comment stating what it is and why.</li><li>Conventional Commit messages (<code>feat:</code>, <code>fix:</code>, <code>chore:</code>, <code>test:</code>, <code>docs:</code>).</li></ul></div></details><details class="ctx-section" id="the-invariant"><summary>The invariant</summary><div class="md"><p><code>src/identity.ts</code>, <code>src/catalog-gen.ts</code>, and <code>src/render-card.ts</code> all describe the <strong>same three sources</strong>: this file, <code>project.fafm</code>, <code>.well-known/fafa</code>. Change what one exposes and you must change the others. <code>npm run catalog:check</code> and <code>npm run card:check</code> enforce it in CI — each regenerates its surface and fails on any diff.</p></div></details><details class="ctx-section" id="safety"><summary>Safety</summary><div class="md"><ul><li>Branch off <code>main</code>; CI must be green before merge.</li><li><code>npm run demo</code> writes a fact to <code>project.fafm</code> and restores the file on exit — don't kill it mid-run.</li><li>No secrets live in this repo; never add any.</li></ul></div></details><details class="ctx-section" id="definition-of-done"><summary>Definition of done</summary><div class="md"><p><code>npm run typecheck && npm run build && npm test && npm run demo</code> all green, plus <code>npm run catalog:check</code> and <code>npm run card:check</code> clean if you touched <code>AGENTS.md</code>, <code>project.fafm</code>, or <code>.well-known/fafa</code>, and <code>npm run faf:check</code> green if you changed the layout, a dependency, or the identity. On a version bump, <code>npm run version:check</code> green (it lists every spot that must move together) and <code>project.faf</code> still Trophy (<code>faf-cli check project.faf --strict</code>). If CI's <code>faf:nudge</code> warns on your PR, reconcile <code>project.faf</code> (and re-check <code>project.fafm</code> facts if <code>AGENTS.md</code> moved) or say why it's fine.</p></div></details><details class="ctx-section" id="authoring-this-file"><summary>Authoring this file</summary><div class="md"><p><code>AGENTS.md</code> here is maintained by hand. The <code>author_agents_md</code> tool (or <code>faf export --agents</code>) would draft a BEST version straight from this repo's own <code>project.faf</code> plus its detected facts — the server doesn't care how the file was authored, only that it's valid Markdown.</p></div></details></div>
|
|
118
118
|
</section>
|
|
119
119
|
<section>
|
|
120
120
|
<p class="label">Memory — 4 facts</p>
|
package/docs/img/card-dark.png
CHANGED
|
Binary file
|
|
Binary file
|
package/docs/img/card-light.png
CHANGED
|
Binary file
|
package/docs/index.html
CHANGED
|
@@ -100,7 +100,7 @@ details.ctx-section>.md{padding:0 0 16px}
|
|
|
100
100
|
<main class="card">
|
|
101
101
|
<div class="top">
|
|
102
102
|
<h1>mcp-context-card</h1>
|
|
103
|
-
<div class="pills"><span class="pill">io.github.Wolfe-Jam</span><span class="pill">v1.
|
|
103
|
+
<div class="pills"><span class="pill">io.github.Wolfe-Jam</span><span class="pill">v1.3.0</span><span class="pill accent">published</span><span class="pill">MIT</span></div>
|
|
104
104
|
</div>
|
|
105
105
|
<section>
|
|
106
106
|
<p class="label">Context — AGENTS.md</p>
|
|
@@ -114,7 +114,7 @@ npm run test:coverage # + the coverage gate (lines 90 / funcs 85 / branches 80,
|
|
|
114
114
|
npm run demo # end to end: all tools over stdio, then over stateless HTTP
|
|
115
115
|
npm run version:check # every version-bearing spot agrees with package.json
|
|
116
116
|
npm run faf:check # project.faf / project.fafm / .well-known/fafa still describe the project</code></pre>
|
|
117
|
-
<p>CI runs <code>version:check → faf:check → typecheck → build → test:coverage → demo</code> on Linux, macOS, and Windows for every push and PR to <code>main</code> (<code>.github/workflows/ci.yml</code>), plus <code>catalog:check</code> / <code>card:check</code>, <code>faf-cli check project.faf --strict</code> (the repo dogfoods a <code>project.faf</code> — this keeps it Trophy), and <code>faf:nudge</code> (PR-only, non-blocking — warns if the code's shape moved without <code>project.faf</code>) on Linux.</p></div></details><details class="ctx-section" id="layout"><summary>Layout</summary><div class="md"><table><thead><tr><th>Path</th><th>What</th></tr></thead><tbody><tr><td><code>src/server.ts</code></td><td>the MCP server — the
|
|
117
|
+
<p>CI runs <code>version:check → faf:check → typecheck → build → test:coverage → demo</code> on Linux, macOS, and Windows for every push and PR to <code>main</code> (<code>.github/workflows/ci.yml</code>), plus <code>catalog:check</code> / <code>card:check</code>, <code>faf-cli check project.faf --strict</code> (the repo dogfoods a <code>project.faf</code> — this keeps it Trophy), and <code>faf:nudge</code> (PR-only, non-blocking — warns if the code's shape moved without <code>project.faf</code>) on Linux.</p></div></details><details class="ctx-section" id="layout"><summary>Layout</summary><div class="md"><table><thead><tr><th>Path</th><th>What</th></tr></thead><tbody><tr><td><code>src/server.ts</code></td><td>the MCP server — the ten tools + the Server Card and card resources</td></tr><tr><td><code>src/agents-md.ts</code></td><td>reads and section-splits this file</td></tr><tr><td><code>src/author.ts</code></td><td><code>author_agents_md</code> — BETTER via <code>agents-md-facts</code>, BEST when <code>project.faf</code> exists</td></tr><tr><td><code>src/md.ts</code></td><td>a minimal dependency-free Markdown → HTML renderer</td></tr><tr><td><code>src/render-card.ts</code></td><td>the card — identity + this file + memory + discovery, as one HTML page</td></tr><tr><td><code>src/memory.ts</code> → <code>src/faf/parse-fafm.ts</code></td><td>file-backed <code>remember</code> / <code>recall</code> / <code>forget</code></td></tr><tr><td><code>src/identity.ts</code></td><td><code>whoami</code> (<code>.fafa</code> → <code>package.json</code> fallback) + the <code>_meta</code> context block</td></tr><tr><td><code>src/catalog-gen.ts</code></td><td>writes <code>.well-known/ai-catalog.json</code> from the same three sources</td></tr><tr><td><code>src/transport/http.ts</code></td><td>the stateless Streamable HTTP app (Hono)</td></tr><tr><td><code>src/bin.ts</code></td><td>the entry point (<code>resolveLaunch</code>) — <code>stdio</code> · <code>--http</code> · <code>card</code> · <code>--help</code> · <code>--version</code></td></tr><tr><td><code>src/faf/parse-fafm.ts</code> · <code>parse-fafa.ts</code></td><td>the <code>.fafm</code> / <code>.fafa</code> parsers</td></tr></tbody></table></div></details><details class="ctx-section" id="conventions"><summary>Conventions</summary><div class="md"><ul><li>TypeScript strict, ESM only (<code>"type": "module"</code>, <code>.js</code> import specifiers).</li><li>Tests use <code>node:test</code> + <code>node:assert/strict</code> — no test framework.</li><li>Every source file opens with a comment stating what it is and why.</li><li>Conventional Commit messages (<code>feat:</code>, <code>fix:</code>, <code>chore:</code>, <code>test:</code>, <code>docs:</code>).</li></ul></div></details><details class="ctx-section" id="the-invariant"><summary>The invariant</summary><div class="md"><p><code>src/identity.ts</code>, <code>src/catalog-gen.ts</code>, and <code>src/render-card.ts</code> all describe the <strong>same three sources</strong>: this file, <code>project.fafm</code>, <code>.well-known/fafa</code>. Change what one exposes and you must change the others. <code>npm run catalog:check</code> and <code>npm run card:check</code> enforce it in CI — each regenerates its surface and fails on any diff.</p></div></details><details class="ctx-section" id="safety"><summary>Safety</summary><div class="md"><ul><li>Branch off <code>main</code>; CI must be green before merge.</li><li><code>npm run demo</code> writes a fact to <code>project.fafm</code> and restores the file on exit — don't kill it mid-run.</li><li>No secrets live in this repo; never add any.</li></ul></div></details><details class="ctx-section" id="definition-of-done"><summary>Definition of done</summary><div class="md"><p><code>npm run typecheck && npm run build && npm test && npm run demo</code> all green, plus <code>npm run catalog:check</code> and <code>npm run card:check</code> clean if you touched <code>AGENTS.md</code>, <code>project.fafm</code>, or <code>.well-known/fafa</code>, and <code>npm run faf:check</code> green if you changed the layout, a dependency, or the identity. On a version bump, <code>npm run version:check</code> green (it lists every spot that must move together) and <code>project.faf</code> still Trophy (<code>faf-cli check project.faf --strict</code>). If CI's <code>faf:nudge</code> warns on your PR, reconcile <code>project.faf</code> (and re-check <code>project.fafm</code> facts if <code>AGENTS.md</code> moved) or say why it's fine.</p></div></details><details class="ctx-section" id="authoring-this-file"><summary>Authoring this file</summary><div class="md"><p><code>AGENTS.md</code> here is maintained by hand. The <code>author_agents_md</code> tool (or <code>faf export --agents</code>) would draft a BEST version straight from this repo's own <code>project.faf</code> plus its detected facts — the server doesn't care how the file was authored, only that it's valid Markdown.</p></div></details></div>
|
|
118
118
|
</section>
|
|
119
119
|
<section>
|
|
120
120
|
<p class="label">Memory — 4 facts</p>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-context-card",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"mcpName": "io.github.Wolfe-Jam/mcp-context-card",
|
|
5
5
|
"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 ai-catalog.json sibling entries.",
|
|
6
6
|
"keywords": [
|
package/project.faf
CHANGED
|
@@ -20,7 +20,7 @@ stack:
|
|
|
20
20
|
tech_stack: [TypeScript, "@modelcontextprotocol/sdk", "agents-md-facts", hono, "@hono/node-server", yaml]
|
|
21
21
|
human_context:
|
|
22
22
|
who: MCP host and server implementers who want a project's AGENTS.md, memory, and identity available over MCP without inventing an ad-hoc shape for each.
|
|
23
|
-
what: The essential context, memory, and identity components for MCP — usable as a base MCP on its own, or dropped into any existing MCP server as an extension.
|
|
23
|
+
what: The essential context, memory, and identity components for MCP — usable as a base MCP on its own, or dropped into any existing MCP server as an extension. Ten tools — read_agents_md / list_agents_md_sections / author_agents_md (context), remember / recall / forget (memory), whoami (identity), list_context_sources / render_context_card / save_context_card (discovery) — exposed through the Server Card _meta block and a self-published ai-catalog.json. Dual transport (stdio + stateless Streamable HTTP).
|
|
24
24
|
why: AGENTS.md is the de-facto standard for agent instructions, but a client has to know the file exists and read it whole. There is no standard way for a server to publish "here is my AGENTS.md, here is what I remember, here is who I am" — every server that wants this grows its own shape, or does without. This does it once, through mechanisms that already exist — as a base MCP, or dropped into what you've already built.
|
|
25
25
|
where: github.com/Wolfe-Jam/mcp-context-card
|
|
26
26
|
when: "2026-08-12"
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.Wolfe-Jam/mcp-context-card",
|
|
4
4
|
"title": "mcp-context-card",
|
|
5
5
|
"description": "MCP server for a project's context (AGENTS.md), memory, and identity — base or drop-in extension.",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.3.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/Wolfe-Jam/mcp-context-card",
|
|
9
9
|
"source": "github"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "mcp-context-card",
|
|
16
|
-
"version": "1.
|
|
16
|
+
"version": "1.3.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|