mcp-context-card 1.2.0 → 1.3.1
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/CHANGELOG.md +49 -0
- package/README.md +9 -6
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +6 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/md.js +63 -1
- package/dist/render-card.js +9 -9
- package/dist/server.d.ts +5 -0
- package/dist/server.js +71 -12
- package/docs/MECHANISMS.md +1 -1
- package/docs/WIRING.md +9 -4
- package/docs/card-dark.html +1 -1
- package/docs/card-light.html +1 -1
- package/docs/card.html +1 -1
- 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 +1 -1
- package/package.json +2 -2
- package/project.faf +18 -0
- package/server.json +5 -4
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.1"
|
|
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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project. Adheres to [Semantic Versioning](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 1.3.1
|
|
6
|
+
|
|
7
|
+
The registry listing catches up with 1.3.0.
|
|
8
|
+
|
|
9
|
+
- **`MCP_CONTEXT_CARD_ROOT` is described as it works now.** The listing
|
|
10
|
+
still said that, unset, the server reads its own bundled copies. Since
|
|
11
|
+
1.3.0 it follows the host's MCP roots, then the start directory if it has
|
|
12
|
+
an `AGENTS.md`. Hosts that install from `server.json` show this text when
|
|
13
|
+
you add the server.
|
|
14
|
+
- **A display name and a website.** `title` is "MCP Context Card" (it was
|
|
15
|
+
the package name), and `websiteUrl` points at the README.
|
|
16
|
+
- **`project.faf` scores 100% on the always-33 scorer** (it was 56% on
|
|
17
|
+
faf-cli 8): the enterprise slots a single npm package doesn't use are
|
|
18
|
+
marked `slotignored`, `package_manager` is `npm`, and the build, test, dev
|
|
19
|
+
and start commands are listed. CI checks it with faf-cli 8.1.0 (it was
|
|
20
|
+
pinned to 7.10.1, which counted only 21 slots).
|
|
21
|
+
|
|
22
|
+
No code change.
|
|
23
|
+
|
|
24
|
+
## 1.3.0
|
|
25
|
+
|
|
26
|
+
Your project, with nothing to configure.
|
|
27
|
+
|
|
28
|
+
- **Finds your project from the host.** With no `MCP_CONTEXT_CARD_ROOT`, a
|
|
29
|
+
local (stdio) server asks the host which project you're in, through MCP
|
|
30
|
+
roots (goose sends its session's working directory), and follows it when
|
|
31
|
+
it changes. It falls back to the directory it was started in if that has
|
|
32
|
+
an `AGENTS.md`, then to its own folder, as before. An explicit
|
|
33
|
+
`MCP_CONTEXT_CARD_ROOT` always wins; HTTP is unchanged.
|
|
34
|
+
- **`list_context_sources`** reports `project: { path, from }`: which
|
|
35
|
+
project the server is reading, and how it found it.
|
|
36
|
+
- **`save_context_card`** opens its reply with what it did, as a plain fact:
|
|
37
|
+
`New file:` (with one line on what the file is) or `Updated:`, and the
|
|
38
|
+
path.
|
|
39
|
+
- **An empty project gets next steps, not dead ends,** on both the HTML and
|
|
40
|
+
the text card: no `AGENTS.md` yet → ask your agent to draft one
|
|
41
|
+
(`author_agents_md`, from the repo's real build and test commands); no
|
|
42
|
+
facts yet → ask it to remember something. The placeholder "MCP context
|
|
43
|
+
card" pill is gone.
|
|
44
|
+
- **A project with no identity file** (no `.well-known/fafa`, no
|
|
45
|
+
`package.json`) is named after its folder on the card, not after this
|
|
46
|
+
server.
|
|
47
|
+
|
|
48
|
+
- **HTML comments in `AGENTS.md` are hidden** on the card, as GitHub hides
|
|
49
|
+
them. `agents-md-facts` marks its managed block with comments, and the
|
|
50
|
+
card showed them as text. Comments inside code stay, as content.
|
|
51
|
+
|
|
52
|
+
No API change. 134 tests, all green.
|
|
53
|
+
|
|
5
54
|
## 1.2.0
|
|
6
55
|
|
|
7
56
|
The card reaches people in any host: inline where the host supports MCP
|
package/README.md
CHANGED
|
@@ -146,22 +146,25 @@ or redirected (`> card.html`, a script, CI) it writes raw HTML to stdout instead
|
|
|
146
146
|
|
|
147
147
|
### Wire it into a host
|
|
148
148
|
|
|
149
|
-
Claude Desktop, Cursor, or any stdio host:
|
|
149
|
+
goose, Claude Desktop, Cursor, or any stdio host:
|
|
150
150
|
|
|
151
151
|
```jsonc
|
|
152
152
|
{
|
|
153
153
|
"mcpServers": {
|
|
154
154
|
"context-card": {
|
|
155
155
|
"command": "npx",
|
|
156
|
-
"args": ["-y", "mcp-context-card"]
|
|
157
|
-
"env": { "MCP_CONTEXT_CARD_ROOT": "/abs/path/to/your/project" }
|
|
156
|
+
"args": ["-y", "mcp-context-card"]
|
|
158
157
|
}
|
|
159
158
|
}
|
|
160
159
|
}
|
|
161
160
|
```
|
|
162
161
|
|
|
163
|
-
|
|
164
|
-
|
|
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:
|
|
165
168
|
`PORT=8080 npx mcp-context-card`. Requires Node ≥20.
|
|
166
169
|
|
|
167
170
|
If `command: "npx"` fails to spawn (`spawn npx ENOENT` — seen on Cursor, whose
|
|
@@ -232,7 +235,7 @@ over both transports:
|
|
|
232
235
|
4. **Discovery** — `list_context_sources()`, then the same server over stateless
|
|
233
236
|
HTTP with its `.well-known` routes and `GET /card`.
|
|
234
237
|
|
|
235
|
-
|
|
238
|
+
134 tests on Linux, macOS, and Windows, coverage‑gated in CI. Two spawn a real
|
|
236
239
|
child process and check a remembered fact survives the restart — one against
|
|
237
240
|
an existing `project.fafm`, one starting from a project that has never had
|
|
238
241
|
one; another checks the stdio and HTTP tool surfaces match, and another checks
|
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.1\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
|
@@ -140,7 +140,12 @@ if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
|
|
|
140
140
|
// terminal otherwise looks hung.
|
|
141
141
|
console.error(`${NAME} · stdio · waiting for an MCP host on stdin (--help for usage · Ctrl-C to exit)`);
|
|
142
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.
|
|
143
145
|
const { openInBrowser } = await import("./open.js");
|
|
144
|
-
await serve(new StdioServerTransport(), root, {
|
|
146
|
+
await serve(new StdioServerTransport(), root, {
|
|
147
|
+
openFile: openInBrowser,
|
|
148
|
+
detectRoot: !process.env.MCP_CONTEXT_CARD_ROOT,
|
|
149
|
+
});
|
|
145
150
|
}
|
|
146
151
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.1";
|
|
5
5
|
export declare const SERVER_CARD_URI = "mcp-context-card://server-card";
|
|
6
6
|
/** MCP Apps (io.modelcontextprotocol/ui): the card as an inline UI resource.
|
|
7
7
|
* A host that supports MCP Apps fetches this resource and renders it in a
|
package/dist/constants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.1";
|
|
5
5
|
export const SERVER_CARD_URI = "mcp-context-card://server-card";
|
|
6
6
|
/** MCP Apps (io.modelcontextprotocol/ui): the card as an inline UI resource.
|
|
7
7
|
* A host that supports MCP Apps fetches this resource and renders it in a
|
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/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>
|
|
@@ -268,7 +268,7 @@ export function renderCardText(root, opts = {}) {
|
|
|
268
268
|
const mem = parseFafm(join(root, "project.fafm"));
|
|
269
269
|
const id = resolveIdentity(root);
|
|
270
270
|
const meta = serverCardMeta();
|
|
271
|
-
const name = id?.displayName ?? id?.name ??
|
|
271
|
+
const name = id?.displayName ?? id?.name ?? basename(resolve(root));
|
|
272
272
|
const plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
|
|
273
273
|
const out = [`### ${name} — context card`];
|
|
274
274
|
const pills = [
|
|
@@ -284,7 +284,7 @@ export function renderCardText(root, opts = {}) {
|
|
|
284
284
|
const sections = agents?.sections.filter((s) => s.level > 1) ?? [];
|
|
285
285
|
out.push(agents
|
|
286
286
|
? `**Context — AGENTS.md** · ${plural(sections.length, "section")}\n${sections.map((s) => s.heading).join(" · ")}`
|
|
287
|
-
: "**Context — AGENTS.md** · none
|
|
287
|
+
: "**Context — AGENTS.md** · none yet. `author_agents_md` drafts one from this repo's real build and test commands, nothing invented.");
|
|
288
288
|
const full = opts.detail === "full";
|
|
289
289
|
const shown = full ? mem.facts : mem.facts.slice(0, TLDR_FACTS);
|
|
290
290
|
const rest = mem.facts.length - shown.length;
|
|
@@ -292,7 +292,7 @@ export function renderCardText(root, opts = {}) {
|
|
|
292
292
|
? `**Memory** · ${plural(mem.facts.length, "fact")}\n${shown
|
|
293
293
|
.map((f) => `- ${full ? f.text : clip(f.text, TLDR_CHARS)}${f.verification_status === "verified" ? " ✓" : ""}`)
|
|
294
294
|
.join("\n")}${rest ? `\n\n…and ${plural(rest, "more fact")}, in the full card` : ""}`
|
|
295
|
-
: "**Memory** · no facts yet");
|
|
295
|
+
: "**Memory** · no facts yet. Ask your agent to remember something, and it lands here.");
|
|
296
296
|
const rows = Object.entries(meta).map(([k, v]) => `| ${k.slice(META_NS.length + 1)} | \`${v.source}\` | \`${v.mediaType}\` |`);
|
|
297
297
|
out.push(["**Discovery**", "| concern | source | media type |", "|---|---|---|", ...rows].join("\n"));
|
|
298
298
|
return out.join("\n\n");
|
package/dist/server.d.ts
CHANGED
|
@@ -54,6 +54,11 @@ export interface ServerOptions {
|
|
|
54
54
|
/** Open a saved file for the person. Set only for a local (stdio) server;
|
|
55
55
|
* over HTTP the browser would open on the server, so it stays unset. */
|
|
56
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;
|
|
57
62
|
}
|
|
58
63
|
export declare function createServer(root?: string, opts?: ServerOptions): Server;
|
|
59
64
|
/** Connect a server instance to a transport (stdio or http). */
|
package/dist/server.js
CHANGED
|
@@ -16,14 +16,14 @@
|
|
|
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 { writeFileSync } from "node:fs";
|
|
21
|
-
import { dirname, join } from "node:path";
|
|
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";
|
|
22
22
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
23
23
|
import { findSection, parseAgentsMd } from "./agents-md.js";
|
|
24
24
|
import { authorAgentsMd } from "./author.js";
|
|
25
25
|
import { forget, parseFafm, recall, remember } from "./memory.js";
|
|
26
|
-
import { identity, serverCardMeta, whoami } from "./identity.js";
|
|
26
|
+
import { identity, resolveIdentity, serverCardMeta, whoami } from "./identity.js";
|
|
27
27
|
import { renderCard, renderCardText, safeAccent } from "./render-card.js";
|
|
28
28
|
export { NAME, VERSION, SERVER_CARD_URI } from "./constants.js";
|
|
29
29
|
import { NAME, VERSION, SERVER_CARD_URI, CARD_UI_URI, MCP_APP_MIME, UI_EXTENSION } from "./constants.js";
|
|
@@ -76,10 +76,59 @@ function hostRendersApps(server) {
|
|
|
76
76
|
return Array.isArray(ext?.mimeTypes) && ext.mimeTypes.includes(MCP_APP_MIME);
|
|
77
77
|
}
|
|
78
78
|
export function createServer(root = ROOT, opts = {}) {
|
|
79
|
-
const AGENTS = join(root, "AGENTS.md");
|
|
80
|
-
const FAFM = join(root, "project.fafm");
|
|
81
|
-
const FAFA = join(root, ".well-known/fafa");
|
|
82
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
|
+
});
|
|
83
132
|
// ── Mechanism 1: the Server Card resource + its _meta context block ───
|
|
84
133
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
85
134
|
resources: [
|
|
@@ -103,7 +152,7 @@ export function createServer(root = ROOT, opts = {}) {
|
|
|
103
152
|
// self-contained (inline CSS, no external requests), and its one script
|
|
104
153
|
// is progressive enhancement, so it still works in a strict sandbox.
|
|
105
154
|
return {
|
|
106
|
-
contents: [{ uri: CARD_UI_URI, mimeType: MCP_APP_MIME, text: renderCard(root, { theme: "auto" }) }],
|
|
155
|
+
contents: [{ uri: CARD_UI_URI, mimeType: MCP_APP_MIME, text: renderCard((await project()).root, { theme: "auto" }) }],
|
|
107
156
|
};
|
|
108
157
|
}
|
|
109
158
|
if (req.params.uri !== SERVER_CARD_URI) {
|
|
@@ -255,6 +304,8 @@ export function createServer(root = ROOT, opts = {}) {
|
|
|
255
304
|
}));
|
|
256
305
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
257
306
|
const args = (req.params.arguments ?? {});
|
|
307
|
+
const { root, from } = await project();
|
|
308
|
+
const { AGENTS, FAFM } = files(root);
|
|
258
309
|
switch (req.params.name) {
|
|
259
310
|
case "read_agents_md": {
|
|
260
311
|
const doc = parseAgentsMd(AGENTS);
|
|
@@ -304,7 +355,7 @@ export function createServer(root = ROOT, opts = {}) {
|
|
|
304
355
|
const mem = parseFafm(FAFM);
|
|
305
356
|
const sections = doc?.sections.length ?? 0;
|
|
306
357
|
const facts = mem.facts.length;
|
|
307
|
-
return text(`Showing the context card for ${
|
|
358
|
+
return text(`Showing the context card for ${resolveIdentity(root)?.name ?? basename(root)}: AGENTS.md with ${sections} section${sections === 1 ? "" : "s"}, ` +
|
|
308
359
|
`${facts} remembered fact${facts === 1 ? "" : "s"}, and this server's identity. ` +
|
|
309
360
|
"The card is displayed to the user; call read_agents_md or recall for the text itself.");
|
|
310
361
|
}
|
|
@@ -312,23 +363,30 @@ export function createServer(root = ROOT, opts = {}) {
|
|
|
312
363
|
}
|
|
313
364
|
case "save_context_card": {
|
|
314
365
|
const out = join(root, "context-card.html");
|
|
366
|
+
const existed = existsSync(out);
|
|
315
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.`;
|
|
316
372
|
// Many hosts won't follow a file:// link, so a local server opens it.
|
|
317
373
|
const raw = args.open;
|
|
318
374
|
const opened = !!opts.openFile && raw !== false && raw !== "false";
|
|
319
375
|
if (opened)
|
|
320
376
|
opts.openFile(out);
|
|
321
|
-
return text(`${
|
|
377
|
+
return text(`${saved}\n\n` +
|
|
378
|
+
`${renderCardText(root, { detail: args.detail === "full" ? "full" : "tldr" })}\n\n` +
|
|
322
379
|
"_In a host that supports MCP Apps, this card shows inline._\n\n---\n\n" +
|
|
323
380
|
(opened ? "Opened the full card in your browser.\n\n" : "") +
|
|
324
381
|
`**[Open the full card in your browser](${pathToFileURL(out).href})**\n\n` +
|
|
325
382
|
// Some hosts won't follow a file:// link; a code block gets a copy button.
|
|
326
|
-
`Or copy this into your browser's address bar:\n\n\`\`\`\n${pathToFileURL(out).href}\n
|
|
383
|
+
`Or copy this into your browser's address bar:\n\n\`\`\`\n${pathToFileURL(out).href}\n\`\`\``);
|
|
327
384
|
}
|
|
328
385
|
case "list_context_sources": {
|
|
329
386
|
const doc = parseAgentsMd(AGENTS);
|
|
330
387
|
const mem = parseFafm(FAFM);
|
|
331
388
|
return text(JSON.stringify({
|
|
389
|
+
project: { path: root, from },
|
|
332
390
|
context: {
|
|
333
391
|
source: "AGENTS.md",
|
|
334
392
|
mediaType: "text/markdown",
|
|
@@ -375,5 +433,6 @@ export async function serve(transport, root = ROOT, opts = {}) {
|
|
|
375
433
|
// this correct on Windows, where argv[1] is a `C:\...` path.
|
|
376
434
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
377
435
|
const { openInBrowser } = await import("./open.js");
|
|
378
|
-
|
|
436
|
+
const pinned = process.env.MCP_CONTEXT_CARD_ROOT;
|
|
437
|
+
await serve(new StdioServerTransport(), pinned ?? ROOT, { openFile: openInBrowser, detectRoot: !pinned });
|
|
379
438
|
}
|
package/docs/MECHANISMS.md
CHANGED
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,
|
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.1</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>
|
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.1</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>
|
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.1</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>
|
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.1</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>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-context-card",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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": [
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"./package.json": "./package.json"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
|
-
"node": ">=
|
|
31
|
+
"node": ">=22"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"dist",
|
package/project.faf
CHANGED
|
@@ -17,6 +17,13 @@ stack:
|
|
|
17
17
|
connection: slotignored # no database
|
|
18
18
|
hosting: Docker / any Node host — stdio for local, stateless Streamable HTTP for remote
|
|
19
19
|
cicd: GitHub Actions — typecheck + build + test:coverage + demo on 3 OSes; catalog:check + card:check on Linux
|
|
20
|
+
monorepo_tool: slotignored
|
|
21
|
+
package_manager: npm
|
|
22
|
+
workspaces: slotignored
|
|
23
|
+
admin: slotignored
|
|
24
|
+
cache: slotignored
|
|
25
|
+
search: slotignored
|
|
26
|
+
storage: slotignored
|
|
20
27
|
tech_stack: [TypeScript, "@modelcontextprotocol/sdk", "agents-md-facts", hono, "@hono/node-server", yaml]
|
|
21
28
|
human_context:
|
|
22
29
|
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.
|
|
@@ -26,3 +33,14 @@ human_context:
|
|
|
26
33
|
when: "2026-08-12"
|
|
27
34
|
how: TypeScript on the MCP SDK. Dual transport in src/bin.ts. The Server Card _meta block and ai-catalog.json are generated from the same three sources (AGENTS.md, project.fafm, .well-known/fafa) — a CI check fails on drift. remember/recall are real file-backed reads/writes proven across a process boundary in demo.ts and the test suite. See docs/MECHANISMS.md, docs/WIRING.md, docs/TRANSPORT.md.
|
|
28
35
|
key_files: [package.json, README.md, AGENTS.md, docs/MECHANISMS.md, src/server.ts, src/identity.ts, src/render-card.ts, src/author.ts, src/agents-md.ts, src/catalog-gen.ts, src/transport/http.ts]
|
|
36
|
+
monorepo:
|
|
37
|
+
packages_count: slotignored
|
|
38
|
+
build_orchestrator: slotignored
|
|
39
|
+
versioning_strategy: slotignored
|
|
40
|
+
shared_configs: slotignored
|
|
41
|
+
remote_cache: slotignored
|
|
42
|
+
commands:
|
|
43
|
+
build: npm run build
|
|
44
|
+
test: npm run test
|
|
45
|
+
dev: npm run dev
|
|
46
|
+
start: npm run start
|
package/server.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.Wolfe-Jam/mcp-context-card",
|
|
4
|
-
"title": "
|
|
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.1",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/Wolfe-Jam/mcp-context-card",
|
|
9
9
|
"source": "github"
|
|
10
10
|
},
|
|
11
|
+
"websiteUrl": "https://github.com/Wolfe-Jam/mcp-context-card#readme",
|
|
11
12
|
"packages": [
|
|
12
13
|
{
|
|
13
14
|
"registryType": "npm",
|
|
14
15
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
16
|
"identifier": "mcp-context-card",
|
|
16
|
-
"version": "1.
|
|
17
|
+
"version": "1.3.1",
|
|
17
18
|
"runtimeHint": "npx",
|
|
18
19
|
"transport": {
|
|
19
20
|
"type": "stdio"
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
"environmentVariables": [
|
|
22
23
|
{
|
|
23
24
|
"name": "MCP_CONTEXT_CARD_ROOT",
|
|
24
|
-
"description": "
|
|
25
|
+
"description": "Pins the project: a directory with AGENTS.md, project.fafm and .well-known/fafa. Unset: the host's MCP roots, then the start directory if it has an AGENTS.md.",
|
|
25
26
|
"isRequired": false
|
|
26
27
|
}
|
|
27
28
|
]
|