mcp-context-card 1.7.0 → 1.8.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 +1 -1
- package/AGENTS.md +5 -3
- package/CHANGELOG.md +72 -0
- package/README.md +26 -4
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +12 -3
- package/dist/card-app.bundle.js +103 -0
- package/dist/card-gen.js +15 -6
- package/dist/constants.d.ts +17 -1
- package/dist/constants.js +35 -1
- package/dist/render-card.d.ts +14 -43
- package/dist/render-card.js +96 -327
- package/dist/server-card.js +3 -2
- package/dist/server.d.ts +19 -3
- package/dist/server.js +88 -33
- package/dist/transport/http.js +23 -9
- package/docs/MECHANISMS.md +2 -2
- package/docs/TRANSPORT.md +76 -11
- package/docs/WIRING.md +6 -0
- package/docs/card-dark.html +5 -5
- package/docs/card-light.html +5 -5
- package/docs/card.html +5 -5
- package/docs/img/card-dark.png +0 -0
- package/docs/img/card-identity.png +0 -0
- package/docs/img/card-light.png +0 -0
- package/docs/index.html +5 -5
- package/package.json +10 -6
- package/project.faf +1 -1
- package/server.json +2 -2
package/dist/render-card.js
CHANGED
|
@@ -7,214 +7,24 @@
|
|
|
7
7
|
* ai-catalog; this is the view for people.
|
|
8
8
|
*
|
|
9
9
|
* Self-contained: inline CSS, no external fonts or resources. The only script
|
|
10
|
-
* is the expand-all / print helper
|
|
11
|
-
*
|
|
10
|
+
* in the plain card is the expand-all / print helper. renderAppsCard adds the
|
|
11
|
+
* MCP Apps helper (src/card-app.bundle.js) and the hidden inline view, and
|
|
12
|
+
* only ui://mcp-context-card/card.html uses that. Tool text, the saved file,
|
|
13
|
+
* GET /card and the CLI stay on renderCard. Renders anywhere.
|
|
12
14
|
*/
|
|
13
|
-
import {
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
14
18
|
import { parseAgentsMd } from "./agents-md.js";
|
|
15
19
|
import { parseFafm } from "./memory.js";
|
|
16
20
|
import { resolveIdentity, serverCardMeta, META_NS } from "./identity.js";
|
|
17
21
|
import { PUBLISH_MEMORY_ENV, SERVER_CARD_URI } from "./constants.js";
|
|
18
22
|
import { escapeHtml, renderInline, renderMarkdown, slug } from "./md.js";
|
|
23
|
+
import { cardInitials, renderBusinessCard } from "agent-business-card";
|
|
24
|
+
// The renderer lives in agent-business-card; these readers build the card from a project's files.
|
|
25
|
+
export { cardInitials, renderBusinessCard, safeAccent } from "agent-business-card";
|
|
19
26
|
/** AAIF brand orange (aaif.io): this project's own card colour, declared in its `.fafa`. */
|
|
20
27
|
export const AAIF_ACCENT = "#FF702D";
|
|
21
|
-
const ACCENT_OK = /^#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3}(?:[0-9a-fA-F]{2})?)?$/;
|
|
22
|
-
/** A valid hex colour, or undefined. Nothing else ever reaches the stylesheet. */
|
|
23
|
-
export function safeAccent(a) {
|
|
24
|
-
return a && ACCENT_OK.test(a) ? a : undefined;
|
|
25
|
-
}
|
|
26
|
-
/*
|
|
27
|
-
* No colour = ink. A card's colour is the owner's to declare (or the reader's
|
|
28
|
-
* to pass); nothing is guessed. Without one the accent is the card's own ink:
|
|
29
|
-
* black on a light card, white on a dark one, and the monogram's letter takes
|
|
30
|
-
* the card colour so it stays readable.
|
|
31
|
-
*/
|
|
32
|
-
const INK_CSS = `
|
|
33
|
-
:root{--accent:#0a0a0a}
|
|
34
|
-
@media (prefers-color-scheme:dark){:root{--accent:#fafafa}}
|
|
35
|
-
.bcard{--accent:var(--fg)}
|
|
36
|
-
.logo{color:var(--card)}
|
|
37
|
-
`;
|
|
38
|
-
const CSS = (accent) => `
|
|
39
|
-
/* The page and the card are themed independently. The page follows the
|
|
40
|
-
viewer's OS. The card is the owner's choice: body[data-theme] (light | dark;
|
|
41
|
-
none = follow the OS), and the reader can switch it with the ◐ toggle (#ink). */
|
|
42
|
-
:root{
|
|
43
|
-
${accent ? `--accent:${accent};` : ""}
|
|
44
|
-
--page-bg:#f4f4f5;--page-muted:#6b6b70;--page-line:rgba(0,0,0,.09);--page-chip:rgba(0,0,0,.05);
|
|
45
|
-
}
|
|
46
|
-
@media (prefers-color-scheme:dark){
|
|
47
|
-
:root{--page-bg:#000;--page-muted:#9a9aa0;--page-line:rgba(255,255,255,.13);--page-chip:rgba(255,255,255,.07)}
|
|
48
|
-
}
|
|
49
|
-
.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)}
|
|
50
|
-
body[data-theme="dark"] .bcard,
|
|
51
|
-
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)}
|
|
52
|
-
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)}
|
|
53
|
-
@media (prefers-color-scheme:dark){
|
|
54
|
-
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)}
|
|
55
|
-
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)}
|
|
56
|
-
}
|
|
57
|
-
@media (prefers-color-scheme:light){
|
|
58
|
-
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)}
|
|
59
|
-
}
|
|
60
|
-
.bcard{color:var(--fg)}
|
|
61
|
-
*{box-sizing:border-box}
|
|
62
|
-
body{margin:0;background:var(--page-bg);color:var(--page-muted);
|
|
63
|
-
font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
64
|
-
padding:40px 16px}
|
|
65
|
-
/* ── the business card: two faces, flip, two views ─────────────────── */
|
|
66
|
-
.sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
67
|
-
.stage{max-width:760px;margin:0 auto}
|
|
68
|
-
.views{display:flex;justify-content:flex-end;margin:0 0 10px}
|
|
69
|
-
.view,.hint label{cursor:pointer}
|
|
70
|
-
.view{font-size:.78rem;font-weight:600;padding:3px 11px;border-radius:20px;border:1px solid var(--page-line);
|
|
71
|
-
background:var(--page-chip);color:var(--page-muted);letter-spacing:.2em}
|
|
72
|
-
.views{gap:8px}
|
|
73
|
-
.view:hover,.hint label:hover{color:var(--accent);border-color:var(--accent)}
|
|
74
|
-
.bcard{margin:0 auto;width:100%;aspect-ratio:7/4;perspective:1600px}
|
|
75
|
-
#portrait:checked~.stage{max-width:480px}
|
|
76
|
-
#portrait:checked~.stage .bcard{aspect-ratio:4/7}
|
|
77
|
-
.faces{position:relative;width:100%;height:100%;transform-style:preserve-3d;transition:transform .6s ease}
|
|
78
|
-
#flip:checked~.stage .faces{transform:rotateY(180deg)}
|
|
79
|
-
.face{position:absolute;inset:0;background:var(--card);border:1px solid var(--line);border-radius:16px;
|
|
80
|
-
box-shadow:var(--card-shadow);backface-visibility:hidden;-webkit-backface-visibility:hidden;overflow:hidden}
|
|
81
|
-
.back{transform:rotateY(180deg) translateZ(1px);display:flex;flex-direction:column}
|
|
82
|
-
/* The face turned away takes no pointer or wheel events, so the visible face
|
|
83
|
-
scrolls (a hidden backface can still win hit-testing, notably in Chrome/Safari on macOS). */
|
|
84
|
-
#flip:checked~.stage .front,#flip:not(:checked)~.stage .back{pointer-events:none;visibility:hidden;transition:visibility 0s .3s}
|
|
85
|
-
#flip:checked~.stage .back,#flip:not(:checked)~.stage .front{visibility:visible;transition:visibility 0s .3s}
|
|
86
|
-
.panes{overscroll-behavior:contain;-webkit-overflow-scrolling:touch}
|
|
87
|
-
.front{display:flex;flex-direction:column;justify-content:center;padding:34px 44px;
|
|
88
|
-
border-top:5px solid var(--accent);
|
|
89
|
-
background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 10%,var(--card)) 0%,var(--card) 55%)}
|
|
90
|
-
.front .id{max-width:34em}
|
|
91
|
-
h1{margin:0;font-size:2.1rem;line-height:1.15;letter-spacing:-.025em}
|
|
92
|
-
.title{margin:8px 0 0;font-size:.82rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--accent)}
|
|
93
|
-
.oneliner{margin:16px 0 0;font-size:1.05rem;color:var(--muted);line-height:1.5}
|
|
94
|
-
.foot-front{position:absolute;left:44px;right:130px;bottom:22px;display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px}
|
|
95
|
-
.domain{font:.8rem ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--muted)}
|
|
96
|
-
.corner{position:absolute;width:30px;height:30px;border-radius:50%;display:grid;place-items:center;
|
|
97
|
-
cursor:pointer;font-weight:700;font-size:.9rem;color:var(--muted);background:var(--chip);border:1px solid var(--line);z-index:4}
|
|
98
|
-
.corner:hover{color:var(--accent);border-color:var(--accent)}
|
|
99
|
-
.corner.tr{top:16px;right:16px;font-family:Georgia,serif;font-style:italic}
|
|
100
|
-
.logo{position:absolute;top:34px;left:44px;width:84px;height:84px;border-radius:50%;display:grid;place-items:center;
|
|
101
|
-
color:#fff;font-weight:800;font-size:2rem;letter-spacing:-.02em;line-height:1;
|
|
102
|
-
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));
|
|
103
|
-
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)}
|
|
104
|
-
.front .id{margin-top:70px}
|
|
105
|
-
.corner.br{bottom:16px;right:16px}
|
|
106
|
-
.flipbtn{position:absolute;right:18px;bottom:16px;z-index:4;cursor:pointer;width:42px;height:42px;border-radius:50%;
|
|
107
|
-
display:grid;place-items:center;color:var(--fg);background:transparent;box-shadow:inset 0 0 0 2px var(--fg);
|
|
108
|
-
transition:transform .2s ease}
|
|
109
|
-
.flipbtn span{font-size:1.3rem;line-height:1;font-weight:700}
|
|
110
|
-
|
|
111
|
-
/* the back's button: same spot and outline as the front's; ↺ instead of ↻. Over
|
|
112
|
-
scrolling text it takes the card's colour, so the arrow stays readable. */
|
|
113
|
-
.flipbtn.back-btn{background:var(--card)}
|
|
114
|
-
/* fast tooltip: shows at once on hover or keyboard focus (a title tooltip waits ~1 s) */
|
|
115
|
-
.flipbtn[data-tip]::after{content:attr(data-tip);position:absolute;right:0;bottom:calc(100% + 8px);white-space:nowrap;
|
|
116
|
-
font:600 .74rem/1 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;letter-spacing:0;padding:6px 9px;border-radius:7px;
|
|
117
|
-
color:var(--card);background:var(--fg);opacity:0;transform:translateY(3px);pointer-events:none;transition:opacity .08s,transform .08s}
|
|
118
|
-
.flipbtn[data-tip]:hover::after,#flip:focus-visible~.stage .flipbtn[data-tip]::after{opacity:1;transform:none}
|
|
119
|
-
.flipbtn:hover{transform:none}.flipbtn:hover span{transform:rotate(-25deg)}.flipbtn span{transition:transform .2s ease}
|
|
120
|
-
.whatis{position:absolute;top:16px;right:16px;z-index:5}
|
|
121
|
-
.whatis>summary{position:static;list-style:none}
|
|
122
|
-
.whatis>summary::-webkit-details-marker{display:none}
|
|
123
|
-
.whatis[open]>summary{color:var(--accent);border-color:var(--accent)}
|
|
124
|
-
.whatis-panel{position:absolute;right:0;top:38px;width:min(320px,70vw);padding:14px 16px;border-radius:12px;
|
|
125
|
-
background:var(--card);border:1px solid var(--line);box-shadow:var(--card-shadow);font-size:.84rem;line-height:1.5}
|
|
126
|
-
.whatis-panel p{margin:0 0 8px}.whatis-head{font-weight:800;color:var(--accent);letter-spacing:-.01em;font-size:.95rem}
|
|
127
|
-
.whatis-foot{color:var(--muted);margin:0!important}.whatis-panel code{font:.86em ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
128
|
-
.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)}
|
|
129
|
-
.tabbar label{cursor:pointer;font-size:.8rem;font-weight:600;color:var(--muted);padding:6px 11px;border-radius:8px 8px 0 0}
|
|
130
|
-
.tabbar label:hover{color:var(--fg)}
|
|
131
|
-
.panes{flex:1;overflow:auto;padding:20px 26px 72px}
|
|
132
|
-
.pane{display:none}
|
|
133
|
-
.lead{margin:0 0 14px;font-size:1rem;line-height:1.6}
|
|
134
|
-
.kv{border-collapse:collapse;font-size:.86rem;width:100%}
|
|
135
|
-
.kv th{text-align:left;font-weight:600;color:var(--muted);padding:6px 14px 6px 0;white-space:nowrap;vertical-align:top}
|
|
136
|
-
.kv td{padding:6px 0;border-bottom:1px solid var(--line)}
|
|
137
|
-
.kv code,.lead code{font:.86em ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
138
|
-
.hint{text-align:center;color:var(--page-muted);font-size:.78rem;margin:14px 0 0}
|
|
139
|
-
.hint label{color:var(--accent)}
|
|
140
|
-
#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}
|
|
141
|
-
@media (max-width:640px){
|
|
142
|
-
.bcard,#portrait:checked~.stage .bcard{aspect-ratio:auto;height:min(78vh,720px)}
|
|
143
|
-
.front{padding:28px 26px}.foot-front{left:26px}
|
|
144
|
-
.logo{top:24px;left:26px;width:64px;height:64px;font-size:1.5rem}.front .id{margin-top:56px}
|
|
145
|
-
h1{font-size:1.7rem}
|
|
146
|
-
}
|
|
147
|
-
/* flat: expanded render and print — both faces, every tab, no flip */
|
|
148
|
-
.flat .faces,.flat .face{position:static;transform:none!important;height:auto}
|
|
149
|
-
.flat .bcard{aspect-ratio:auto;perspective:none}
|
|
150
|
-
.flat .face{margin:0 0 18px}.flat .front{min-height:260px;position:relative}
|
|
151
|
-
.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}
|
|
152
|
-
.flat .panes{overflow:visible}
|
|
153
|
-
@media print{
|
|
154
|
-
.faces,.face{position:static!important;transform:none!important;height:auto!important}
|
|
155
|
-
.bcard{aspect-ratio:auto!important}.face{margin:0 0 18px;box-shadow:none}
|
|
156
|
-
.pane{display:block!important;margin:0 0 22px}.tabbar,.views,.hint,.corner,.flipbtn,.whatis{display:none!important}
|
|
157
|
-
.panes{overflow:visible}
|
|
158
|
-
}
|
|
159
|
-
.pills{display:flex;flex-wrap:wrap;gap:6px}
|
|
160
|
-
.pill{font-size:.74rem;font-weight:600;padding:3px 9px;border-radius:20px;background:var(--chip);color:var(--muted)}
|
|
161
|
-
.pill.accent{background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent)}
|
|
162
|
-
.label{font-size:.7rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase;
|
|
163
|
-
color:var(--accent);margin:0 0 14px}
|
|
164
|
-
.toc{display:flex;flex-wrap:wrap;gap:6px 14px;margin:0;padding:0;list-style:none}
|
|
165
|
-
.toc a{font-size:.82rem;color:var(--muted);text-decoration:none}
|
|
166
|
-
.toc a:hover{color:var(--accent)}
|
|
167
|
-
/* ── collapsible context ─────────────────────────────────────────── */
|
|
168
|
-
.ctx-nav{position:sticky;top:-20px;z-index:3;background:var(--card);
|
|
169
|
-
margin:0 -26px 16px;padding:11px 26px;border-bottom:1px solid var(--line);
|
|
170
|
-
display:flex;flex-wrap:wrap;align-items:flex-start;gap:8px 16px}
|
|
171
|
-
.xall{margin-left:auto;flex:none;font:inherit;font-size:.76rem;font-weight:600;
|
|
172
|
-
white-space:nowrap;padding:3px 11px;border-radius:20px;border:1px solid var(--line);
|
|
173
|
-
background:var(--chip);color:var(--muted);cursor:pointer}
|
|
174
|
-
.xall:hover{color:var(--accent);border-color:var(--accent)}
|
|
175
|
-
.ctx-preamble{padding-bottom:4px}
|
|
176
|
-
details.ctx-section{border-top:1px solid var(--line)}
|
|
177
|
-
details.ctx-section>summary{cursor:pointer;list-style:none;padding:11px 0;
|
|
178
|
-
font-weight:600;font-size:1rem;letter-spacing:-.01em;display:flex;gap:9px}
|
|
179
|
-
details.ctx-section>summary::-webkit-details-marker{display:none}
|
|
180
|
-
details.ctx-section>summary::before{content:"›";color:var(--accent);font-weight:700;
|
|
181
|
-
transition:transform .15s ease}
|
|
182
|
-
details.ctx-section[open]>summary::before{transform:rotate(90deg)}
|
|
183
|
-
details.ctx-section>.md{padding:0 0 16px}
|
|
184
|
-
@media print{
|
|
185
|
-
.ctx-nav{display:none}
|
|
186
|
-
details.ctx-section:not([open])>.md{display:block!important}
|
|
187
|
-
details.ctx-section>summary::before{content:""}
|
|
188
|
-
}
|
|
189
|
-
.md h1,.md h2,.md h3,.md h4{margin:22px 0 8px;font-size:1rem;letter-spacing:-.01em}
|
|
190
|
-
.md h1{font-size:1.15rem}
|
|
191
|
-
.md p{margin:8px 0}
|
|
192
|
-
.md ul,.md ol{margin:8px 0;padding-left:22px}
|
|
193
|
-
.md li{margin:3px 0}
|
|
194
|
-
.md code{background:var(--chip);padding:1px 5px;border-radius:5px;
|
|
195
|
-
font:.86em ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
196
|
-
.md pre{background:var(--chip);padding:14px 16px;border-radius:9px;overflow:auto}
|
|
197
|
-
.md pre code{background:none;padding:0}
|
|
198
|
-
.md table{border-collapse:collapse;width:100%;margin:12px 0;font-size:.88rem;display:block;overflow:auto}
|
|
199
|
-
.md th,.md td{border:1px solid var(--line);padding:6px 10px;text-align:left}
|
|
200
|
-
.md blockquote{margin:10px 0;padding-left:14px;border-left:3px solid var(--line);color:var(--muted)}
|
|
201
|
-
.md a{color:var(--accent)}
|
|
202
|
-
.fact{padding:12px 0;border-bottom:1px solid var(--line)}
|
|
203
|
-
.fact:last-child{border-bottom:0}
|
|
204
|
-
.fact p{margin:0 0 7px}
|
|
205
|
-
.meta{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
|
|
206
|
-
.tag{font-size:.72rem;padding:2px 8px;border-radius:5px;background:var(--chip);color:var(--muted)}
|
|
207
|
-
.dot{width:7px;height:7px;border-radius:50%;background:var(--accent);display:inline-block}
|
|
208
|
-
.dot.pending{background:var(--muted)}
|
|
209
|
-
.disc{width:100%;border-collapse:collapse;font-size:.84rem}
|
|
210
|
-
.disc th,.disc td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--line)}
|
|
211
|
-
.disc th{color:var(--muted);font-weight:600}
|
|
212
|
-
.disc code{font:.86em ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--muted)}
|
|
213
|
-
.fetch{margin:14px 0 0;font-size:.82rem;color:var(--muted)}
|
|
214
|
-
.fetch code{background:var(--chip);padding:1px 5px;border-radius:5px}
|
|
215
|
-
.none{color:var(--muted);font-style:italic}
|
|
216
|
-
`;
|
|
217
|
-
const htmlAttr = (theme) => theme === "auto" ? "" : ` data-theme="${theme}"`;
|
|
218
28
|
/** What the front calls this thing, from where it runs: "MCP server", "A2A agent", or both. */
|
|
219
29
|
export function cardKind(id) {
|
|
220
30
|
const kinds = [];
|
|
@@ -229,28 +39,6 @@ export function cardKind(id) {
|
|
|
229
39
|
export function cardTitle(id) {
|
|
230
40
|
return [cardKind(id).join(" · "), id?.agentVersion ? `v${id.agentVersion}` : ""].filter(Boolean).join(" · ");
|
|
231
41
|
}
|
|
232
|
-
/** The front's logo: the first letters of the name's first two words ("mcp-context-card" → "MC"). */
|
|
233
|
-
/** Words that say what kind of thing it is, not whose: never the logo ("mcp-context-card" → C, not M). */
|
|
234
|
-
const GENERIC_WORDS = new Set(["mcp", "server", "servers", "sdk", "agent", "agents", "api", "cli", "app", "tool", "tools", "plugin", "core", "the"]);
|
|
235
|
-
/** Names whose two-letter mark is obvious. */
|
|
236
|
-
const OBVIOUS_MARKS = { github: "GH", gitlab: "GL", typescript: "TS", javascript: "JS" };
|
|
237
|
-
/**
|
|
238
|
-
* The front's logo: the first letter of the name's first meaningful word
|
|
239
|
-
* (generic words like "mcp", "server", "sdk" skipped), plus a second letter
|
|
240
|
-
* only when it's obvious: a known name (github → GH, typescript → TS) or a
|
|
241
|
-
* word written with two capitals (GitHub, TypeScript). Never more than two.
|
|
242
|
-
*/
|
|
243
|
-
export function cardInitials(name) {
|
|
244
|
-
const words = name.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
245
|
-
const first = words.find((w) => !GENERIC_WORDS.has(w.toLowerCase())) ?? words[0] ?? "";
|
|
246
|
-
const known = OBVIOUS_MARKS[first.toLowerCase()];
|
|
247
|
-
if (known)
|
|
248
|
-
return known;
|
|
249
|
-
const capitals = first.match(/\p{Lu}/gu) ?? [];
|
|
250
|
-
if (capitals.length === 2 && /^\p{Lu}/u.test(first) && /\p{Ll}/u.test(first))
|
|
251
|
-
return capitals.join("");
|
|
252
|
-
return (Array.from(first)[0] ?? "").toUpperCase();
|
|
253
|
-
}
|
|
254
42
|
/** The front's one-liner: the first sentence of the description. */
|
|
255
43
|
export function cardOneLiner(id, max = 140) {
|
|
256
44
|
const d = id?.description;
|
|
@@ -264,10 +52,95 @@ export function cardOneLiner(id, max = 140) {
|
|
|
264
52
|
const pause = /\s[—–]\s|;\s|:\s/.exec(d);
|
|
265
53
|
return pause && pause.index >= 40 && pause.index <= max ? d.slice(0, pause.index).trim() : first;
|
|
266
54
|
}
|
|
267
|
-
/**
|
|
55
|
+
/** MCP Apps helper, bundled at build time. Same bytes in src/ and, after build, dist/. */
|
|
56
|
+
const CARD_APP_JS = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "card-app.bundle.js"), "utf8");
|
|
57
|
+
/**
|
|
58
|
+
* A plain card (tool text, the saved file, GET /card, the CLI, docs/) must
|
|
59
|
+
* not exceed this. The ui:// resource is separate and is not checked here.
|
|
60
|
+
*/
|
|
61
|
+
export const PLAIN_CARD_LIMIT_BYTES = 64 * 1024;
|
|
62
|
+
/** True when a plain card exceeds 64 KB. card:check fails on that. */
|
|
63
|
+
export function plainCardExceedsLimit(html) {
|
|
64
|
+
return Buffer.byteLength(html) > PLAIN_CARD_LIMIT_BYTES;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Add the bundled helper as its own inline script, after the card's script.
|
|
68
|
+
* Its own element, so an error in the card's script (a malformed URL hash)
|
|
69
|
+
* can't stop the helper. The page is already the full card; the helper is
|
|
70
|
+
* only for an MCP Apps host.
|
|
71
|
+
*/
|
|
72
|
+
function withCardApp(html) {
|
|
73
|
+
const close = html.lastIndexOf("</script>");
|
|
74
|
+
if (close === -1)
|
|
75
|
+
return html;
|
|
76
|
+
const end = close + "</script>".length;
|
|
77
|
+
return `${html.slice(0, end)}\n<script data-card-app>\n${CARD_APP_JS}\n</script>${html.slice(end)}`;
|
|
78
|
+
}
|
|
79
|
+
/** The same sections, facts and identity the full card is built from. */
|
|
80
|
+
function inlineCounts(root) {
|
|
81
|
+
const agents = parseAgentsMd(join(root, "AGENTS.md"));
|
|
82
|
+
const mem = parseFafm(join(root, "project.fafm"));
|
|
83
|
+
const id = resolveIdentity(root);
|
|
84
|
+
return {
|
|
85
|
+
sections: agents?.sections.filter((s) => s.level > 1).length ?? 0,
|
|
86
|
+
facts: mem.facts.length,
|
|
87
|
+
identity: id !== null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function countLabel(n, word) {
|
|
91
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The inline view: monogram, name, kind · version, one line, a counts strip,
|
|
95
|
+
* and one action. Hidden until a host's display mode is inline or pip.
|
|
96
|
+
* No host, and the script off, keep the full card.
|
|
97
|
+
*/
|
|
98
|
+
function inlineView(card, counts) {
|
|
99
|
+
const kind = card.title ? `<p class="kind">${escapeHtml(card.title)}</p>` : "";
|
|
100
|
+
const line = card.oneLiner ? `<p class="line">${escapeHtml(card.oneLiner)}</p>` : "";
|
|
101
|
+
const strip = `${countLabel(counts.sections, "section")} · ${countLabel(counts.facts, "fact")} · ${counts.identity ? "identity" : "no identity"}`;
|
|
102
|
+
return `<style>
|
|
103
|
+
#inline-view{--iv-fg:#0a0a0a;--iv-bg:#fff;--iv-muted:#6b6b70;--iv-line:rgba(0,0,0,.12);color:var(--iv-fg);background:var(--iv-bg);border:1px solid var(--iv-line);border-top:4px solid var(--accent,#FF702D);border-radius:14px;padding:12px 14px;font:14px/1.35 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
104
|
+
body[data-theme="dark"] #inline-view{--iv-fg:#fafafa;--iv-bg:#0d0d0d;--iv-muted:#9a9aa0;--iv-line:rgba(255,255,255,.16)}
|
|
105
|
+
@media (prefers-color-scheme:dark){body:not([data-theme]) #inline-view{--iv-fg:#fafafa;--iv-bg:#0d0d0d;--iv-muted:#9a9aa0;--iv-line:rgba(255,255,255,.16)}}
|
|
106
|
+
#inline-view .row{display:flex;gap:12px;align-items:center}
|
|
107
|
+
#inline-view .mark{width:36px;height:36px;border-radius:50%;display:grid;place-items:center;flex:none;color:#fff;font-weight:800;background:var(--accent,#FF702D)}
|
|
108
|
+
#inline-view h2{margin:0;font-size:1.05rem;line-height:1.2;letter-spacing:-.02em}
|
|
109
|
+
#inline-view .kind{margin:2px 0 0;font-size:.72rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--accent,#FF702D)}
|
|
110
|
+
#inline-view .line{margin:8px 0 0;color:var(--iv-muted)}
|
|
111
|
+
#inline-view .counts{margin:8px 0 0;font-size:.78rem;color:var(--iv-muted)}
|
|
112
|
+
#inline-view .open{margin-top:10px;font:inherit;font-size:.85rem;font-weight:650;padding:6px 12px;border-radius:999px;border:1px solid var(--accent,#FF702D);background:transparent;color:var(--accent,#FF702D);cursor:pointer}
|
|
113
|
+
#inline-view .saved{margin:8px 0 0;font-size:.8rem;color:var(--iv-muted)}
|
|
114
|
+
html[data-view="inline"] body{padding:8px;background:transparent}
|
|
115
|
+
html[data-view="inline"] body>:not(#inline-view){display:none}
|
|
116
|
+
</style>
|
|
117
|
+
<div class="row"><div class="mark" aria-hidden="true">${escapeHtml(cardInitials(card.name))}</div><div><h2>${escapeHtml(card.name)}</h2>${kind}</div></div>
|
|
118
|
+
${line}
|
|
119
|
+
<p class="counts">${strip}</p>
|
|
120
|
+
<button type="button" id="open-full-card" class="open">Open full card</button>
|
|
121
|
+
<p id="inline-saved" class="saved" hidden>This host keeps the inline view. Ask for <code>save_context_card</code> to write the full card to <code>context-card.html</code>.</p>`;
|
|
122
|
+
}
|
|
123
|
+
/** Drop the hidden inline view at the top of <body>. The full card stays the page. */
|
|
124
|
+
function withInlineView(html, block) {
|
|
125
|
+
const body = /<body[^>]*>/.exec(html);
|
|
126
|
+
if (!body || body.index === undefined)
|
|
127
|
+
return html;
|
|
128
|
+
const at = body.index + body[0].length;
|
|
129
|
+
return `${html.slice(0, at)}\n<!-- inline-view -->\n<div id="inline-view" hidden>\n${block}\n</div>\n<!-- /inline-view -->${html.slice(at)}`;
|
|
130
|
+
}
|
|
131
|
+
/** The project's card: read its files and draw. No helper, no inline view. */
|
|
268
132
|
export function renderCard(root, opts = {}) {
|
|
269
133
|
return renderBusinessCard(readCard(root, opts), opts);
|
|
270
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* ui://mcp-context-card/card.html — the same card, plus the hidden inline
|
|
137
|
+
* view and the bundled MCP Apps helper. Nothing else calls this.
|
|
138
|
+
*/
|
|
139
|
+
export function renderAppsCard(root, opts = {}) {
|
|
140
|
+
const card = readCard(root, opts);
|
|
141
|
+
const drawn = withInlineView(renderBusinessCard(card, opts), inlineView(card, inlineCounts(root)));
|
|
142
|
+
return withCardApp(drawn);
|
|
143
|
+
}
|
|
271
144
|
/** The (i) panel for an agent or server card. */
|
|
272
145
|
const AGENT_ABOUT = `<p class="whatis-head">Business Cards for Agents</p>
|
|
273
146
|
<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>
|
|
@@ -384,110 +257,6 @@ export function readCard(root, opts = {}) {
|
|
|
384
257
|
...(id?.accent ? { accent: id.accent } : {}),
|
|
385
258
|
};
|
|
386
259
|
}
|
|
387
|
-
/** Draw any business card as one self-contained HTML page. */
|
|
388
|
-
export function renderBusinessCard(card, opts = {}) {
|
|
389
|
-
const theme = opts.theme ?? "auto";
|
|
390
|
-
const accent = safeAccent(opts.accent) ?? safeAccent(card.accent);
|
|
391
|
-
const portrait = opts.layout === "portrait";
|
|
392
|
-
const flat = !!opts.expanded;
|
|
393
|
-
const { name, title, oneLiner, domain } = card;
|
|
394
|
-
const tabs = card.tabs.map((t) => ({ ...t, key: t.key.replace(/[^a-z0-9-]/gi, "").toLowerCase() || "tab" }));
|
|
395
|
-
const pills = card.chips
|
|
396
|
-
.map((c) => `<span class="pill${c.accent ? " accent" : ""}">${escapeHtml(c.text)}</span>`)
|
|
397
|
-
.join("");
|
|
398
|
-
const radios = tabs
|
|
399
|
-
.map((t, i) => `<input type="radio" name="tab" id="t-${t.key}" class="sr"${i === 0 ? " checked" : ""}>`)
|
|
400
|
-
.join("");
|
|
401
|
-
const tabbar = tabs.map((t) => `<label for="t-${t.key}">${escapeHtml(t.label)}</label>`).join("");
|
|
402
|
-
const panes = tabs
|
|
403
|
-
.map((t) => `<div class="pane p-${t.key}"><p class="label">${escapeHtml(t.heading)}</p>${t.html}</div>`)
|
|
404
|
-
.join("");
|
|
405
|
-
// One CSS rule pair per tab: its pane shows, its label lights, when its radio is checked.
|
|
406
|
-
const tabCss = tabs
|
|
407
|
-
.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)}`)
|
|
408
|
-
.join("");
|
|
409
|
-
// The (i) panel: the same control, in the same spot, on both faces.
|
|
410
|
-
const WHATIS = `<details class="whatis">
|
|
411
|
-
<summary class="corner tr" title="About this card" aria-label="About this card">i</summary>
|
|
412
|
-
<div class="whatis-panel">
|
|
413
|
-
${card.about}
|
|
414
|
-
</div>
|
|
415
|
-
</details>`;
|
|
416
|
-
return `<!doctype html>
|
|
417
|
-
<html lang="en">
|
|
418
|
-
<head>
|
|
419
|
-
<meta charset="utf-8">
|
|
420
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
421
|
-
<title>${escapeHtml(name)} — business card</title>
|
|
422
|
-
<style>${CSS(accent)}${accent ? "" : INK_CSS}${tabCss}</style>
|
|
423
|
-
</head>
|
|
424
|
-
<body${flat ? ` class="flat"` : ""}${htmlAttr(theme)}>
|
|
425
|
-
<input type="checkbox" id="flip" class="sr" aria-label="Flip the card">
|
|
426
|
-
<input type="checkbox" id="ink" class="sr" aria-label="Switch the card between light and dark">
|
|
427
|
-
<input type="checkbox" id="portrait" class="sr" aria-label="Portrait view"${portrait ? " checked" : ""}>
|
|
428
|
-
<div class="stage">
|
|
429
|
-
<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>
|
|
430
|
-
<main class="bcard">
|
|
431
|
-
<div class="faces">
|
|
432
|
-
<section class="face front">
|
|
433
|
-
<div class="logo" aria-hidden="true">${escapeHtml(cardInitials(name))}</div>
|
|
434
|
-
${WHATIS}
|
|
435
|
-
<div class="id">
|
|
436
|
-
<h1>${escapeHtml(name)}</h1>
|
|
437
|
-
${title ? `<p class="title">${escapeHtml(title)}</p>` : ""}
|
|
438
|
-
${oneLiner ? `<p class="oneliner">${escapeHtml(oneLiner)}</p>` : ""}
|
|
439
|
-
</div>
|
|
440
|
-
<div class="foot-front">${domain ? `<span class="domain">${escapeHtml(domain)}</span>` : ""}${pills ? `<div class="pills">${pills}</div>` : ""}</div>
|
|
441
|
-
<label for="flip" class="flipbtn" data-tip="Flip the card" aria-label="Flip the card"><span aria-hidden="true">↻</span></label>
|
|
442
|
-
</section>
|
|
443
|
-
<section class="face back">
|
|
444
|
-
${radios}
|
|
445
|
-
${WHATIS}
|
|
446
|
-
<div class="tabbar">${tabbar}</div>
|
|
447
|
-
<div class="panes">${panes}</div>
|
|
448
|
-
<label for="flip" class="flipbtn back-btn" data-tip="Flip back" aria-label="Flip back"><span aria-hidden="true">↺</span></label>
|
|
449
|
-
</section>
|
|
450
|
-
</div>
|
|
451
|
-
</main>
|
|
452
|
-
<p class="hint">${escapeHtml(name)} · business card · <label for="flip">flip it over</label></p>
|
|
453
|
-
</div>
|
|
454
|
-
${CARD_SCRIPT}
|
|
455
|
-
</body>
|
|
456
|
-
</html>
|
|
457
|
-
`;
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* The card's only script — a progressive enhancement. Flip, tabs and the view
|
|
461
|
-
* toggle are CSS (checkbox and radio inputs) and work without it. It adds:
|
|
462
|
-
* Expand all / Collapse all for the context sections, opening the section a
|
|
463
|
-
* `#hash` names (flipping to the back and its Context tab first), and opening
|
|
464
|
-
* every section for printing (browsers disagree on printing a closed
|
|
465
|
-
* `<details>`).
|
|
466
|
-
*/
|
|
467
|
-
const CARD_SCRIPT = `<script>
|
|
468
|
-
(function(){
|
|
469
|
-
var secs=[].slice.call(document.querySelectorAll("details.ctx-section"));
|
|
470
|
-
var btn=document.querySelector(".xall");
|
|
471
|
-
var sync=function(){if(btn)btn.textContent=secs.every(function(d){return d.open})?"Collapse all":"Expand all"};
|
|
472
|
-
if(btn&&secs.length){
|
|
473
|
-
btn.hidden=false;
|
|
474
|
-
btn.addEventListener("click",function(){var open=!secs.every(function(d){return d.open});secs.forEach(function(d){d.open=open});sync()});
|
|
475
|
-
secs.forEach(function(d){d.addEventListener("toggle",sync)});
|
|
476
|
-
}
|
|
477
|
-
var openHash=function(){
|
|
478
|
-
var d=document.getElementById(decodeURIComponent(location.hash.slice(1)));
|
|
479
|
-
if(!d||d.tagName!=="DETAILS")return;
|
|
480
|
-
var f=document.getElementById("flip"),t=document.getElementById("t-context");
|
|
481
|
-
if(f)f.checked=true;if(t)t.checked=true;d.open=true;sync();
|
|
482
|
-
setTimeout(function(){d.scrollIntoView({block:"nearest"})},620);
|
|
483
|
-
};
|
|
484
|
-
addEventListener("hashchange",openHash);openHash();
|
|
485
|
-
var pre=[];
|
|
486
|
-
addEventListener("beforeprint",function(){pre=secs.map(function(d){return d.open});secs.forEach(function(d){d.open=true})});
|
|
487
|
-
addEventListener("afterprint",function(){secs.forEach(function(d,i){d.open=pre[i]});sync()});
|
|
488
|
-
sync();
|
|
489
|
-
})();
|
|
490
|
-
</script>`;
|
|
491
260
|
const TLDR_FACTS = 5;
|
|
492
261
|
const TLDR_CHARS = 160;
|
|
493
262
|
/**
|
package/dist/server-card.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* `name`, `title` and `version` are the same values the live connection reports
|
|
11
11
|
* in `serverInfo`, so the card never contradicts runtime.
|
|
12
12
|
*/
|
|
13
|
-
import { SUPPORTED_PROTOCOL_VERSIONS } from "@modelcontextprotocol/
|
|
13
|
+
import { SUPPORTED_PROTOCOL_VERSIONS } from "@modelcontextprotocol/server";
|
|
14
|
+
import { MODERN_PROTOCOL_VERSIONS } from "./constants.js";
|
|
14
15
|
import { DESCRIPTION, MCP_PATH, REGISTRY_NAME, REPOSITORY_URL, SERVER_CARD_SCHEMA, TITLE, VERSION, } from "./constants.js";
|
|
15
16
|
import { serverCardMeta } from "./identity.js";
|
|
16
17
|
export function serverCard(opts = {}) {
|
|
@@ -28,7 +29,7 @@ export function serverCard(opts = {}) {
|
|
|
28
29
|
{
|
|
29
30
|
type: "streamable-http",
|
|
30
31
|
url: `${opts.origin}${MCP_PATH}`,
|
|
31
|
-
supportedProtocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
|
|
32
|
+
supportedProtocolVersions: [...MODERN_PROTOCOL_VERSIONS, ...SUPPORTED_PROTOCOL_VERSIONS],
|
|
32
33
|
},
|
|
33
34
|
],
|
|
34
35
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -13,10 +13,15 @@
|
|
|
13
13
|
* and `GET /.well-known/mcp/server-card` (out of band, http transport).
|
|
14
14
|
* 2. ai-catalog — `GET /.well-known/ai-catalog.json`, three sibling entries
|
|
15
15
|
* keyed by media type (see catalog-gen.ts).
|
|
16
|
+
*
|
|
17
|
+
* Both protocol eras are served: 2026-07-28 (`server/discover`, a `_meta`
|
|
18
|
+
* envelope on every request) and 2025 (`initialize`). See docs/TRANSPORT.md.
|
|
16
19
|
*/
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
20
|
+
import { type StdioServerHandle } from "@modelcontextprotocol/server/stdio";
|
|
21
|
+
import { Server } from "@modelcontextprotocol/server";
|
|
22
|
+
import type { ProtocolEra, Transport } from "@modelcontextprotocol/server";
|
|
19
23
|
export { NAME, VERSION, SERVER_CARD_URI } from "./constants.js";
|
|
24
|
+
import { type CardApp } from "./constants.js";
|
|
20
25
|
/** Default package root — `dist/` at runtime, `src/` under tsx. Both are one up. */
|
|
21
26
|
export declare const ROOT: string;
|
|
22
27
|
/**
|
|
@@ -40,7 +45,18 @@ export interface ServerOptions {
|
|
|
40
45
|
detectRoot?: boolean;
|
|
41
46
|
/** The directory the server was started in (default: process.cwd()). */
|
|
42
47
|
cwd?: string;
|
|
48
|
+
/** What the ui:// card carries: `helper` (MCP Apps helper, the default) or
|
|
49
|
+
* `plain`. Default: MCP_CONTEXT_CARD_APP, else `helper`. */
|
|
50
|
+
cardApp?: CardApp;
|
|
51
|
+
/** The protocol era this instance serves, set by the stdio/HTTP factory:
|
|
52
|
+
* "modern" = 2026-07-28, "legacy" = 2024-10-07 … 2025-11-25 (the default). */
|
|
53
|
+
era?: ProtocolEra;
|
|
43
54
|
}
|
|
44
55
|
export declare function createServer(root?: string, opts?: ServerOptions): Server;
|
|
45
|
-
/**
|
|
56
|
+
/** Serve over stdio, both protocol eras: a 2026-07-28 client connects with
|
|
57
|
+
* `server/discover`, a 2025 client with `initialize`. The factory builds one
|
|
58
|
+
* instance per connection, for that connection's era. */
|
|
59
|
+
export declare function serveStdioServer(root?: string, opts?: ServerOptions): StdioServerHandle;
|
|
60
|
+
/** Connect a server instance to a transport you supply. 2025 era only (the
|
|
61
|
+
* transport decides the wire); for both eras use `serveStdioServer` or `httpApp`. */
|
|
46
62
|
export declare function serve(transport: Transport, root?: string, opts?: ServerOptions): Promise<Server>;
|