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 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.2.0"
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
- `MCP_CONTEXT_CARD_ROOT` points at the directory with your `AGENTS.md`. The
164
- memory tools work with or without it; identity is optional. Over HTTP instead:
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
- 117 tests on Linux, macOS, and Windows, coverage‑gated in CI. Two spawn a real
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.2.0\nServe a project's context (AGENTS.md), memory, and identity over MCP.\n\nUSAGE\n mcp-context-card stdio MCP server \u2014 what an MCP host spawns (default)\n mcp-context-card --http stateless Streamable HTTP on PORT (default 3000)\n mcp-context-card --stdio force stdio even when PORT is set\n mcp-context-card card this dir's context card \u2014 opens it in your browser\n at a terminal; HTML to stdout when piped ( > f.html )\n --theme light|dark --accent #hex\n --expanded (all sections open) --stdout\n mcp-context-card --help this text\n mcp-context-card --version print version\n\nENV\n MCP_CONTEXT_CARD_ROOT read AGENTS.md / project.fafm / .well-known/ from here\n PORT if set, run HTTP instead of stdio\n\nA bare run is an stdio server: it waits for a host to speak JSON-RPC on stdin,\nso it looks idle at a terminal. Try `card` (opens your context in a browser) or `--http`.\nhttps://github.com/Wolfe-Jam/mcp-context-card\n";
11
+ export declare const HELP = "mcp-context-card 1.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, { openFile: openInBrowser });
146
+ await serve(new StdioServerTransport(), root, {
147
+ openFile: openInBrowser,
148
+ detectRoot: !process.env.MCP_CONTEXT_CARD_ROOT,
149
+ });
145
150
  }
146
151
  }
@@ -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.2.0";
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.2.0";
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) => {
@@ -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 { NAME, SERVER_CARD_URI } from "./constants.js";
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 ?? 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 in this project.</p>`;
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
- <div class="pills">${pills || '<span class="pill">MCP context card</span>'}</div>
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 ?? 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 in this project");
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 ${NAME}: AGENTS.md with ${sections} section${sections === 1 ? "" : "s"}, ` +
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(`${renderCardText(root, { detail: args.detail === "full" ? "full" : "tldr" })}\n\n` +
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\`\`\`\n\nSaved to ${out}`);
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
- await serve(new StdioServerTransport(), ROOT, { openFile: openInBrowser });
436
+ const pinned = process.env.MCP_CONTEXT_CARD_ROOT;
437
+ await serve(new StdioServerTransport(), pinned ?? ROOT, { openFile: openInBrowser, detectRoot: !pinned });
379
438
  }
@@ -29,7 +29,7 @@ Both return:
29
29
  ```jsonc
30
30
  {
31
31
  "name": "mcp-context-card",
32
- "version": "1.2.0",
32
+ "version": "1.3.1",
33
33
  "_meta": {
34
34
  "io.github.Wolfe-Jam.mcp-context-card/context": {
35
35
  "source": "AGENTS.md",
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
- - **`MCP_CONTEXT_CARD_ROOT`** — directory holding `AGENTS.md`, `project.fafm`, and
27
- `.well-known/fafa`. Omit it and the server uses its own bundled copies.
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,
@@ -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.2.0</span><span class="pill accent">published</span><span class="pill">MIT</span></div>
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>
@@ -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.2.0</span><span class="pill accent">published</span><span class="pill">MIT</span></div>
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.2.0</span><span class="pill accent">published</span><span class="pill">MIT</span></div>
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>
Binary file
Binary file
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.2.0</span><span class="pill accent">published</span><span class="pill">MIT</span></div>
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.2.0",
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": ">=20"
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": "mcp-context-card",
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.2.0",
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.2.0",
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": "Directory holding project.faf / project.fafm / .well-known/fafa. Omitted → the server's own bundled copies.",
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
  ]