mcp-context-card 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.well-known/ai-catalog.json +1 -1
- package/.well-known/fafa +6 -1
- package/CHANGELOG.md +43 -2
- package/README.md +44 -19
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +3 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/faf/parse-fafa.js +21 -0
- package/dist/faf/types.d.ts +15 -0
- package/dist/render-card.d.ts +46 -0
- package/dist/render-card.js +311 -86
- package/dist/server.js +3 -1
- package/dist/transport/http.js +2 -1
- package/docs/MECHANISMS.md +28 -15
- package/docs/WIRING.md +9 -4
- package/docs/card-dark.html +179 -62
- package/docs/card-light.html +179 -62
- package/docs/card.html +177 -60
- package/docs/img/card-context.png +0 -0
- 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/img/card-memory.png +0 -0
- package/docs/index.html +177 -60
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/render-card.js
CHANGED
|
@@ -23,48 +23,137 @@ export function safeAccent(a) {
|
|
|
23
23
|
return a && ACCENT_OK.test(a) ? a : AAIF_ACCENT;
|
|
24
24
|
}
|
|
25
25
|
const CSS = (accent) => `
|
|
26
|
+
/* The page and the card are themed independently. The page follows the
|
|
27
|
+
viewer's OS. The card is the owner's choice: body[data-theme] (light | dark;
|
|
28
|
+
none = follow the OS), and the reader can switch it with the ◐ toggle (#ink). */
|
|
26
29
|
:root{
|
|
27
30
|
--accent:${accent};
|
|
28
|
-
--bg:#f4f4f5
|
|
29
|
-
--line:rgba(0,0,0,.09); --chip:rgba(0,0,0,.05);
|
|
30
|
-
--card-shadow:0 1px 3px rgba(0,0,0,.06), 0 12px 32px rgba(0,0,0,.10);
|
|
31
|
+
--page-bg:#f4f4f5;--page-muted:#6b6b70;--page-line:rgba(0,0,0,.09);--page-chip:rgba(0,0,0,.05);
|
|
31
32
|
}
|
|
32
|
-
:
|
|
33
|
-
--bg:#000
|
|
34
|
-
--line:rgba(255,255,255,.13); --chip:rgba(255,255,255,.07);
|
|
35
|
-
--card-shadow:0 0 0 1px rgba(255,255,255,.16),
|
|
36
|
-
0 8px 40px color-mix(in srgb, var(--accent) 20%, transparent);
|
|
33
|
+
@media (prefers-color-scheme:dark){
|
|
34
|
+
:root{--page-bg:#000;--page-muted:#9a9aa0;--page-line:rgba(255,255,255,.13);--page-chip:rgba(255,255,255,.07)}
|
|
37
35
|
}
|
|
36
|
+
.bcard{--card:#fff;--fg:#0a0a0a;--muted:#6b6b70;--line:rgba(0,0,0,.09);--chip:rgba(0,0,0,.05);--card-shadow:0 1px 3px rgba(0,0,0,.06),0 12px 32px rgba(0,0,0,.10)}
|
|
37
|
+
body[data-theme="dark"] .bcard,
|
|
38
|
+
body[data-theme="light"] #ink:checked~.stage .bcard{--card:#0d0d0d;--fg:#fafafa;--muted:#9a9aa0;--line:rgba(255,255,255,.13);--chip:rgba(255,255,255,.07);--card-shadow:0 0 0 1px rgba(255,255,255,.16),0 8px 40px color-mix(in srgb,var(--accent) 20%,transparent)}
|
|
39
|
+
body[data-theme="dark"] #ink:checked~.stage .bcard{--card:#fff;--fg:#0a0a0a;--muted:#6b6b70;--line:rgba(0,0,0,.09);--chip:rgba(0,0,0,.05);--card-shadow:0 1px 3px rgba(0,0,0,.06),0 12px 32px rgba(0,0,0,.10)}
|
|
38
40
|
@media (prefers-color-scheme:dark){
|
|
39
|
-
:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
41
|
+
body:not([data-theme]) .bcard{--card:#0d0d0d;--fg:#fafafa;--muted:#9a9aa0;--line:rgba(255,255,255,.13);--chip:rgba(255,255,255,.07);--card-shadow:0 0 0 1px rgba(255,255,255,.16),0 8px 40px color-mix(in srgb,var(--accent) 20%,transparent)}
|
|
42
|
+
body:not([data-theme]) #ink:checked~.stage .bcard{--card:#fff;--fg:#0a0a0a;--muted:#6b6b70;--line:rgba(0,0,0,.09);--chip:rgba(0,0,0,.05);--card-shadow:0 1px 3px rgba(0,0,0,.06),0 12px 32px rgba(0,0,0,.10)}
|
|
43
|
+
}
|
|
44
|
+
@media (prefers-color-scheme:light){
|
|
45
|
+
body:not([data-theme]) #ink:checked~.stage .bcard{--card:#0d0d0d;--fg:#fafafa;--muted:#9a9aa0;--line:rgba(255,255,255,.13);--chip:rgba(255,255,255,.07);--card-shadow:0 0 0 1px rgba(255,255,255,.16),0 8px 40px color-mix(in srgb,var(--accent) 20%,transparent)}
|
|
45
46
|
}
|
|
47
|
+
.bcard{color:var(--fg)}
|
|
46
48
|
*{box-sizing:border-box}
|
|
47
|
-
body{margin:0;background:var(--bg);color:var(--
|
|
49
|
+
body{margin:0;background:var(--page-bg);color:var(--page-muted);
|
|
48
50
|
font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
49
|
-
padding:40px
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
.
|
|
54
|
-
|
|
51
|
+
padding:40px 16px}
|
|
52
|
+
/* ── the business card: two faces, flip, two views ─────────────────── */
|
|
53
|
+
.sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
54
|
+
.stage{max-width:760px;margin:0 auto}
|
|
55
|
+
.views{display:flex;justify-content:flex-end;margin:0 0 10px}
|
|
56
|
+
.view,.hint label{cursor:pointer}
|
|
57
|
+
.view{font-size:.78rem;font-weight:600;padding:3px 11px;border-radius:20px;border:1px solid var(--page-line);
|
|
58
|
+
background:var(--page-chip);color:var(--page-muted);letter-spacing:.2em}
|
|
59
|
+
.views{gap:8px}
|
|
60
|
+
.view:hover,.hint label:hover{color:var(--accent);border-color:var(--accent)}
|
|
61
|
+
.bcard{margin:0 auto;width:100%;aspect-ratio:7/4;perspective:1600px}
|
|
62
|
+
#portrait:checked~.stage{max-width:480px}
|
|
63
|
+
#portrait:checked~.stage .bcard{aspect-ratio:4/7}
|
|
64
|
+
.faces{position:relative;width:100%;height:100%;transform-style:preserve-3d;transition:transform .6s ease}
|
|
65
|
+
#flip:checked~.stage .faces{transform:rotateY(180deg)}
|
|
66
|
+
.face{position:absolute;inset:0;background:var(--card);border:1px solid var(--line);border-radius:16px;
|
|
67
|
+
box-shadow:var(--card-shadow);backface-visibility:hidden;-webkit-backface-visibility:hidden;overflow:hidden}
|
|
68
|
+
.back{transform:rotateY(180deg) translateZ(1px);display:flex;flex-direction:column}
|
|
69
|
+
/* The face turned away takes no pointer or wheel events, so the visible face
|
|
70
|
+
scrolls (a hidden backface can still win hit-testing, notably in Chrome/Safari on macOS). */
|
|
71
|
+
#flip:checked~.stage .front,#flip:not(:checked)~.stage .back{pointer-events:none;visibility:hidden;transition:visibility 0s .3s}
|
|
72
|
+
#flip:checked~.stage .back,#flip:not(:checked)~.stage .front{visibility:visible;transition:visibility 0s .3s}
|
|
73
|
+
.panes{overscroll-behavior:contain;-webkit-overflow-scrolling:touch}
|
|
74
|
+
.front{display:flex;flex-direction:column;justify-content:center;padding:34px 44px;
|
|
75
|
+
border-top:5px solid var(--accent);
|
|
76
|
+
background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 10%,var(--card)) 0%,var(--card) 55%)}
|
|
77
|
+
.front .id{max-width:34em}
|
|
78
|
+
h1{margin:0;font-size:2.1rem;line-height:1.15;letter-spacing:-.025em}
|
|
79
|
+
.title{margin:8px 0 0;font-size:.82rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--accent)}
|
|
80
|
+
.oneliner{margin:16px 0 0;font-size:1.05rem;color:var(--muted);line-height:1.5}
|
|
81
|
+
.foot-front{position:absolute;left:44px;right:130px;bottom:22px;display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px}
|
|
82
|
+
.domain{font:.8rem ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--muted)}
|
|
83
|
+
.corner{position:absolute;width:30px;height:30px;border-radius:50%;display:grid;place-items:center;
|
|
84
|
+
cursor:pointer;font-weight:700;font-size:.9rem;color:var(--muted);background:var(--chip);border:1px solid var(--line);z-index:4}
|
|
85
|
+
.corner:hover{color:var(--accent);border-color:var(--accent)}
|
|
86
|
+
.corner.tr{top:16px;right:16px;font-family:Georgia,serif;font-style:italic}
|
|
87
|
+
.logo{position:absolute;top:34px;left:44px;width:84px;height:84px;border-radius:50%;display:grid;place-items:center;
|
|
88
|
+
color:#fff;font-weight:800;font-size:2rem;letter-spacing:-.02em;line-height:1;
|
|
89
|
+
background:radial-gradient(circle at 30% 25%,color-mix(in srgb,var(--accent) 80%,#fff),var(--accent) 45%,color-mix(in srgb,var(--accent) 50%,#000));
|
|
90
|
+
box-shadow:0 0 0 3px var(--card),0 0 0 4px color-mix(in srgb,var(--accent) 45%,transparent),0 6px 20px color-mix(in srgb,var(--accent) 30%,transparent)}
|
|
91
|
+
.front .id{margin-top:70px}
|
|
92
|
+
.corner.br{bottom:16px;right:16px}
|
|
93
|
+
.flipbtn{position:absolute;right:18px;bottom:16px;z-index:4;cursor:pointer;width:42px;height:42px;border-radius:50%;
|
|
94
|
+
display:grid;place-items:center;color:var(--fg);background:transparent;box-shadow:inset 0 0 0 2px var(--fg);
|
|
95
|
+
transition:transform .2s ease}
|
|
96
|
+
.flipbtn span{font-size:1.3rem;line-height:1;font-weight:700}
|
|
97
|
+
|
|
98
|
+
/* the back's button: same spot and outline as the front's; ↺ instead of ↻. Over
|
|
99
|
+
scrolling text it takes the card's colour, so the arrow stays readable. */
|
|
100
|
+
.flipbtn.back-btn{background:var(--card)}
|
|
101
|
+
/* fast tooltip: shows at once on hover or keyboard focus (a title tooltip waits ~1 s) */
|
|
102
|
+
.flipbtn[data-tip]::after{content:attr(data-tip);position:absolute;right:0;bottom:calc(100% + 8px);white-space:nowrap;
|
|
103
|
+
font:600 .74rem/1 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;letter-spacing:0;padding:6px 9px;border-radius:7px;
|
|
104
|
+
color:var(--card);background:var(--fg);opacity:0;transform:translateY(3px);pointer-events:none;transition:opacity .08s,transform .08s}
|
|
105
|
+
.flipbtn[data-tip]:hover::after,#flip:focus-visible~.stage .flipbtn[data-tip]::after{opacity:1;transform:none}
|
|
106
|
+
.flipbtn:hover{transform:none}.flipbtn:hover span{transform:rotate(-25deg)}.flipbtn span{transition:transform .2s ease}
|
|
107
|
+
.whatis{position:absolute;top:16px;right:16px;z-index:5}
|
|
108
|
+
.whatis>summary{position:static;list-style:none}
|
|
109
|
+
.whatis>summary::-webkit-details-marker{display:none}
|
|
110
|
+
.whatis[open]>summary{color:var(--accent);border-color:var(--accent)}
|
|
111
|
+
.whatis-panel{position:absolute;right:0;top:38px;width:min(320px,70vw);padding:14px 16px;border-radius:12px;
|
|
112
|
+
background:var(--card);border:1px solid var(--line);box-shadow:var(--card-shadow);font-size:.84rem;line-height:1.5}
|
|
113
|
+
.whatis-panel p{margin:0 0 8px}.whatis-head{font-weight:800;color:var(--accent);letter-spacing:-.01em;font-size:.95rem}
|
|
114
|
+
.whatis-foot{color:var(--muted);margin:0!important}.whatis-panel code{font:.86em ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
115
|
+
.tabbar{display:flex;flex-wrap:wrap;gap:4px;padding:14px 60px 0 18px;border-bottom:1px solid var(--line);border-top:5px solid var(--accent)}
|
|
116
|
+
.tabbar label{cursor:pointer;font-size:.8rem;font-weight:600;color:var(--muted);padding:6px 11px;border-radius:8px 8px 0 0}
|
|
117
|
+
.tabbar label:hover{color:var(--fg)}
|
|
118
|
+
.panes{flex:1;overflow:auto;padding:20px 26px 72px}
|
|
119
|
+
.pane{display:none}
|
|
120
|
+
.lead{margin:0 0 14px;font-size:1rem;line-height:1.6}
|
|
121
|
+
.kv{border-collapse:collapse;font-size:.86rem;width:100%}
|
|
122
|
+
.kv th{text-align:left;font-weight:600;color:var(--muted);padding:6px 14px 6px 0;white-space:nowrap;vertical-align:top}
|
|
123
|
+
.kv td{padding:6px 0;border-bottom:1px solid var(--line)}
|
|
124
|
+
.kv code,.lead code{font:.86em ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
125
|
+
.hint{text-align:center;color:var(--page-muted);font-size:.78rem;margin:14px 0 0}
|
|
126
|
+
.hint label{color:var(--accent)}
|
|
127
|
+
#flip:focus-visible~.stage .flipbtn,#portrait:focus-visible~.stage .view[for=portrait],#ink:focus-visible~.stage .view[for=ink]{outline:2px solid var(--accent);outline-offset:2px}
|
|
128
|
+
@media (max-width:640px){
|
|
129
|
+
.bcard,#portrait:checked~.stage .bcard{aspect-ratio:auto;height:min(78vh,720px)}
|
|
130
|
+
.front{padding:28px 26px}.foot-front{left:26px}
|
|
131
|
+
.logo{top:24px;left:26px;width:64px;height:64px;font-size:1.5rem}.front .id{margin-top:56px}
|
|
132
|
+
h1{font-size:1.7rem}
|
|
133
|
+
}
|
|
134
|
+
/* flat: expanded render and print — both faces, every tab, no flip */
|
|
135
|
+
.flat .faces,.flat .face{position:static;transform:none!important;height:auto}
|
|
136
|
+
.flat .bcard{aspect-ratio:auto;perspective:none}
|
|
137
|
+
.flat .face{margin:0 0 18px}.flat .front{min-height:260px;position:relative}
|
|
138
|
+
.flat .pane{display:block;margin:0 0 22px}.flat .bcard{color:var(--fg)}.flat .tabbar,.flat .views,.flat .hint,.flat .corner,.flat .flipbtn,.flat .whatis{display:none}
|
|
139
|
+
.flat .panes{overflow:visible}
|
|
140
|
+
@media print{
|
|
141
|
+
.faces,.face{position:static!important;transform:none!important;height:auto!important}
|
|
142
|
+
.bcard{aspect-ratio:auto!important}.face{margin:0 0 18px;box-shadow:none}
|
|
143
|
+
.pane{display:block!important;margin:0 0 22px}.tabbar,.views,.hint,.corner,.flipbtn,.whatis{display:none!important}
|
|
144
|
+
.panes{overflow:visible}
|
|
145
|
+
}
|
|
55
146
|
.pills{display:flex;flex-wrap:wrap;gap:6px}
|
|
56
147
|
.pill{font-size:.74rem;font-weight:600;padding:3px 9px;border-radius:20px;background:var(--chip);color:var(--muted)}
|
|
57
148
|
.pill.accent{background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent)}
|
|
58
|
-
section{border-bottom:1px solid var(--line)}
|
|
59
|
-
section:last-child{border-bottom:0}
|
|
60
149
|
.label{font-size:.7rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase;
|
|
61
150
|
color:var(--accent);margin:0 0 14px}
|
|
62
151
|
.toc{display:flex;flex-wrap:wrap;gap:6px 14px;margin:0;padding:0;list-style:none}
|
|
63
152
|
.toc a{font-size:.82rem;color:var(--muted);text-decoration:none}
|
|
64
153
|
.toc a:hover{color:var(--accent)}
|
|
65
154
|
/* ── collapsible context ─────────────────────────────────────────── */
|
|
66
|
-
.ctx-nav{position:sticky;top
|
|
67
|
-
margin:0 -
|
|
155
|
+
.ctx-nav{position:sticky;top:-20px;z-index:3;background:var(--card);
|
|
156
|
+
margin:0 -26px 16px;padding:11px 26px;border-bottom:1px solid var(--line);
|
|
68
157
|
display:flex;flex-wrap:wrap;align-items:flex-start;gap:8px 16px}
|
|
69
158
|
.xall{margin-left:auto;flex:none;font:inherit;font-size:.76rem;font-weight:600;
|
|
70
159
|
white-space:nowrap;padding:3px 11px;border-radius:20px;border:1px solid var(--line);
|
|
@@ -110,29 +199,74 @@ details.ctx-section>.md{padding:0 0 16px}
|
|
|
110
199
|
.disc code{font:.86em ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--muted)}
|
|
111
200
|
.fetch{margin:14px 0 0;font-size:.82rem;color:var(--muted)}
|
|
112
201
|
.fetch code{background:var(--chip);padding:1px 5px;border-radius:5px}
|
|
113
|
-
.foot{color:var(--muted);font-size:.78rem;text-align:center;border-top:1px solid var(--line)}
|
|
114
202
|
.none{color:var(--muted);font-style:italic}
|
|
115
203
|
`;
|
|
116
204
|
const htmlAttr = (theme) => theme === "auto" ? "" : ` data-theme="${theme}"`;
|
|
205
|
+
/** What the front calls this thing, from where it runs: "MCP server", "A2A agent", or both. */
|
|
206
|
+
export function cardKind(id) {
|
|
207
|
+
const kinds = [];
|
|
208
|
+
const protocols = new Set((id?.endpoints ?? []).map((e) => e.protocol));
|
|
209
|
+
if (protocols.has("a2a"))
|
|
210
|
+
kinds.push("A2A agent");
|
|
211
|
+
if (protocols.has("mcp") || (id?.packages ?? []).length)
|
|
212
|
+
kinds.push("MCP server");
|
|
213
|
+
return kinds;
|
|
214
|
+
}
|
|
215
|
+
/** The front's title line: what it is, then its version. Empty when nothing is known. */
|
|
216
|
+
export function cardTitle(id) {
|
|
217
|
+
return [cardKind(id).join(" · "), id?.agentVersion ? `v${id.agentVersion}` : ""].filter(Boolean).join(" · ");
|
|
218
|
+
}
|
|
219
|
+
/** The front's logo: the first letters of the name's first two words ("mcp-context-card" → "MC"). */
|
|
220
|
+
export function cardInitials(name) {
|
|
221
|
+
const words = name.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
222
|
+
return words
|
|
223
|
+
.slice(0, 2)
|
|
224
|
+
.map((w) => Array.from(w)[0].toUpperCase())
|
|
225
|
+
.join("");
|
|
226
|
+
}
|
|
227
|
+
/** The front's one-liner: the first sentence of the description. */
|
|
228
|
+
export function cardOneLiner(id, max = 140) {
|
|
229
|
+
const d = id?.description;
|
|
230
|
+
if (!d)
|
|
231
|
+
return "";
|
|
232
|
+
const first = clip(d, max);
|
|
233
|
+
if (!first.endsWith("…"))
|
|
234
|
+
return first;
|
|
235
|
+
// A long first sentence: stop at its first natural pause (— ; :) when that
|
|
236
|
+
// still says something, rather than cutting a phrase in half.
|
|
237
|
+
const pause = /\s[—–]\s|;\s|:\s/.exec(d);
|
|
238
|
+
return pause && pause.index >= 40 && pause.index <= max ? d.slice(0, pause.index).trim() : first;
|
|
239
|
+
}
|
|
240
|
+
/** The project's card: read its files, then draw. */
|
|
117
241
|
export function renderCard(root, opts = {}) {
|
|
118
|
-
|
|
119
|
-
|
|
242
|
+
return renderBusinessCard(readCard(root, opts), opts);
|
|
243
|
+
}
|
|
244
|
+
/** The (i) panel for an agent or server card. */
|
|
245
|
+
const AGENT_ABOUT = `<p class="whatis-head">Business Cards for Agents</p>
|
|
246
|
+
<p>This is the public card of an AI agent or MCP server: who it is, what it does, where to reach it. Flip it for the detail.</p>
|
|
247
|
+
<p>People read it here. Machines read the same facts from its MCP Server Card and AI Catalog, before they ever connect.</p>
|
|
248
|
+
<p class="whatis-foot">Made with <code>mcp-context-card</code></p>`;
|
|
249
|
+
/** Read a project's AGENTS.md, memory and identity into a business card. */
|
|
250
|
+
export function readCard(root, opts = {}) {
|
|
251
|
+
const flat = !!opts.expanded;
|
|
120
252
|
const agents = parseAgentsMd(join(root, "AGENTS.md"));
|
|
121
253
|
const mem = parseFafm(join(root, "project.fafm"));
|
|
122
254
|
const id = resolveIdentity(root);
|
|
123
255
|
const meta = serverCardMeta();
|
|
124
256
|
const name = id?.displayName ?? id?.name ?? basename(resolve(root));
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
.
|
|
132
|
-
.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
//
|
|
257
|
+
const title = cardTitle(id);
|
|
258
|
+
const oneLiner = cardOneLiner(id);
|
|
259
|
+
const domain = /^urn:air:([^:]+):/i.exec(id?.id ?? "")?.[1];
|
|
260
|
+
// The version rides in the title line, so the front's chips skip it.
|
|
261
|
+
const chips = [
|
|
262
|
+
id?.vendor && id.vendor !== id.status ? { text: id.vendor } : null,
|
|
263
|
+
!title && id?.agentVersion ? { text: `v${id.agentVersion}` } : null,
|
|
264
|
+
id?.status ? { text: id.status, accent: true } : null,
|
|
265
|
+
id?.license ? { text: id.license } : null,
|
|
266
|
+
].filter((c) => !!c);
|
|
267
|
+
// CONTEXT — one <details> per AGENTS.md section, collapsed by default;
|
|
268
|
+
// `expanded` renders them all open. The "# AGENTS.md" top-level heading is
|
|
269
|
+
// dropped; its intro rides above the sections.
|
|
136
270
|
const bodySections = agents?.sections.filter((s) => s.level > 1) ?? [];
|
|
137
271
|
const toc = bodySections.length
|
|
138
272
|
? `<ul class="toc">${bodySections
|
|
@@ -142,18 +276,19 @@ export function renderCard(root, opts = {}) {
|
|
|
142
276
|
const preamble = [agents?.preamble, agents?.sections.find((s) => s.level === 1)?.body ?? ""]
|
|
143
277
|
.filter(Boolean)
|
|
144
278
|
.join("\n\n");
|
|
145
|
-
const openAttr =
|
|
279
|
+
const openAttr = flat ? " open" : "";
|
|
146
280
|
const sections = bodySections
|
|
147
281
|
.map((s) => `<details class="ctx-section"${openAttr} id="${slug(s.heading)}"><summary>${escapeHtml(s.heading)}</summary><div class="md">${renderMarkdown(s.body)}</div></details>`)
|
|
148
282
|
.join("");
|
|
149
283
|
const contextBody = agents
|
|
150
284
|
? `<div class="ctx-nav">${toc}${bodySections.length
|
|
151
|
-
? `<button type="button" class="xall" hidden>${
|
|
285
|
+
? `<button type="button" class="xall" hidden>${flat ? "Collapse all" : "Expand all"}</button>`
|
|
152
286
|
: ""}</div>
|
|
153
287
|
${preamble ? `<div class="ctx-preamble md">${renderMarkdown(preamble)}</div>` : ""}
|
|
154
288
|
<div class="ctx-body">${sections}</div>`
|
|
155
289
|
: `<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
290
|
// MEMORY
|
|
291
|
+
const memLabel = `${mem.facts.length} fact${mem.facts.length === 1 ? "" : "s"}${opts.memory === "private" && mem.facts.length ? ", kept private" : ""}`;
|
|
157
292
|
const memoryBody = opts.memory === "private" && mem.facts.length
|
|
158
293
|
? `<p class="none">Kept private on this page. To show the facts, set <code>${PUBLISH_MEMORY_ENV}=1</code>.</p>`
|
|
159
294
|
: mem.facts.length
|
|
@@ -167,6 +302,27 @@ export function renderCard(root, opts = {}) {
|
|
|
167
302
|
})
|
|
168
303
|
.join("")
|
|
169
304
|
: `<p class="none">No facts yet. Ask your agent to remember something, and it lands here.</p>`;
|
|
305
|
+
// ABOUT — the identity, in full
|
|
306
|
+
const aboutRows = [
|
|
307
|
+
id?.id && ["Agent ID", `<code>${escapeHtml(id.id)}</code>`],
|
|
308
|
+
id?.vendor && ["Publisher", escapeHtml(id.vendor)],
|
|
309
|
+
id?.agentVersion && ["Version", `v${escapeHtml(id.agentVersion)}`],
|
|
310
|
+
id?.status && ["Status", escapeHtml(id.status)],
|
|
311
|
+
id?.license && ["License", escapeHtml(id.license)],
|
|
312
|
+
...(id?.endpoints ?? []).map((e) => [
|
|
313
|
+
e.protocol === "a2a" ? "A2A endpoint" : e.protocol === "mcp" ? "MCP endpoint" : escapeHtml(e.protocol),
|
|
314
|
+
e.location ? `<code>${escapeHtml(e.location)}</code>` : "—",
|
|
315
|
+
]),
|
|
316
|
+
...(id?.packages ?? []).map((pk) => [`Package (${escapeHtml(pk.registryType)})`, `<code>${escapeHtml(pk.identifier)}</code>`]),
|
|
317
|
+
].filter(Boolean);
|
|
318
|
+
const aboutBody = `${id?.description ? `<p class="lead">${escapeHtml(id.description)}</p>` : `<p class="none">No description yet. <code>npx faf-cli card init</code> writes one into the project's <code>.fafa</code>.</p>`}${aboutRows.length
|
|
319
|
+
? `<table class="kv"><tbody>${aboutRows.map(([k, v]) => `<tr><th>${k}</th><td>${v}</td></tr>`).join("")}</tbody></table>`
|
|
320
|
+
: ""}`;
|
|
321
|
+
// SKILLS — only when the .fafa lists capabilities
|
|
322
|
+
const skills = id?.skills ?? [];
|
|
323
|
+
const skillsBody = skills
|
|
324
|
+
.map((sk) => `<div class="fact"><p><b>${escapeHtml(sk.name)}</b>${sk.description ? ` — ${escapeHtml(sk.description)}` : ""}</p></div>`)
|
|
325
|
+
.join("");
|
|
170
326
|
// DISCOVERY
|
|
171
327
|
const rows = Object.entries(meta)
|
|
172
328
|
.map(([k, v]) => {
|
|
@@ -174,63 +330,129 @@ export function renderCard(root, opts = {}) {
|
|
|
174
330
|
return `<tr><td>${concern}</td><td><code>${escapeHtml(v.source)}</code></td><td><code>${escapeHtml(v.mediaType)}</code></td></tr>`;
|
|
175
331
|
})
|
|
176
332
|
.join("");
|
|
333
|
+
const discoveryBody = `<table class="disc"><thead><tr><th>concern</th><th>source</th><th>media type</th></tr></thead><tbody>${rows}</tbody></table>
|
|
334
|
+
<p class="fetch">A machine reads this over <b>MCP</b> from the
|
|
335
|
+
<code>${escapeHtml(SERVER_CARD_URI)}</code> resource; over <b>HTTP</b> also
|
|
336
|
+
from <code>GET /mcp/server-card</code> and
|
|
337
|
+
<code>GET /.well-known/ai-catalog.json</code>.</p>`;
|
|
338
|
+
// The back: tabs (CSS radios; every pane shows when flat or printed).
|
|
339
|
+
// [key, tab label, pane heading, body]
|
|
340
|
+
const tabs = [
|
|
341
|
+
["about", "About", "About", aboutBody],
|
|
342
|
+
...(skills.length
|
|
343
|
+
? [["skills", "Skills", `Skills — ${skills.length}`, skillsBody]]
|
|
344
|
+
: []),
|
|
345
|
+
["context", "Context", "Context — AGENTS.md", contextBody],
|
|
346
|
+
["memory", "Memory", `Memory — ${memLabel}`, memoryBody],
|
|
347
|
+
["discovery", "Discovery", "Discovery", discoveryBody],
|
|
348
|
+
];
|
|
349
|
+
return {
|
|
350
|
+
name,
|
|
351
|
+
title,
|
|
352
|
+
oneLiner,
|
|
353
|
+
domain,
|
|
354
|
+
chips,
|
|
355
|
+
tabs: tabs.map(([key, label, heading, html]) => ({ key, label, heading, html })),
|
|
356
|
+
about: AGENT_ABOUT,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/** Draw any business card as one self-contained HTML page. */
|
|
360
|
+
export function renderBusinessCard(card, opts = {}) {
|
|
361
|
+
const theme = opts.theme ?? "auto";
|
|
362
|
+
const accent = safeAccent(opts.accent);
|
|
363
|
+
const portrait = opts.layout === "portrait";
|
|
364
|
+
const flat = !!opts.expanded;
|
|
365
|
+
const { name, title, oneLiner, domain } = card;
|
|
366
|
+
const tabs = card.tabs.map((t) => ({ ...t, key: t.key.replace(/[^a-z0-9-]/gi, "").toLowerCase() || "tab" }));
|
|
367
|
+
const pills = card.chips
|
|
368
|
+
.map((c) => `<span class="pill${c.accent ? " accent" : ""}">${escapeHtml(c.text)}</span>`)
|
|
369
|
+
.join("");
|
|
370
|
+
const radios = tabs
|
|
371
|
+
.map((t, i) => `<input type="radio" name="tab" id="t-${t.key}" class="sr"${i === 0 ? " checked" : ""}>`)
|
|
372
|
+
.join("");
|
|
373
|
+
const tabbar = tabs.map((t) => `<label for="t-${t.key}">${escapeHtml(t.label)}</label>`).join("");
|
|
374
|
+
const panes = tabs
|
|
375
|
+
.map((t) => `<div class="pane p-${t.key}"><p class="label">${escapeHtml(t.heading)}</p>${t.html}</div>`)
|
|
376
|
+
.join("");
|
|
377
|
+
// One CSS rule pair per tab: its pane shows, its label lights, when its radio is checked.
|
|
378
|
+
const tabCss = tabs
|
|
379
|
+
.map((t) => `#t-${t.key}:checked~.panes .p-${t.key}{display:block}#t-${t.key}:checked~.tabbar label[for=t-${t.key}]{color:var(--accent);background:var(--chip)}`)
|
|
380
|
+
.join("");
|
|
381
|
+
// The (i) panel: the same control, in the same spot, on both faces.
|
|
382
|
+
const WHATIS = `<details class="whatis">
|
|
383
|
+
<summary class="corner tr" title="About this card" aria-label="About this card">i</summary>
|
|
384
|
+
<div class="whatis-panel">
|
|
385
|
+
${card.about}
|
|
386
|
+
</div>
|
|
387
|
+
</details>`;
|
|
177
388
|
return `<!doctype html>
|
|
178
|
-
<html lang="en"
|
|
389
|
+
<html lang="en">
|
|
179
390
|
<head>
|
|
180
391
|
<meta charset="utf-8">
|
|
181
392
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
182
|
-
<title>${escapeHtml(name)} —
|
|
183
|
-
<style>${CSS(accent)}</style>
|
|
393
|
+
<title>${escapeHtml(name)} — business card</title>
|
|
394
|
+
<style>${CSS(accent)}${tabCss}</style>
|
|
184
395
|
</head>
|
|
185
|
-
<body>
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
<
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
</
|
|
209
|
-
|
|
396
|
+
<body${flat ? ` class="flat"` : ""}${htmlAttr(theme)}>
|
|
397
|
+
<input type="checkbox" id="flip" class="sr" aria-label="Flip the card">
|
|
398
|
+
<input type="checkbox" id="ink" class="sr" aria-label="Switch the card between light and dark">
|
|
399
|
+
<input type="checkbox" id="portrait" class="sr" aria-label="Portrait view"${portrait ? " checked" : ""}>
|
|
400
|
+
<div class="stage">
|
|
401
|
+
<div class="views"><label for="ink" class="view" title="Card: light or dark">◐</label><label for="portrait" class="view" title="Landscape or portrait">▭ ▯</label></div>
|
|
402
|
+
<main class="bcard">
|
|
403
|
+
<div class="faces">
|
|
404
|
+
<section class="face front">
|
|
405
|
+
<div class="logo" aria-hidden="true">${escapeHtml(cardInitials(name))}</div>
|
|
406
|
+
${WHATIS}
|
|
407
|
+
<div class="id">
|
|
408
|
+
<h1>${escapeHtml(name)}</h1>
|
|
409
|
+
${title ? `<p class="title">${escapeHtml(title)}</p>` : ""}
|
|
410
|
+
${oneLiner ? `<p class="oneliner">${escapeHtml(oneLiner)}</p>` : ""}
|
|
411
|
+
</div>
|
|
412
|
+
<div class="foot-front">${domain ? `<span class="domain">${escapeHtml(domain)}</span>` : ""}${pills ? `<div class="pills">${pills}</div>` : ""}</div>
|
|
413
|
+
<label for="flip" class="flipbtn" data-tip="Flip the card" aria-label="Flip the card"><span aria-hidden="true">↻</span></label>
|
|
414
|
+
</section>
|
|
415
|
+
<section class="face back">
|
|
416
|
+
${radios}
|
|
417
|
+
${WHATIS}
|
|
418
|
+
<div class="tabbar">${tabbar}</div>
|
|
419
|
+
<div class="panes">${panes}</div>
|
|
420
|
+
<label for="flip" class="flipbtn back-btn" data-tip="Flip back" aria-label="Flip back"><span aria-hidden="true">↺</span></label>
|
|
421
|
+
</section>
|
|
422
|
+
</div>
|
|
423
|
+
</main>
|
|
424
|
+
<p class="hint">${escapeHtml(name)} · business card · <label for="flip">flip it over</label></p>
|
|
425
|
+
</div>
|
|
426
|
+
${CARD_SCRIPT}
|
|
210
427
|
</body>
|
|
211
428
|
</html>
|
|
212
429
|
`;
|
|
213
430
|
}
|
|
214
431
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
432
|
+
* The card's only script — a progressive enhancement. Flip, tabs and the view
|
|
433
|
+
* toggle are CSS (checkbox and radio inputs) and work without it. It adds:
|
|
434
|
+
* Expand all / Collapse all for the context sections, opening the section a
|
|
435
|
+
* `#hash` names (flipping to the back and its Context tab first), and opening
|
|
436
|
+
* every section for printing (browsers disagree on printing a closed
|
|
437
|
+
* `<details>`).
|
|
220
438
|
*/
|
|
221
|
-
const
|
|
439
|
+
const CARD_SCRIPT = `<script>
|
|
222
440
|
(function(){
|
|
223
|
-
var btn=document.querySelector(".xall");
|
|
224
441
|
var secs=[].slice.call(document.querySelectorAll("details.ctx-section"));
|
|
225
|
-
|
|
226
|
-
var sync=function(){btn.textContent=secs.every(function(d){return d.open})?"Collapse all":"Expand all"};
|
|
227
|
-
btn.
|
|
228
|
-
|
|
229
|
-
var open=!secs.every(function(d){return d.open});
|
|
230
|
-
secs.forEach(function(d){d.
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
442
|
+
var btn=document.querySelector(".xall");
|
|
443
|
+
var sync=function(){if(btn)btn.textContent=secs.every(function(d){return d.open})?"Collapse all":"Expand all"};
|
|
444
|
+
if(btn&&secs.length){
|
|
445
|
+
btn.hidden=false;
|
|
446
|
+
btn.addEventListener("click",function(){var open=!secs.every(function(d){return d.open});secs.forEach(function(d){d.open=open});sync()});
|
|
447
|
+
secs.forEach(function(d){d.addEventListener("toggle",sync)});
|
|
448
|
+
}
|
|
449
|
+
var openHash=function(){
|
|
450
|
+
var d=document.getElementById(decodeURIComponent(location.hash.slice(1)));
|
|
451
|
+
if(!d||d.tagName!=="DETAILS")return;
|
|
452
|
+
var f=document.getElementById("flip"),t=document.getElementById("t-context");
|
|
453
|
+
if(f)f.checked=true;if(t)t.checked=true;d.open=true;sync();
|
|
454
|
+
setTimeout(function(){d.scrollIntoView({block:"nearest"})},620);
|
|
455
|
+
};
|
|
234
456
|
addEventListener("hashchange",openHash);openHash();
|
|
235
457
|
var pre=[];
|
|
236
458
|
addEventListener("beforeprint",function(){pre=secs.map(function(d){return d.open});secs.forEach(function(d){d.open=true})});
|
|
@@ -272,7 +494,10 @@ export function renderCardText(root, opts = {}) {
|
|
|
272
494
|
const meta = serverCardMeta();
|
|
273
495
|
const name = id?.displayName ?? id?.name ?? basename(resolve(root));
|
|
274
496
|
const plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
|
|
275
|
-
const out = [`### ${name} —
|
|
497
|
+
const out = [`### ${name} — business card`];
|
|
498
|
+
const title = cardTitle(id);
|
|
499
|
+
if (title)
|
|
500
|
+
out.push(`**${title}**`);
|
|
276
501
|
const pills = [
|
|
277
502
|
id?.vendor && id.vendor !== id.status ? id.vendor : null,
|
|
278
503
|
id?.agentVersion ? `v${id.agentVersion}` : null,
|
package/dist/server.js
CHANGED
|
@@ -57,12 +57,14 @@ function cardOptions(args) {
|
|
|
57
57
|
theme: (["light", "dark", "auto"].includes(theme) ? theme : "auto"),
|
|
58
58
|
accent: safeAccent(args.accent),
|
|
59
59
|
expanded: args.expanded === true || args.expanded === "true",
|
|
60
|
+
layout: (args.layout === "portrait" ? "portrait" : "landscape"),
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
const CARD_ARGS = {
|
|
63
64
|
theme: { type: "string", enum: ["light", "dark", "auto"], description: "default: auto" },
|
|
64
65
|
accent: { type: "string", description: "CSS hex colour, e.g. #FF702D (default: the AAIF palette)" },
|
|
65
66
|
expanded: { type: "boolean", description: "render every AGENTS.md section open (default: collapsed)" },
|
|
67
|
+
layout: { type: "string", enum: ["landscape", "portrait"], description: "business-card view (default: landscape)" },
|
|
66
68
|
};
|
|
67
69
|
/**
|
|
68
70
|
* @param root directory holding `AGENTS.md`, `project.fafm`, `.well-known/`.
|
|
@@ -276,7 +278,7 @@ export function createServer(root = ROOT, opts = {}) {
|
|
|
276
278
|
// resource. Both key forms, as the official ext-apps helper writes them.
|
|
277
279
|
_meta: { ui: { resourceUri: CARD_UI_URI }, "ui/resourceUri": CARD_UI_URI },
|
|
278
280
|
annotations: { title: "Render Context Card", readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
279
|
-
description: "Render the
|
|
281
|
+
description: "Render the project's business card as one self-contained HTML page a person can read or screenshot. Front: name, title, one-liner. Flip it for the back: About, Skills (when the .fafa lists any), Context (AGENTS.md), Memory, Discovery. layout:\"portrait\" for the tall view; expanded:true renders both faces flat with every section open. Also served at GET /card (?expand=all) over the HTTP transport.",
|
|
280
282
|
inputSchema: { type: "object", properties: CARD_ARGS },
|
|
281
283
|
},
|
|
282
284
|
{
|
package/dist/transport/http.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* transports in a map; this one deliberately does not.
|
|
12
12
|
*
|
|
13
13
|
* Alongside the MCP endpoint it serves the discovery documents (MCP Server
|
|
14
|
-
* Cards, SEP-2127 Final; AI Catalog 1.0):
|
|
14
|
+
* Cards, SEP-2127 Final; AI Catalog specVersion 1.0):
|
|
15
15
|
* GET /mcp/server-card — the Server Card (the spec's reserved
|
|
16
16
|
* `<streamable-http-url>/server-card`)
|
|
17
17
|
* GET /.well-known/mcp/server-card — the same card, the 1.x location (alias)
|
|
@@ -125,6 +125,7 @@ export function httpApp(root = ROOT, opts = {}) {
|
|
|
125
125
|
return c.body(renderCard(root, {
|
|
126
126
|
theme,
|
|
127
127
|
accent: safeAccent(q.accent),
|
|
128
|
+
layout: q.layout === "portrait" ? "portrait" : "landscape",
|
|
128
129
|
expanded: q.expand === "all",
|
|
129
130
|
// A card that may be read beyond this machine keeps memory private
|
|
130
131
|
// unless the project publishes it.
|
package/docs/MECHANISMS.md
CHANGED
|
@@ -32,7 +32,7 @@ Streamable HTTP endpoint at `<origin>/mcp` and its protocol versions):
|
|
|
32
32
|
{
|
|
33
33
|
"$schema": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
|
|
34
34
|
"name": "io.github.Wolfe-Jam/mcp-context-card",
|
|
35
|
-
"version": "1.
|
|
35
|
+
"version": "1.6.0",
|
|
36
36
|
"title": "MCP Context Card",
|
|
37
37
|
"description": "MCP server for a project's context (AGENTS.md), memory, and identity — base or drop-in extension.",
|
|
38
38
|
"websiteUrl": "https://github.com/Wolfe-Jam/mcp-context-card",
|
|
@@ -85,39 +85,52 @@ publisher lists artifacts, each entry keyed by its **media type** (`type`).
|
|
|
85
85
|
GET /.well-known/ai-catalog.json
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
Served from `https://ctx.example.com` (1.6.0, this repo's own files):
|
|
89
|
+
|
|
88
90
|
```jsonc
|
|
89
91
|
{
|
|
90
92
|
"specVersion": "1.0",
|
|
91
93
|
"host": { "displayName": "mcp-context-card", "identifier": "https://github.com/Wolfe-Jam/mcp-context-card" },
|
|
92
94
|
"entries": [
|
|
93
|
-
{
|
|
94
|
-
"
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
"description": "
|
|
98
|
-
"url": "
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
{ "identifier": "urn:air:faf.one:mcp:mcp-context-card", "type": "application/mcp-server-card+json",
|
|
96
|
+
"url": "https://ctx.example.com/mcp/server-card" },
|
|
97
|
+
{ "identifier": "urn:air:faf.one:context:mcp-context-card", "type": "text/markdown",
|
|
98
|
+
"displayName": "mcp-context-card — project context (AGENTS.md)",
|
|
99
|
+
"description": "Agent instructions for this project — 9 section(s): Setup, Build, Test, …",
|
|
100
|
+
"url": "https://ctx.example.com/AGENTS.md" },
|
|
101
|
+
{ "identifier": "urn:air:faf.one:identity:mcp-context-card", "type": "application/vnd.fafa+yaml",
|
|
102
|
+
"url": "https://ctx.example.com/.well-known/fafa",
|
|
103
|
+
"extensions": { "io.github.Wolfe-Jam.mcp-context-card": { "iana": "https://www.iana.org/assignments/media-types/application/vnd.fafa+yaml" } },
|
|
104
|
+
"…": "…" }
|
|
102
105
|
]
|
|
103
106
|
}
|
|
104
107
|
```
|
|
105
108
|
|
|
109
|
+
The static `.well-known/ai-catalog.json` is the same, with relative URLs and the
|
|
110
|
+
Server Card inline as `data`. Memory (`application/vnd.fafm+yaml`) is listed only
|
|
111
|
+
with `MCP_CONTEXT_CARD_PUBLISH_MEMORY=1`.
|
|
112
|
+
|
|
106
113
|
**Why it looks like this:**
|
|
107
114
|
|
|
115
|
+
- **The Server Card comes first** (`application/mcp-server-card+json`). It
|
|
116
|
+
carries its own name, description and version, so the entry repeats none.
|
|
108
117
|
- **`type` is the routing key.** A consumer scanning catalogs for
|
|
109
118
|
`text/markdown` context, or `application/vnd.fafm+yaml` memory, finds the
|
|
110
119
|
entry without knowing this publisher.
|
|
111
120
|
- **`identifier` is a `urn:air:` URN** scoped to the publisher
|
|
112
|
-
(`urn:air
|
|
121
|
+
(`urn:air:{domain}:{namespace}:{name}`). The domain is the one your `.fafa`
|
|
122
|
+
declares (`faf card init` writes it into `agent.id`); `faf.one` above is this
|
|
123
|
+
repo's. With no `.fafa` domain, the served catalog uses the serving host and
|
|
124
|
+
the static file uses plain IDs. No domain is ever invented. In ai-catalog's
|
|
113
125
|
[trust‑manifest ADRs](https://github.com/Agent-Card/ai-catalog/tree/main/adr),
|
|
114
|
-
`urn:air` identifiers carry a publisher‑domain‑aligned trust manifest
|
|
115
|
-
|
|
126
|
+
`urn:air` identifiers carry a publisher‑domain‑aligned trust manifest, so the
|
|
127
|
+
domain in the identifier is the one a publisher would sign with.
|
|
116
128
|
- **`description` is derived from real content** — the live AGENTS.md heading
|
|
117
129
|
list, the current fact count, the agent's own description — not a blurb that
|
|
118
130
|
drifts. See `buildCatalog()` in [`src/catalog-gen.ts`](../src/catalog-gen.ts).
|
|
119
|
-
- **`url` is
|
|
120
|
-
|
|
131
|
+
- **`url` is absolute when served** (against the request's origin, honouring a
|
|
132
|
+
reverse proxy's forwarded headers) and relative in the static file, so it also
|
|
133
|
+
resolves in a repo browser.
|
|
121
134
|
|
|
122
135
|
---
|
|
123
136
|
|