mcp-context-card 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.well-known/ai-catalog.json +39 -0
- package/.well-known/fafa +18 -0
- package/AGENTS.md +85 -0
- package/CHANGELOG.md +75 -0
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/agents-md.d.ts +22 -0
- package/dist/agents-md.js +65 -0
- package/dist/author.d.ts +11 -0
- package/dist/author.js +27 -0
- package/dist/bin.d.ts +21 -0
- package/dist/bin.js +70 -0
- package/dist/card-gen.d.ts +1 -0
- package/dist/card-gen.js +14 -0
- package/dist/catalog-gen.d.ts +26 -0
- package/dist/catalog-gen.js +72 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +5 -0
- package/dist/faf/parse-fafa.d.ts +2 -0
- package/dist/faf/parse-fafa.js +32 -0
- package/dist/faf/parse-fafm.d.ts +7 -0
- package/dist/faf/parse-fafm.js +119 -0
- package/dist/faf/types.d.ts +34 -0
- package/dist/faf/types.js +7 -0
- package/dist/identity.d.ts +35 -0
- package/dist/identity.js +82 -0
- package/dist/md.d.ts +22 -0
- package/dist/md.js +186 -0
- package/dist/memory.d.ts +13 -0
- package/dist/memory.js +12 -0
- package/dist/render-card.d.ts +10 -0
- package/dist/render-card.js +175 -0
- package/dist/server.d.ts +56 -0
- package/dist/server.js +250 -0
- package/dist/transport/http.d.ts +2 -0
- package/dist/transport/http.js +74 -0
- package/docs/MECHANISMS.md +137 -0
- package/docs/TRANSPORT.md +86 -0
- package/docs/WIRING.md +97 -0
- package/docs/card.html +121 -0
- package/docs/img/card.png +0 -0
- package/package.json +77 -0
- package/project.faf +28 -0
- package/project.fafm +46 -0
- package/server.json +49 -0
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory — real, file-backed remember/recall against a `.fafm` file.
|
|
3
|
+
*
|
|
4
|
+
* No in-memory cache, no side-file: every call reads/writes the `.fafm`
|
|
5
|
+
* from disk via a structural YAML edit (comments + layout preserved).
|
|
6
|
+
* That's the actual claim `.fafm` makes ("memory that survives across
|
|
7
|
+
* sessions") — proven by demo.ts / the test suite restarting the server
|
|
8
|
+
* process between remember() and recall() and getting the same fact back.
|
|
9
|
+
*
|
|
10
|
+
* Thin re-export: the parse/read/write logic lives in ./faf/parse-fafm.
|
|
11
|
+
*/
|
|
12
|
+
export { recall, remember, forget, parseFafm } from "./faf/parse-fafm.js";
|
|
13
|
+
export type { Memory, MemoryFact } from "./faf/types.js";
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory — real, file-backed remember/recall against a `.fafm` file.
|
|
3
|
+
*
|
|
4
|
+
* No in-memory cache, no side-file: every call reads/writes the `.fafm`
|
|
5
|
+
* from disk via a structural YAML edit (comments + layout preserved).
|
|
6
|
+
* That's the actual claim `.fafm` makes ("memory that survives across
|
|
7
|
+
* sessions") — proven by demo.ts / the test suite restarting the server
|
|
8
|
+
* process between remember() and recall() and getting the same fact back.
|
|
9
|
+
*
|
|
10
|
+
* Thin re-export: the parse/read/write logic lives in ./faf/parse-fafm.
|
|
11
|
+
*/
|
|
12
|
+
export { recall, remember, forget, parseFafm } from "./faf/parse-fafm.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Theme = "light" | "dark" | "auto";
|
|
2
|
+
export interface CardOptions {
|
|
3
|
+
theme?: Theme;
|
|
4
|
+
/** CSS hex colour for the accent. Validated; invalid falls back to AAIF. */
|
|
5
|
+
accent?: string;
|
|
6
|
+
}
|
|
7
|
+
/** AAIF brand orange (aaif.io). The default accent. */
|
|
8
|
+
export declare const AAIF_ACCENT = "#FF702D";
|
|
9
|
+
export declare function safeAccent(a?: string): string;
|
|
10
|
+
export declare function renderCard(root: string, opts?: CardOptions): string;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render-card - the project's context card as one self-contained HTML page.
|
|
3
|
+
*
|
|
4
|
+
* Everything an MCP client discovers about a project - its AGENTS.md, its
|
|
5
|
+
* memory, its identity - rendered as a card a person can read, screenshot,
|
|
6
|
+
* or drop into a PR. Same three sources as the Server Card _meta block and
|
|
7
|
+
* ai-catalog; this is the view for people.
|
|
8
|
+
*
|
|
9
|
+
* Self-contained: inline CSS, no JS, no external fonts. Renders anywhere,
|
|
10
|
+
* including as a data: URI.
|
|
11
|
+
*/
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { parseAgentsMd } from "./agents-md.js";
|
|
14
|
+
import { parseFafm } from "./memory.js";
|
|
15
|
+
import { resolveIdentity, trinityMeta, META_NS } from "./identity.js";
|
|
16
|
+
import { NAME, SERVER_CARD_URI } from "./constants.js";
|
|
17
|
+
import { escapeHtml, renderInline, renderMarkdown, slug } from "./md.js";
|
|
18
|
+
/** AAIF brand orange (aaif.io). The default accent. */
|
|
19
|
+
export const AAIF_ACCENT = "#FF702D";
|
|
20
|
+
const ACCENT_OK = /^#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3}(?:[0-9a-fA-F]{2})?)?$/;
|
|
21
|
+
export function safeAccent(a) {
|
|
22
|
+
return a && ACCENT_OK.test(a) ? a : AAIF_ACCENT;
|
|
23
|
+
}
|
|
24
|
+
const CSS = (accent) => `
|
|
25
|
+
:root{
|
|
26
|
+
--accent:${accent};
|
|
27
|
+
--bg:#f4f4f5; --card:#fff; --fg:#0a0a0a; --muted:#6b6b70;
|
|
28
|
+
--line:rgba(0,0,0,.09); --chip:rgba(0,0,0,.05);
|
|
29
|
+
}
|
|
30
|
+
:root[data-theme="dark"]{
|
|
31
|
+
--bg:#000; --card:#0d0d0d; --fg:#fafafa; --muted:#9a9aa0;
|
|
32
|
+
--line:rgba(255,255,255,.13); --chip:rgba(255,255,255,.07);
|
|
33
|
+
}
|
|
34
|
+
@media (prefers-color-scheme:dark){
|
|
35
|
+
:root:not([data-theme="light"]){
|
|
36
|
+
--bg:#000; --card:#0d0d0d; --fg:#fafafa; --muted:#9a9aa0;
|
|
37
|
+
--line:rgba(255,255,255,.13); --chip:rgba(255,255,255,.07);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
*{box-sizing:border-box}
|
|
41
|
+
body{margin:0;background:var(--bg);color:var(--fg);
|
|
42
|
+
font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
43
|
+
padding:40px 18px}
|
|
44
|
+
.card{max-width:760px;margin:0 auto;background:var(--card);border:1px solid var(--line);
|
|
45
|
+
border-radius:14px;overflow:hidden}
|
|
46
|
+
.card>*{padding:26px 30px}
|
|
47
|
+
.top{border-top:4px solid var(--accent);border-bottom:1px solid var(--line)}
|
|
48
|
+
h1{margin:0 0 10px;font-size:1.7rem;letter-spacing:-.02em}
|
|
49
|
+
.pills{display:flex;flex-wrap:wrap;gap:6px}
|
|
50
|
+
.pill{font-size:.74rem;font-weight:600;padding:3px 9px;border-radius:20px;background:var(--chip);color:var(--muted)}
|
|
51
|
+
.pill.accent{background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent)}
|
|
52
|
+
section{border-bottom:1px solid var(--line)}
|
|
53
|
+
section:last-child{border-bottom:0}
|
|
54
|
+
.label{font-size:.7rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase;
|
|
55
|
+
color:var(--accent);margin:0 0 14px}
|
|
56
|
+
.toc{display:flex;flex-wrap:wrap;gap:6px 14px;margin:0 0 20px;padding:0;list-style:none}
|
|
57
|
+
.toc a{font-size:.82rem;color:var(--muted);text-decoration:none}
|
|
58
|
+
.toc a:hover{color:var(--accent)}
|
|
59
|
+
.md h1,.md h2,.md h3,.md h4{margin:22px 0 8px;font-size:1rem;letter-spacing:-.01em}
|
|
60
|
+
.md h1{font-size:1.15rem}
|
|
61
|
+
.md p{margin:8px 0}
|
|
62
|
+
.md ul,.md ol{margin:8px 0;padding-left:22px}
|
|
63
|
+
.md li{margin:3px 0}
|
|
64
|
+
.md code{background:var(--chip);padding:1px 5px;border-radius:5px;
|
|
65
|
+
font:.86em ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
66
|
+
.md pre{background:var(--chip);padding:14px 16px;border-radius:9px;overflow:auto}
|
|
67
|
+
.md pre code{background:none;padding:0}
|
|
68
|
+
.md table{border-collapse:collapse;width:100%;margin:12px 0;font-size:.88rem;display:block;overflow:auto}
|
|
69
|
+
.md th,.md td{border:1px solid var(--line);padding:6px 10px;text-align:left}
|
|
70
|
+
.md blockquote{margin:10px 0;padding-left:14px;border-left:3px solid var(--line);color:var(--muted)}
|
|
71
|
+
.md a{color:var(--accent)}
|
|
72
|
+
.fact{padding:12px 0;border-bottom:1px solid var(--line)}
|
|
73
|
+
.fact:last-child{border-bottom:0}
|
|
74
|
+
.fact p{margin:0 0 7px}
|
|
75
|
+
.meta{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
|
|
76
|
+
.tag{font-size:.72rem;padding:2px 8px;border-radius:5px;background:var(--chip);color:var(--muted)}
|
|
77
|
+
.dot{width:7px;height:7px;border-radius:50%;background:var(--accent);display:inline-block}
|
|
78
|
+
.dot.pending{background:var(--muted)}
|
|
79
|
+
.disc{width:100%;border-collapse:collapse;font-size:.84rem}
|
|
80
|
+
.disc th,.disc td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--line)}
|
|
81
|
+
.disc th{color:var(--muted);font-weight:600}
|
|
82
|
+
.disc code{font:.86em ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--muted)}
|
|
83
|
+
.fetch{margin:14px 0 0;font-size:.82rem;color:var(--muted)}
|
|
84
|
+
.fetch code{background:var(--chip);padding:1px 5px;border-radius:5px}
|
|
85
|
+
.foot{color:var(--muted);font-size:.78rem;text-align:center;border-top:1px solid var(--line)}
|
|
86
|
+
.none{color:var(--muted);font-style:italic}
|
|
87
|
+
`;
|
|
88
|
+
const htmlAttr = (theme) => theme === "auto" ? "" : ` data-theme="${theme}"`;
|
|
89
|
+
export function renderCard(root, opts = {}) {
|
|
90
|
+
const theme = opts.theme ?? "auto";
|
|
91
|
+
const accent = safeAccent(opts.accent);
|
|
92
|
+
const agents = parseAgentsMd(join(root, "AGENTS.md"));
|
|
93
|
+
const mem = parseFafm(join(root, "project.fafm"));
|
|
94
|
+
const id = resolveIdentity(root);
|
|
95
|
+
const meta = trinityMeta();
|
|
96
|
+
const name = id?.displayName ?? id?.name ?? NAME;
|
|
97
|
+
const pills = [
|
|
98
|
+
id?.vendor && id.vendor !== id.status && `<span class="pill">${escapeHtml(id.vendor)}</span>`,
|
|
99
|
+
id?.agentVersion && `<span class="pill">v${escapeHtml(id.agentVersion)}</span>`,
|
|
100
|
+
id?.status && `<span class="pill accent">${escapeHtml(id.status)}</span>`,
|
|
101
|
+
id?.license && `<span class="pill">${escapeHtml(id.license)}</span>`,
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join("");
|
|
105
|
+
// CONTEXT — drop the redundant top-level "# AGENTS.md" heading, keep its intro
|
|
106
|
+
const bodySections = agents?.sections.filter((s) => s.level > 1) ?? [];
|
|
107
|
+
const toc = bodySections.length
|
|
108
|
+
? `<ul class="toc">${bodySections
|
|
109
|
+
.map((s) => `<li><a href="#${slug(s.heading)}">${escapeHtml(s.heading)}</a></li>`)
|
|
110
|
+
.join("")}</ul>`
|
|
111
|
+
: "";
|
|
112
|
+
const contextBody = agents
|
|
113
|
+
? `${toc}<div class="md">${renderMarkdown([
|
|
114
|
+
agents.preamble,
|
|
115
|
+
agents.sections.find((s) => s.level === 1)?.body ?? "",
|
|
116
|
+
...bodySections.map((s) => `${"#".repeat(s.level)} ${s.heading}\n\n${s.body}`),
|
|
117
|
+
]
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join("\n\n"))}</div>`
|
|
120
|
+
: `<p class="none">No AGENTS.md in this project.</p>`;
|
|
121
|
+
// MEMORY
|
|
122
|
+
const memoryBody = mem.facts.length
|
|
123
|
+
? mem.facts
|
|
124
|
+
.map((f) => {
|
|
125
|
+
const verified = f.verification_status === "verified";
|
|
126
|
+
const tags = (f.tags ?? [])
|
|
127
|
+
.map((t) => `<span class="tag">${escapeHtml(t)}</span>`)
|
|
128
|
+
.join("");
|
|
129
|
+
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>`;
|
|
130
|
+
})
|
|
131
|
+
.join("")
|
|
132
|
+
: `<p class="none">No facts yet.</p>`;
|
|
133
|
+
// DISCOVERY
|
|
134
|
+
const rows = Object.entries(meta)
|
|
135
|
+
.map(([k, v]) => {
|
|
136
|
+
const concern = k.slice(META_NS.length + 1);
|
|
137
|
+
return `<tr><td>${concern}</td><td><code>${escapeHtml(v.source)}</code></td><td><code>${escapeHtml(v.mediaType)}</code></td></tr>`;
|
|
138
|
+
})
|
|
139
|
+
.join("");
|
|
140
|
+
return `<!doctype html>
|
|
141
|
+
<html lang="en"${htmlAttr(theme)}>
|
|
142
|
+
<head>
|
|
143
|
+
<meta charset="utf-8">
|
|
144
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
145
|
+
<title>${escapeHtml(name)} — context card</title>
|
|
146
|
+
<style>${CSS(accent)}</style>
|
|
147
|
+
</head>
|
|
148
|
+
<body>
|
|
149
|
+
<main class="card">
|
|
150
|
+
<div class="top">
|
|
151
|
+
<h1>${escapeHtml(name)}</h1>
|
|
152
|
+
<div class="pills">${pills || '<span class="pill">MCP context card</span>'}</div>
|
|
153
|
+
</div>
|
|
154
|
+
<section>
|
|
155
|
+
<p class="label">Context — AGENTS.md</p>
|
|
156
|
+
${contextBody}
|
|
157
|
+
</section>
|
|
158
|
+
<section>
|
|
159
|
+
<p class="label">Memory — ${mem.facts.length} fact${mem.facts.length === 1 ? "" : "s"}</p>
|
|
160
|
+
${memoryBody}
|
|
161
|
+
</section>
|
|
162
|
+
<section>
|
|
163
|
+
<p class="label">Discovery</p>
|
|
164
|
+
<table class="disc"><thead><tr><th>concern</th><th>source</th><th>media type</th></tr></thead><tbody>${rows}</tbody></table>
|
|
165
|
+
<p class="fetch">A machine reads this over <b>MCP</b> from the
|
|
166
|
+
<code>${escapeHtml(SERVER_CARD_URI)}</code> resource; over <b>HTTP</b> also
|
|
167
|
+
from <code>GET /.well-known/mcp/server-card</code> and
|
|
168
|
+
<code>GET /.well-known/ai-catalog.json</code>.</p>
|
|
169
|
+
</section>
|
|
170
|
+
<div class="foot">${escapeHtml(name)} · context card</div>
|
|
171
|
+
</main>
|
|
172
|
+
</body>
|
|
173
|
+
</html>
|
|
174
|
+
`;
|
|
175
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-context-card server — makes a project's context, memory, and identity
|
|
3
|
+
* discoverable to any MCP client.
|
|
4
|
+
*
|
|
5
|
+
* context — read_agents_md · list_agents_md_sections (this project's AGENTS.md)
|
|
6
|
+
* memory — remember · recall · forget (a .fafm file)
|
|
7
|
+
* identity — whoami (this server's .fafa)
|
|
8
|
+
* discovery — list_context_sources (what's published, and how)
|
|
9
|
+
*
|
|
10
|
+
* ...exposed through the two mechanisms already in the ecosystem:
|
|
11
|
+
*
|
|
12
|
+
* 1. Server Card `_meta` — the `mcp-context-card://server-card` resource (in band)
|
|
13
|
+
* and `GET /.well-known/mcp/server-card` (out of band, http transport).
|
|
14
|
+
* 2. ai-catalog — `GET /.well-known/ai-catalog.json`, three sibling entries
|
|
15
|
+
* keyed by media type (see catalog-gen.ts).
|
|
16
|
+
*/
|
|
17
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
19
|
+
export { NAME, VERSION, SERVER_CARD_URI } from "./constants.js";
|
|
20
|
+
/** Default package root — `dist/` at runtime, `src/` under tsx. Both are one up. */
|
|
21
|
+
export declare const ROOT: string;
|
|
22
|
+
/**
|
|
23
|
+
* The Server Card — this server's identity plus the `_meta` context block,
|
|
24
|
+
* one namespaced key per concern. Served in-band as the
|
|
25
|
+
* `mcp-context-card://server-card` resource and out-of-band at
|
|
26
|
+
* `/.well-known/mcp/server-card`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function serverCard(): {
|
|
29
|
+
name: string;
|
|
30
|
+
version: string;
|
|
31
|
+
_meta: {
|
|
32
|
+
readonly "io.github.wolfe-jam.mcp-context-card/context": {
|
|
33
|
+
readonly source: "AGENTS.md";
|
|
34
|
+
readonly mediaType: "text/markdown";
|
|
35
|
+
};
|
|
36
|
+
readonly "io.github.wolfe-jam.mcp-context-card/memory": {
|
|
37
|
+
readonly source: "project.fafm";
|
|
38
|
+
readonly mediaType: "application/vnd.fafm+yaml";
|
|
39
|
+
readonly iana: string;
|
|
40
|
+
readonly note: "no de-facto standard for agent memory yet — this is one instantiation";
|
|
41
|
+
};
|
|
42
|
+
readonly "io.github.wolfe-jam.mcp-context-card/identity": {
|
|
43
|
+
readonly source: ".well-known/fafa";
|
|
44
|
+
readonly mediaType: "application/vnd.fafa+yaml";
|
|
45
|
+
readonly iana: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* @param root directory holding `AGENTS.md`, `project.fafm`, `.well-known/`.
|
|
51
|
+
* Defaults to the package root; a deploy points `MCP_CONTEXT_CARD_ROOT`
|
|
52
|
+
* at a real project, a test points it at a fixture.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createServer(root?: string): Server;
|
|
55
|
+
/** Connect a server instance to a transport (stdio or http). */
|
|
56
|
+
export declare function serve(transport: Transport, root?: string): Promise<Server>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-context-card server — makes a project's context, memory, and identity
|
|
3
|
+
* discoverable to any MCP client.
|
|
4
|
+
*
|
|
5
|
+
* context — read_agents_md · list_agents_md_sections (this project's AGENTS.md)
|
|
6
|
+
* memory — remember · recall · forget (a .fafm file)
|
|
7
|
+
* identity — whoami (this server's .fafa)
|
|
8
|
+
* discovery — list_context_sources (what's published, and how)
|
|
9
|
+
*
|
|
10
|
+
* ...exposed through the two mechanisms already in the ecosystem:
|
|
11
|
+
*
|
|
12
|
+
* 1. Server Card `_meta` — the `mcp-context-card://server-card` resource (in band)
|
|
13
|
+
* and `GET /.well-known/mcp/server-card` (out of band, http transport).
|
|
14
|
+
* 2. ai-catalog — `GET /.well-known/ai-catalog.json`, three sibling entries
|
|
15
|
+
* keyed by media type (see catalog-gen.ts).
|
|
16
|
+
*/
|
|
17
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
22
|
+
import { findSection, parseAgentsMd } from "./agents-md.js";
|
|
23
|
+
import { authorAgentsMd } from "./author.js";
|
|
24
|
+
import { forget, parseFafm, recall, remember } from "./memory.js";
|
|
25
|
+
import { identity, trinityMeta, whoami } from "./identity.js";
|
|
26
|
+
import { renderCard, safeAccent } from "./render-card.js";
|
|
27
|
+
export { NAME, VERSION, SERVER_CARD_URI } from "./constants.js";
|
|
28
|
+
import { NAME, VERSION, SERVER_CARD_URI } from "./constants.js";
|
|
29
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
/** Default package root — `dist/` at runtime, `src/` under tsx. Both are one up. */
|
|
31
|
+
export const ROOT = join(here, "..");
|
|
32
|
+
/**
|
|
33
|
+
* The Server Card — this server's identity plus the `_meta` context block,
|
|
34
|
+
* one namespaced key per concern. Served in-band as the
|
|
35
|
+
* `mcp-context-card://server-card` resource and out-of-band at
|
|
36
|
+
* `/.well-known/mcp/server-card`.
|
|
37
|
+
*/
|
|
38
|
+
export function serverCard() {
|
|
39
|
+
return { name: NAME, version: VERSION, _meta: trinityMeta() };
|
|
40
|
+
}
|
|
41
|
+
const text = (s) => ({ content: [{ type: "text", text: s }] });
|
|
42
|
+
/**
|
|
43
|
+
* @param root directory holding `AGENTS.md`, `project.fafm`, `.well-known/`.
|
|
44
|
+
* Defaults to the package root; a deploy points `MCP_CONTEXT_CARD_ROOT`
|
|
45
|
+
* at a real project, a test points it at a fixture.
|
|
46
|
+
*/
|
|
47
|
+
export function createServer(root = ROOT) {
|
|
48
|
+
const AGENTS = join(root, "AGENTS.md");
|
|
49
|
+
const FAFM = join(root, "project.fafm");
|
|
50
|
+
const FAFA = join(root, ".well-known/fafa");
|
|
51
|
+
const server = new Server({ name: NAME, version: VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
52
|
+
// ── Mechanism 1: the Server Card resource + its _meta context block ───
|
|
53
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
54
|
+
resources: [
|
|
55
|
+
{
|
|
56
|
+
uri: SERVER_CARD_URI,
|
|
57
|
+
name: "Server Card",
|
|
58
|
+
description: "This server's identity + the _meta context block.",
|
|
59
|
+
mimeType: "application/json",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
}));
|
|
63
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
64
|
+
if (req.params.uri !== SERVER_CARD_URI) {
|
|
65
|
+
throw new Error(`unknown resource: ${req.params.uri}`);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
contents: [
|
|
69
|
+
{ uri: SERVER_CARD_URI, mimeType: "application/json", text: JSON.stringify(serverCard(), null, 2) },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
// ── Tools ───────────────────────────────────────────────────────────
|
|
74
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
75
|
+
tools: [
|
|
76
|
+
{
|
|
77
|
+
name: "read_agents_md",
|
|
78
|
+
description: "Return this project's AGENTS.md — the whole file, or one section by heading. The instructions a client would otherwise have to know to look for and read wholesale.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
section: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "A heading to return just that section (case-insensitive, prefix match). Omit for the whole file.",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "author_agents_md",
|
|
91
|
+
description: "Author an AGENTS.md for this project from its repo facts (via agents-md-facts) and return the draft — a managed block, ready to drop in. Detects real build/test commands, entry points, and toolchain conventions; nothing invented. Does not write a file.",
|
|
92
|
+
inputSchema: { type: "object", properties: {} },
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "list_agents_md_sections",
|
|
96
|
+
description: "List the headings in this project's AGENTS.md, so a client can pull one section instead of spending context on the whole file.",
|
|
97
|
+
inputSchema: { type: "object", properties: {} },
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "remember",
|
|
101
|
+
description: "Persist a fact past the session boundary — written to a .fafm file, not held in memory.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: { id: { type: "string" }, text: { type: "string" } },
|
|
105
|
+
required: ["id", "text"],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "recall",
|
|
110
|
+
description: "Retrieve a fact stored in a previous session by id.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: { id: { type: "string" } },
|
|
114
|
+
required: ["id"],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "forget",
|
|
119
|
+
description: "Remove a fact by id — to correct or drop something stale.",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: { id: { type: "string" } },
|
|
123
|
+
required: ["id"],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "whoami",
|
|
128
|
+
description: "This server's own identity — name, vendor, version, status, license — from its .fafa card.",
|
|
129
|
+
inputSchema: { type: "object", properties: {} },
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "list_context_sources",
|
|
133
|
+
description: "What context does this project publish (AGENTS.md, memory, identity), in what media types, and through which discovery surface. For a client connecting cold.",
|
|
134
|
+
inputSchema: { type: "object", properties: {} },
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "render_context_card",
|
|
138
|
+
description: "Render the whole card — identity, AGENTS.md, memory, discovery — as one self-contained HTML page a person can read or screenshot. Also served at GET /card over the HTTP transport.",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
theme: { type: "string", enum: ["light", "dark", "auto"], description: "default: auto" },
|
|
143
|
+
accent: { type: "string", description: "CSS hex colour, e.g. #FF702D (default: the AAIF palette)" },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
}));
|
|
149
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
150
|
+
const args = (req.params.arguments ?? {});
|
|
151
|
+
switch (req.params.name) {
|
|
152
|
+
case "read_agents_md": {
|
|
153
|
+
const doc = parseAgentsMd(AGENTS);
|
|
154
|
+
if (!doc)
|
|
155
|
+
return text("(no AGENTS.md in this project)");
|
|
156
|
+
if (!args.section)
|
|
157
|
+
return text(doc.raw);
|
|
158
|
+
const s = findSection(doc, args.section);
|
|
159
|
+
return s
|
|
160
|
+
? text(`${"#".repeat(s.level)} ${s.heading}\n\n${s.body}`)
|
|
161
|
+
: text(`(no section matching "${args.section}" — headings: ${doc.sections
|
|
162
|
+
.map((x) => x.heading)
|
|
163
|
+
.join(", ")})`);
|
|
164
|
+
}
|
|
165
|
+
case "list_agents_md_sections": {
|
|
166
|
+
const doc = parseAgentsMd(AGENTS);
|
|
167
|
+
if (!doc)
|
|
168
|
+
return text("(no AGENTS.md in this project)");
|
|
169
|
+
return text(JSON.stringify(doc.sections.map((s) => ({ heading: s.heading, level: s.level })), null, 2));
|
|
170
|
+
}
|
|
171
|
+
case "author_agents_md": {
|
|
172
|
+
const a = authorAgentsMd(root);
|
|
173
|
+
const note = a.exists
|
|
174
|
+
? "AGENTS.md already exists — diff this managed block in, don't overwrite"
|
|
175
|
+
: "no AGENTS.md yet — write this, then `npx agents-md-facts --check` keeps it true";
|
|
176
|
+
return text(`<!-- ${note} -->\n\n${a.markdown}`);
|
|
177
|
+
}
|
|
178
|
+
case "remember": {
|
|
179
|
+
remember(FAFM, args.id, args.text);
|
|
180
|
+
return text(`remembered: ${args.id}`);
|
|
181
|
+
}
|
|
182
|
+
case "recall": {
|
|
183
|
+
const fact = recall(FAFM, args.id);
|
|
184
|
+
return text(fact ? fact.text : `(no memory for "${args.id}")`);
|
|
185
|
+
}
|
|
186
|
+
case "forget": {
|
|
187
|
+
return text(forget(FAFM, args.id) ? `forgot: ${args.id}` : `(no memory for "${args.id}")`);
|
|
188
|
+
}
|
|
189
|
+
case "whoami":
|
|
190
|
+
return text(whoami(root));
|
|
191
|
+
case "render_context_card":
|
|
192
|
+
return {
|
|
193
|
+
content: [
|
|
194
|
+
{
|
|
195
|
+
type: "text",
|
|
196
|
+
text: renderCard(root, {
|
|
197
|
+
theme: (["light", "dark", "auto"].includes(args.theme) ? args.theme : "auto"),
|
|
198
|
+
accent: safeAccent(args.accent),
|
|
199
|
+
}),
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
case "list_context_sources": {
|
|
204
|
+
const doc = parseAgentsMd(AGENTS);
|
|
205
|
+
const mem = parseFafm(FAFM);
|
|
206
|
+
return text(JSON.stringify({
|
|
207
|
+
context: {
|
|
208
|
+
source: "AGENTS.md",
|
|
209
|
+
mediaType: "text/markdown",
|
|
210
|
+
present: !!doc,
|
|
211
|
+
sections: doc?.sections.length ?? 0,
|
|
212
|
+
},
|
|
213
|
+
memory: {
|
|
214
|
+
source: "project.fafm",
|
|
215
|
+
mediaType: "application/vnd.fafm+yaml",
|
|
216
|
+
present: mem.facts.length > 0 || mem.profile !== undefined,
|
|
217
|
+
facts: mem.facts.length,
|
|
218
|
+
},
|
|
219
|
+
identity: {
|
|
220
|
+
source: ".well-known/fafa",
|
|
221
|
+
mediaType: "application/vnd.fafa+yaml",
|
|
222
|
+
present: identity(root) !== null,
|
|
223
|
+
},
|
|
224
|
+
surfaces: {
|
|
225
|
+
mcp: { serverCard: `resource ${SERVER_CARD_URI}` },
|
|
226
|
+
http: {
|
|
227
|
+
serverCard: "GET /.well-known/mcp/server-card",
|
|
228
|
+
aiCatalog: "GET /.well-known/ai-catalog.json",
|
|
229
|
+
card: "GET /card",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
}, null, 2));
|
|
233
|
+
}
|
|
234
|
+
default:
|
|
235
|
+
throw new Error(`unknown tool: ${req.params.name}`);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
return server;
|
|
239
|
+
}
|
|
240
|
+
/** Connect a server instance to a transport (stdio or http). */
|
|
241
|
+
export async function serve(transport, root = ROOT) {
|
|
242
|
+
const server = createServer(root);
|
|
243
|
+
await server.connect(transport);
|
|
244
|
+
return server;
|
|
245
|
+
}
|
|
246
|
+
// Direct run (incl. the demo's spawned child) → stdio. pathToFileURL keeps
|
|
247
|
+
// this correct on Windows, where argv[1] is a `C:\...` path.
|
|
248
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
249
|
+
await serve(new StdioServerTransport());
|
|
250
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* http — the stateless Streamable HTTP transport (MCP 2026-03-26+).
|
|
3
|
+
*
|
|
4
|
+
* A Hono app. `POST /mcp` is the MCP endpoint, run **stateless**: a fresh
|
|
5
|
+
* server + transport per request, `sessionIdGenerator: undefined`, and
|
|
6
|
+
* `enableJsonResponse` so every response is a complete JSON body (no SSE
|
|
7
|
+
* stream, nothing to keep open). That's the right default for a reference
|
|
8
|
+
* server — it scales horizontally, needs no sticky sessions, and there's
|
|
9
|
+
* no per-connection state to leak. A server that needs server-streamed
|
|
10
|
+
* notifications or resumability would set a `sessionIdGenerator` and hold
|
|
11
|
+
* transports in a map; this one deliberately does not.
|
|
12
|
+
*
|
|
13
|
+
* Alongside the MCP endpoint it serves the discovery documents:
|
|
14
|
+
* GET /.well-known/mcp/server-card — the Server Card + _meta trinity
|
|
15
|
+
* GET /.well-known/ai-catalog.json — the three sibling entries
|
|
16
|
+
* GET /.well-known/fafa — the agent identity card
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { Hono } from "hono";
|
|
21
|
+
import { cors } from "hono/cors";
|
|
22
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
23
|
+
import { buildCatalog } from "../catalog-gen.js";
|
|
24
|
+
import { createServer, NAME, ROOT, VERSION, serverCard } from "../server.js";
|
|
25
|
+
import { renderCard, safeAccent } from "../render-card.js";
|
|
26
|
+
export function httpApp(root = ROOT) {
|
|
27
|
+
const app = new Hono();
|
|
28
|
+
app.use("*", cors());
|
|
29
|
+
// ── MCP endpoint — stateless ────────────────────────────────────────
|
|
30
|
+
app.all("/mcp", async (c) => {
|
|
31
|
+
const server = createServer(root);
|
|
32
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
33
|
+
sessionIdGenerator: undefined, // stateless
|
|
34
|
+
enableJsonResponse: true, // complete JSON body, no SSE stream
|
|
35
|
+
});
|
|
36
|
+
await server.connect(transport);
|
|
37
|
+
const res = await transport.handleRequest(c.req.raw);
|
|
38
|
+
// JSON/stateless mode: the response body is fully built before we get
|
|
39
|
+
// here, so closing the per-request instances now can't truncate it.
|
|
40
|
+
void transport.close();
|
|
41
|
+
void server.close();
|
|
42
|
+
return res;
|
|
43
|
+
});
|
|
44
|
+
// ── Discovery documents ─────────────────────────────────────────────
|
|
45
|
+
app.get("/.well-known/mcp/server-card", (c) => c.json(serverCard()));
|
|
46
|
+
app.get("/.well-known/ai-catalog.json", (c) => {
|
|
47
|
+
c.header("content-type", "application/ai-catalog+json");
|
|
48
|
+
return c.body(JSON.stringify(buildCatalog(root), null, 2));
|
|
49
|
+
});
|
|
50
|
+
app.get("/.well-known/fafa", (c) => {
|
|
51
|
+
c.header("content-type", "application/vnd.fafa+yaml");
|
|
52
|
+
return c.body(readFileSync(join(root, ".well-known/fafa"), "utf8"));
|
|
53
|
+
});
|
|
54
|
+
// ── The card — the view for people ──────────────────────────────────
|
|
55
|
+
app.get("/card", (c) => {
|
|
56
|
+
const q = c.req.query();
|
|
57
|
+
const theme = (["light", "dark", "auto"].includes(q.theme ?? "") ? q.theme : "auto");
|
|
58
|
+
c.header("content-type", "text/html; charset=utf-8");
|
|
59
|
+
return c.body(renderCard(root, { theme, accent: safeAccent(q.accent) }));
|
|
60
|
+
});
|
|
61
|
+
// ── Index ───────────────────────────────────────────────────────────
|
|
62
|
+
app.get("/", (c) => c.json({
|
|
63
|
+
name: NAME,
|
|
64
|
+
version: VERSION,
|
|
65
|
+
mcp: "/mcp",
|
|
66
|
+
card: "/card",
|
|
67
|
+
wellKnown: [
|
|
68
|
+
"/.well-known/mcp/server-card",
|
|
69
|
+
"/.well-known/ai-catalog.json",
|
|
70
|
+
"/.well-known/fafa",
|
|
71
|
+
],
|
|
72
|
+
}));
|
|
73
|
+
return app;
|
|
74
|
+
}
|