mcp-context-card 1.9.2 → 1.9.4

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.
@@ -11,7 +11,7 @@
11
11
  "data": {
12
12
  "$schema": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
13
13
  "name": "io.github.Wolfe-Jam/mcp-context-card",
14
- "version": "1.9.2",
14
+ "version": "1.9.4",
15
15
  "title": "MCP Context Card",
16
16
  "description": "MCP server for a project's context (AGENTS.md), memory, and identity — base or drop-in extension.",
17
17
  "websiteUrl": "https://github.com/Wolfe-Jam/mcp-context-card",
package/.well-known/fafa CHANGED
@@ -13,7 +13,7 @@ agent:
13
13
  # a deployment for your own project uses your domain.
14
14
  id: "urn:air:faf.one:agent:mcp-context-card"
15
15
  vendor: "io.github.Wolfe-Jam"
16
- version: "1.9.2"
16
+ version: "1.9.4"
17
17
  description: >-
18
18
  The essential MCP components for a project's context (AGENTS.md),
19
19
  cross-session memory, and identity — a base MCP on its own, or a
package/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  All notable changes to this project. Adheres to [Semantic Versioning](https://semver.org).
4
4
 
5
+ ## 1.9.4
6
+
7
+ - **Say "show context card".** One prompt, `show`, asks the model to show the
8
+ card; clients that list MCP prompts offer it as a command (Claude Code:
9
+ `/context-card:show`). The model picks the surface: drawn in the chat by
10
+ MCP Apps hosts, Markdown elsewhere. Naming one changes it: *as markdown*,
11
+ *in the browser*.
12
+ - **Seeing the card no longer writes a file.** The instructions sent a client
13
+ without MCP Apps to `save_context_card`, which writes `context-card.html`.
14
+ They now send it to `render_context_card`, which has returned the Markdown
15
+ card there since 1.9.0; `save_context_card` is for when the user asks for
16
+ the browser or a file. The instructions also ask for nothing or one line
17
+ after a drawn card, so the model doesn't repeat it.
18
+ - **The instructions changed for the first time since 1.8.0**, after a goose
19
+ desktop 1.57 load check: 1.52 had stopped loading a build whose
20
+ instructions changed; 1.57 loads one. A test pins the new text.
21
+
22
+ ## 1.9.3
23
+
24
+ From outside reviews of #91 and #94 (goose and claude):
25
+
26
+ - **A malformed root never fails a call.** On 2026-07-28 the client's roots
27
+ arrive unvalidated; a non-string or missing `uri`, or a `null` entry, made
28
+ `list_context_sources` (and every tool that resolves the project) fail.
29
+ Every entry is now checked by one function, `rootPath`, on both eras.
30
+ - **`..` is refused in both forms.** It was only refused in a bare path;
31
+ `file:///a/../b` and `%2e%2e` slipped through as the URL parser had already
32
+ resolved them.
33
+ - **UNC and device paths are refused** (`\\host\share`, `//host/share`,
34
+ `\\?\…`, a `file://` URI with a host on Windows) before anything touches
35
+ the filesystem, so a root can't make Windows contact another machine.
36
+ - **Smaller:** at most 64 roots are looked at; `FILE://` is read like
37
+ `file://`; a trailing slash is dropped; `format` and `detail` are read
38
+ case- and space-insensitively (`" Markdown "`, `"FULL"`).
39
+ - **Tests that fail without each guard**, a pure `rootPath` table that runs on
40
+ the Windows CI leg too, the 2026-07-28 path with malformed entries, and
41
+ `format` over HTTP. The roots schema is typed, so the cast is gone.
42
+
5
43
  ## 1.9.2
6
44
 
7
45
  - **The Cursor editor's project is found.** Cursor answers `roots/list` with
package/README.md CHANGED
@@ -22,6 +22,13 @@ title, one-liner. Flip it for the back: About, Context, Memory, Discovery.
22
22
  npx mcp-context-card card
23
23
  ```
24
24
 
25
+ **In your AI client**, once the server is added ([Add it to your setup](#add-it-to-your-setup)), say
26
+ **show context card**. The AI picks where it appears: drawn in the chat by
27
+ hosts that support MCP Apps, as Markdown elsewhere. Name a place to choose:
28
+ *show context card as markdown*, *show context card in the browser*. Clients
29
+ that list MCP prompts also offer it as the prompt `show`; in Claude Code type
30
+ `/context-card:show`.
31
+
25
32
  **context** — the project's `AGENTS.md`, served whole or one section at a time.
26
33
 
27
34
  ![the back's Context tab — AGENTS.md, section nav, read_agents_md](./docs/img/card-context.png)
@@ -61,7 +68,7 @@ say what it is. `mcp-context-card` is those three, done once:
61
68
  gains context, memory, and identity discovery it didn't have. Nothing to
62
69
  migrate; it composes.
63
70
 
64
- Ten tools, two discovery surfaces already in the ecosystem (Server Card
71
+ Ten tools, one prompt (`show`), two discovery surfaces already in the ecosystem (Server Card
65
72
  `_meta`, `ai-catalog.json`), and a rendered [card](#the-card). MIT, on npm.
66
73
 
67
74
  It composes:
@@ -127,15 +134,15 @@ whose host supports [MCP Apps](https://modelcontextprotocol.io/extensions/apps),
127
134
  the host draws the card (the mini card in a panel) and the model gets a short
128
135
  line. Anywhere else, a terminal for one, the reply is the text card: the card
129
136
  as Markdown. Ask for it as text and `format: "markdown"` gives the text card in
130
- any chat. HTML goes only to a browser: `save_context_card` writes
137
+ any chat (an MCP Apps host still draws the card too). HTML goes only to a browser: `save_context_card` writes
131
138
  `context-card.html` into the project, opens it in your browser when the
132
139
  server runs locally, and replies with the card as Markdown (identity, the
133
140
  `AGENTS.md` sections, memory, discovery), a link to the full card, and its
134
141
  address to copy. The Markdown is a tl;dr by default: five facts, each cut to
135
142
  its first sentence as stored, so it stays small however much a project
136
- remembers. `detail: "full"` lists every fact whole. For a client without
137
- MCP Apps, the server's instructions point the model at `save_context_card`
138
- when the user wants to see the card.
143
+ remembers. `detail: "full"` lists every fact whole. The server's
144
+ instructions send *show context card* to `render_context_card`, and use
145
+ `save_context_card` only when the user asks for the browser or a file.
139
146
 
140
147
  Light, dark, or auto. The colour is the owner's: set it in the project's
141
148
  `.fafa` (`metadata.cards.accent: "#0066cc"`), or pass any hex with `--accent`
@@ -287,6 +294,9 @@ Seven tools only read. `remember` and `forget` write the memory file, and
287
294
  destructive and a host can ask before running them. Every tool carries a
288
295
  title and MCP tool annotations.
289
296
 
297
+ One prompt, `show`, asks the model to show the context card. Clients that
298
+ list MCP prompts offer it as a command.
299
+
290
300
  ## The demo
291
301
 
292
302
  `npm run demo` walks through context, memory, identity and discovery, live,
@@ -300,7 +310,7 @@ over both transports:
300
310
  4. **Discovery** — `list_context_sources()`, then the same server over stateless
301
311
  HTTP with its `.well-known` routes and `GET /card`.
302
312
 
303
- 254 tests on Linux, macOS, and Windows, coverage‑gated in CI.
313
+ 258 tests on Linux, macOS, and Windows, coverage‑gated in CI.
304
314
  `npm run wjttc` runs the WJTTC certification suite (seven tiers, from protocol
305
315
  and Server Card conformance to stdio/HTTP parity and the shipped package). Two spawn a real
306
316
  child process and check a remembered fact survives the restart — one against
package/dist/bin.d.ts CHANGED
@@ -8,7 +8,7 @@ export interface Launch {
8
8
  root: string;
9
9
  }
10
10
  /** what a bare `--help` / `help` prints. */
11
- export declare const HELP = "mcp-context-card 1.9.2\nServe a project's context (AGENTS.md), memory, and identity over MCP.\n\nUSAGE\n mcp-context-card stdio MCP server \u2014 what an MCP host spawns (default)\n mcp-context-card --http stateless Streamable HTTP on PORT (default 3000)\n mcp-context-card --stdio force stdio even when PORT is set\n mcp-context-card card this dir's context card \u2014 opens it in your browser\n at a terminal; HTML to stdout when piped ( > f.html )\n --theme light|dark --accent #hex\n --expanded (all sections open) --portrait --stdout\n mcp-context-card --help this text\n mcp-context-card --version print version\n\nENV\n MCP_CONTEXT_CARD_ROOT read AGENTS.md / project.fafm / .well-known/ from here\n PORT if set, run HTTP instead of stdio\n HOST HTTP bind address (default 127.0.0.1, this machine\n only); 0.0.0.0 exposes it, e.g. in a container\n MCP_CONTEXT_CARD_ALLOWED_HOSTS / _ALLOWED_ORIGINS\n comma-separated Host names / browser origins to\n accept besides this machine's own (reverse proxy, web app)\n MCP_CONTEXT_CARD_PUBLISH_MEMORY=1\n HTTP: serve project.fafm, list it in the AI Catalog,\n and show its facts on an exposed /card (off: memory\n is session data). HTTP has no auth\n\nA bare run is an stdio server: it waits for a host to speak JSON-RPC on stdin,\nso it looks idle at a terminal. Try `card` (opens your context in a browser) or `--http`.\nhttps://github.com/Wolfe-Jam/mcp-context-card\n";
11
+ export declare const HELP = "mcp-context-card 1.9.4\nServe a project's context (AGENTS.md), memory, and identity over MCP.\n\nUSAGE\n mcp-context-card stdio MCP server \u2014 what an MCP host spawns (default)\n mcp-context-card --http stateless Streamable HTTP on PORT (default 3000)\n mcp-context-card --stdio force stdio even when PORT is set\n mcp-context-card card this dir's context card \u2014 opens it in your browser\n at a terminal; HTML to stdout when piped ( > f.html )\n --theme light|dark --accent #hex\n --expanded (all sections open) --portrait --stdout\n mcp-context-card --help this text\n mcp-context-card --version print version\n\nENV\n MCP_CONTEXT_CARD_ROOT read AGENTS.md / project.fafm / .well-known/ from here\n PORT if set, run HTTP instead of stdio\n HOST HTTP bind address (default 127.0.0.1, this machine\n only); 0.0.0.0 exposes it, e.g. in a container\n MCP_CONTEXT_CARD_ALLOWED_HOSTS / _ALLOWED_ORIGINS\n comma-separated Host names / browser origins to\n accept besides this machine's own (reverse proxy, web app)\n MCP_CONTEXT_CARD_PUBLISH_MEMORY=1\n HTTP: serve project.fafm, list it in the AI Catalog,\n and show its facts on an exposed /card (off: memory\n is session data). HTTP has no auth\n\nA bare run is an stdio server: it waits for a host to speak JSON-RPC on stdin,\nso it looks idle at a terminal. Try `card` (opens your context in a browser) or `--http`.\nhttps://github.com/Wolfe-Jam/mcp-context-card\n";
12
12
  /**
13
13
  * The address the HTTP server binds: `HOST`, else 127.0.0.1. Local by default,
14
14
  * as the MCP transports spec recommends; a hosted deploy sets HOST=0.0.0.0.
@@ -100,4 +100,4 @@ Boolean requesting whether a visible border and background is provided by the ho
100
100
  - omitted: host decides border`)}),RA=p.object({method:p.literal("ui/request-display-mode"),params:p.object({mode:Tc.describe("The display mode being requested.")})}),vE=p.object({mode:Tc.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),_E=p.union([p.literal("model"),p.literal("app")]).describe("Tool visibility scope - who can access the tool."),PA=p.object({resourceUri:p.string().optional(),visibility:p.array(_E).optional().describe(`Who can access this tool. Default: ["model", "app"]
101
101
  - "model": Tool visible to and callable by the agent
102
102
  - "app": Tool callable by the app from this server only`),csp:p.never().optional(),permissions:p.never().optional()}),TA=p.object({mimeTypes:p.array(p.string()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')}),EA=p.object({method:p.literal("ui/download-file"),params:p.object({contents:p.array(p.union([zn,wn])).describe("Resource contents to download \u2014 embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})}),OA=p.object({method:p.literal("ui/message"),params:p.object({role:p.literal("user").describe('Message role, currently only "user" is supported.'),content:p.array(Ht).describe("Message content blocks (text, image, etc.).")})}),CA=p.object({method:p.literal("ui/notifications/sandbox-resource-ready"),params:p.object({html:p.string().describe("HTML content to load into the inner iframe."),sandbox:p.string().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:jv.optional().describe("CSP configuration from resource metadata."),permissions:Av.optional().describe("Sandbox permissions from resource metadata.")})}),SE=p.object({method:p.literal("ui/notifications/tool-result"),params:jr.describe("Standard MCP tool execution result.")}),az=p.object({toolInfo:p.object({id:wt.optional().describe("JSON-RPC id of the tools/call request."),tool:Nr.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:rE.optional().describe("Current color theme preference."),styles:mE.optional().describe("Style configuration for theming the app."),displayMode:Tc.optional().describe("How the UI is currently displayed."),availableDisplayModes:p.array(Tc).optional().describe("Display modes the host supports."),containerDimensions:p.union([p.object({height:p.number().describe("Fixed container height in pixels.")}),p.object({maxHeight:p.union([p.number(),p.undefined()]).optional().describe("Maximum container height in pixels.")})]).and(p.union([p.object({width:p.number().describe("Fixed container width in pixels.")}),p.object({maxWidth:p.union([p.number(),p.undefined()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
103
- container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:p.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:p.string().optional().describe("User's timezone in IANA format."),userAgent:p.string().optional().describe("Host application identifier."),platform:p.union([p.literal("web"),p.literal("desktop"),p.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:p.object({touch:p.boolean().optional().describe("Whether the device supports touch input."),hover:p.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:p.object({top:p.number().describe("Top safe area inset in pixels."),right:p.number().describe("Right safe area inset in pixels."),bottom:p.number().describe("Bottom safe area inset in pixels."),left:p.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),bE=p.object({method:p.literal("ui/notifications/host-context-changed"),params:az.describe("Partial context update containing only changed fields.")}),NA=p.object({method:p.literal("ui/update-model-context"),params:p.object({content:p.array(Ht).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:p.record(p.string(),p.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})}),jA=p.object({method:p.literal("ui/initialize"),params:p.object({appInfo:Ft.describe("App identification (name and version)."),appCapabilities:gE.describe("Features and capabilities this app provides."),protocolVersion:p.string().describe("Protocol version this app supports.")})}),yE=p.object({protocolVersion:p.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ft.describe("Host application identification and version."),hostCapabilities:hE.describe("Features and capabilities provided by the host."),hostContext:az.describe("Rich context about the host environment.")}).passthrough(),$E={target:"draft-2020-12"};async function iz(e,t){let n=e["~standard"];if(n.jsonSchema)return n.jsonSchema[t]($E);throw Error(`Schema (vendor: ${n.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function oz(e,t,n=""){let o=await e["~standard"].validate(t);if(o.issues){let r=o.issues.map(i=>{let a=i.path?.map(s=>typeof s=="object"?s.key:s).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(n+r)}return o.value}function zE(e,t){let n={...e,...t};return t.experimental!==void 0&&(n.experimental={...e.experimental,...t.experimental}),t.tools!==void 0&&(n.tools={...e.tools,...t.tools}),n}var Ud=class e extends Rc{_appInfo;_appCapabilities;options;_hostCapabilities;_hostInfo;_hostContext;_registeredTools={};_initializedSent=!1;_registeredEvents=new Set;_events=new Ov;_methods=new Cv;setRequestHandler=(...t)=>{let[n,...o]=rz("request",t)??t;this._methods.claim(n,"setRequestHandler"),super.setRequestHandler.call(this,n,...o)};setNotificationHandler=(...t)=>{let[n,...o]=rz("notification",t)??t;this._methods.claim(n,"setNotificationHandler"),super.setNotificationHandler.call(this,n,...o)};removeRequestHandler=t=>{this._methods.release(t),super.removeRequestHandler(t)};removeNotificationHandler=t=>{this._methods.release(t),super.removeNotificationHandler(t)};replaceRequestHandler=(t,...n)=>{this._methods.replace(t),super.setRequestHandler.call(this,t,...n)};_assertInitialized(t){if(this._initializedSent)return;let n=`[ext-apps] App.${t}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if(this.options?.strict)throw Error(n);console.warn(`${n}. This will throw in a future release.`)}eventSchemas={toolinput:cE,toolinputpartial:lE,toolresult:SE,toolcancelled:uE,hostcontextchanged:bE};static ONE_SHOT_EVENTS=new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]);_everHadListener=new Set;_assertHandlerTiming(t){if(!e.ONE_SHOT_EVENTS.has(t)||this._everHadListener.has(t)||(this._everHadListener.add(t),!this._initializedSent))return;let n=`[ext-apps] "${String(t)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if(this.options?.strict)throw Error(n);console.warn(n)}setEventHandler(t,n){n&&this._assertHandlerTiming(t),this._ensureEventSlot(t),this._events.setHandler(t,n)}addEventListener(t,n){this._assertHandlerTiming(t),this._ensureEventSlot(t),this._events.addEventListener(t,n)}removeEventListener(t,n){this._events.removeEventListener(t,n)}_ensureEventSlot(t){if(this._registeredEvents.has(t))return;let n=this.eventSchemas[t];if(!n)throw Error(`Unknown event: ${String(t)}`);this.setNotificationHandler(n.shape.method.value,{params:n.shape.params},o=>{let r=o;t==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...r}),this._events.dispatch(t,r)}),this._registeredEvents.add(t)}getEventHandler(t){return this._events.getHandler(t)}warnIfRequestHandlerReplaced(t,n,o){n&&o&&console.warn(`[MCP Apps] ${t} handler replaced. Previous handler will no longer be called.`)}constructor(t,n={},o={autoResize:!0}){super(o),this._appInfo=t,this._appCapabilities=n,this.options=o,this._methods.replace("notifications/cancelled"),this._methods.replace("notifications/progress"),!o.allowUnsafeEval&&p.config({jitless:!0}),this.replaceRequestHandler("ping",r=>(console.log("Received ping:",r.params),{})),this.setEventHandler("hostcontextchanged",void 0)}buildContext(t){return t}registerAppCapabilities(t){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._appCapabilities=zE(this._appCapabilities,t)}registerTool(t,n,o){if(this._registeredTools[t])throw Error(`Tool ${t} is already registered`);let r=this,i=()=>{r._initializedSent&&r._appCapabilities.tools?.listChanged&&r.sendToolListChanged()},a=n.inputSchema!==void 0,s={title:n.title,description:n.description,inputSchema:n.inputSchema,outputSchema:n.outputSchema,annotations:n.annotations,_meta:n._meta,enabled:!0,enable(){this.enabled=!0,i()},disable(){this.enabled=!1,i()},update(c){Object.assign(this,c),i()},remove(){r._registeredTools[t]===s&&(delete r._registeredTools[t],i())},handler:async(c,l)=>{if(!s.enabled)throw Error(`Tool ${t} is disabled`);let d;if(a){let m=s.inputSchema,f=m?await oz(m,c??{},`Invalid input for tool ${t}: `):c??{};d=await o(f,l)}else d=await o(l);return s.outputSchema&&!d.isError&&(d.structuredContent=await oz(s.outputSchema,d.structuredContent,`Invalid output for tool ${t}: `)),d}};return this._registeredTools[t]=s,!this._appCapabilities.tools&&!this.transport&&this.registerAppCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),i(),s}_toolHandlersInitialized=!1;ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(t,n)=>{let o=this._registeredTools[t.name];if(!o)throw Error(`Tool ${t.name} not found`);return o.handler(t.arguments,n)},this.onlisttools=async(t,n)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([o,r])=>r.enabled).map(async([o,r])=>{let i={name:o,title:r.title,description:r.description,inputSchema:r.inputSchema?await iz(r.inputSchema,"input"):{type:"object",properties:{}}};return r.outputSchema&&(i.outputSchema=await iz(r.outputSchema,"output")),r.annotations&&(i.annotations=r.annotations),r._meta&&(i._meta=r._meta),i}))}))}async sendToolListChanged(t={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:t})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(t){this.setEventHandler("toolinput",t)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(t){this.setEventHandler("toolinputpartial",t)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(t){this.setEventHandler("toolresult",t)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(t){this.setEventHandler("toolcancelled",t)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(t){this.setEventHandler("hostcontextchanged",t)}_onteardown;get onteardown(){return this._onteardown}set onteardown(t){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,t),this._onteardown=t,this.replaceRequestHandler("ui/resource-teardown",{params:pE.shape.params,result:fE},(n,o)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(n,o)})}_oncalltool;get oncalltool(){return this._oncalltool}set oncalltool(t){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,t),this._oncalltool=t,this.replaceRequestHandler("tools/call",(n,o)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(n.params,o)})}_onlisttools;get onlisttools(){return this._onlisttools}set onlisttools(t){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,t),this._onlisttools=t,this.replaceRequestHandler("tools/list",(n,o)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(n.params,o)})}assertCapabilityForMethod(t){switch(t){case"sampling/createMessage":if(!this._hostCapabilities?.sampling)throw Error(`Host does not support sampling (required for ${t})`);break}}assertRequestHandlerCapability(t){switch(t){case"tools/call":case"tools/list":if(!this._appCapabilities.tools)throw Error(`Client does not support tool capability (required for ${t})`);return;default:return}}assertNotificationCapability(t){}async callServerTool(t,n){if(this._assertInitialized("callServerTool"),typeof t=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${t}"). Did you mean: callServerTool({ name: "${t}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:t},{onprogress:()=>{},resetTimeoutOnProgress:!0,...n})}async readServerResource(t,n){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:t},n)}async listServerResources(t,n){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:t},n)}async createSamplingMessage(t,n){return this._assertInitialized("createSamplingMessage"),await this.request({method:"sampling/createMessage",params:t},n)}sendMessage(t,n){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:t},sE,n)}sendLog(t){return this.notification({method:"notifications/message",params:t})}updateModelContext(t,n){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:t},Or,n)}openLink(t,n){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:t},oE,n)}sendOpenLink=this.openLink;downloadFile(t,n){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:t},aE,n)}requestTeardown(t={}){return this.notification({method:"ui/notifications/request-teardown",params:t})}requestDisplayMode(t,n){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:t},vE,n)}sendSizeChanged(t){return this.notification({method:"ui/notifications/size-changed",params:t})}setupSizeChangedNotifications(){let t=!1,n=0,o=0,r=()=>{t||(t=!0,requestAnimationFrame(()=>{t=!1;let a=document.documentElement,s=a.style.height;a.style.height="max-content";let c=Math.ceil(a.getBoundingClientRect().height);a.style.height=s;let l=Math.ceil(window.innerWidth);(l!==n||c!==o)&&(n=l,o=c,this.sendSizeChanged({width:l,height:c}))}))};r();let i=new ResizeObserver(r);return i.observe(document.documentElement),i.observe(document.body),()=>i.disconnect()}async connect(t=new Nv(window.parent,window.parent),n){if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1;try{await super.connect(t);let o=await this.request({method:"ui/initialize",params:{appCapabilities:this._appCapabilities,appInfo:this._appInfo,protocolVersion:eE}},yE,n);if(o===void 0)throw Error(`Server sent invalid initialize result: ${o}`);this._hostCapabilities=o.hostCapabilities,this._hostInfo=o.hostInfo,this._hostContext=o.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,this.options?.autoResize&&this.setupSizeChangedNotifications()}catch(o){throw this.close(),o}}};function wE(e){return e==="inline"||e==="pip"}function Dd(e){let t=document.getElementById("inline-view");if(!t)return;let n=document.documentElement;if(wE(e?.displayMode))n.dataset.view="inline",t.hidden=!1;else{delete n.dataset.view,t.hidden=!0;let a=document.getElementById("inline-saved");a&&(a.hidden=!0)}let r=e?.theme,i=document.body;i?.dataset&&(r==="light"||r==="dark")&&(i.dataset.theme=r)}function Uv(){let e=document.getElementById("inline-saved");e&&(e.hidden=!1)}async function kE(e){if(!e.getHostContext()?.availableDisplayModes?.includes("fullscreen")){Uv();return}try{if((await e.requestDisplayMode({mode:"fullscreen"})).mode!=="fullscreen"){Uv(),Dd(e.getHostContext());return}let o=document.getElementById("inline-saved");o&&(o.hidden=!0),Dd({...e.getHostContext()??{},displayMode:"fullscreen"})}catch{Uv()}}var IE=window.parent;if(IE!==window){let e=new Ud({name:"mcp-context-card",version:"1.9.2"},{availableDisplayModes:["inline","pip","fullscreen"]},{autoResize:!0});e.addEventListener("hostcontextchanged",()=>{Dd(e.getHostContext())}),document.getElementById("open-full-card")?.addEventListener("click",()=>{kE(e)}),e.connect().then(()=>Dd(e.getHostContext())).catch(()=>{})}})();
103
+ container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:p.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:p.string().optional().describe("User's timezone in IANA format."),userAgent:p.string().optional().describe("Host application identifier."),platform:p.union([p.literal("web"),p.literal("desktop"),p.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:p.object({touch:p.boolean().optional().describe("Whether the device supports touch input."),hover:p.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:p.object({top:p.number().describe("Top safe area inset in pixels."),right:p.number().describe("Right safe area inset in pixels."),bottom:p.number().describe("Bottom safe area inset in pixels."),left:p.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),bE=p.object({method:p.literal("ui/notifications/host-context-changed"),params:az.describe("Partial context update containing only changed fields.")}),NA=p.object({method:p.literal("ui/update-model-context"),params:p.object({content:p.array(Ht).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:p.record(p.string(),p.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})}),jA=p.object({method:p.literal("ui/initialize"),params:p.object({appInfo:Ft.describe("App identification (name and version)."),appCapabilities:gE.describe("Features and capabilities this app provides."),protocolVersion:p.string().describe("Protocol version this app supports.")})}),yE=p.object({protocolVersion:p.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ft.describe("Host application identification and version."),hostCapabilities:hE.describe("Features and capabilities provided by the host."),hostContext:az.describe("Rich context about the host environment.")}).passthrough(),$E={target:"draft-2020-12"};async function iz(e,t){let n=e["~standard"];if(n.jsonSchema)return n.jsonSchema[t]($E);throw Error(`Schema (vendor: ${n.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function oz(e,t,n=""){let o=await e["~standard"].validate(t);if(o.issues){let r=o.issues.map(i=>{let a=i.path?.map(s=>typeof s=="object"?s.key:s).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(n+r)}return o.value}function zE(e,t){let n={...e,...t};return t.experimental!==void 0&&(n.experimental={...e.experimental,...t.experimental}),t.tools!==void 0&&(n.tools={...e.tools,...t.tools}),n}var Ud=class e extends Rc{_appInfo;_appCapabilities;options;_hostCapabilities;_hostInfo;_hostContext;_registeredTools={};_initializedSent=!1;_registeredEvents=new Set;_events=new Ov;_methods=new Cv;setRequestHandler=(...t)=>{let[n,...o]=rz("request",t)??t;this._methods.claim(n,"setRequestHandler"),super.setRequestHandler.call(this,n,...o)};setNotificationHandler=(...t)=>{let[n,...o]=rz("notification",t)??t;this._methods.claim(n,"setNotificationHandler"),super.setNotificationHandler.call(this,n,...o)};removeRequestHandler=t=>{this._methods.release(t),super.removeRequestHandler(t)};removeNotificationHandler=t=>{this._methods.release(t),super.removeNotificationHandler(t)};replaceRequestHandler=(t,...n)=>{this._methods.replace(t),super.setRequestHandler.call(this,t,...n)};_assertInitialized(t){if(this._initializedSent)return;let n=`[ext-apps] App.${t}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if(this.options?.strict)throw Error(n);console.warn(`${n}. This will throw in a future release.`)}eventSchemas={toolinput:cE,toolinputpartial:lE,toolresult:SE,toolcancelled:uE,hostcontextchanged:bE};static ONE_SHOT_EVENTS=new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]);_everHadListener=new Set;_assertHandlerTiming(t){if(!e.ONE_SHOT_EVENTS.has(t)||this._everHadListener.has(t)||(this._everHadListener.add(t),!this._initializedSent))return;let n=`[ext-apps] "${String(t)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if(this.options?.strict)throw Error(n);console.warn(n)}setEventHandler(t,n){n&&this._assertHandlerTiming(t),this._ensureEventSlot(t),this._events.setHandler(t,n)}addEventListener(t,n){this._assertHandlerTiming(t),this._ensureEventSlot(t),this._events.addEventListener(t,n)}removeEventListener(t,n){this._events.removeEventListener(t,n)}_ensureEventSlot(t){if(this._registeredEvents.has(t))return;let n=this.eventSchemas[t];if(!n)throw Error(`Unknown event: ${String(t)}`);this.setNotificationHandler(n.shape.method.value,{params:n.shape.params},o=>{let r=o;t==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...r}),this._events.dispatch(t,r)}),this._registeredEvents.add(t)}getEventHandler(t){return this._events.getHandler(t)}warnIfRequestHandlerReplaced(t,n,o){n&&o&&console.warn(`[MCP Apps] ${t} handler replaced. Previous handler will no longer be called.`)}constructor(t,n={},o={autoResize:!0}){super(o),this._appInfo=t,this._appCapabilities=n,this.options=o,this._methods.replace("notifications/cancelled"),this._methods.replace("notifications/progress"),!o.allowUnsafeEval&&p.config({jitless:!0}),this.replaceRequestHandler("ping",r=>(console.log("Received ping:",r.params),{})),this.setEventHandler("hostcontextchanged",void 0)}buildContext(t){return t}registerAppCapabilities(t){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._appCapabilities=zE(this._appCapabilities,t)}registerTool(t,n,o){if(this._registeredTools[t])throw Error(`Tool ${t} is already registered`);let r=this,i=()=>{r._initializedSent&&r._appCapabilities.tools?.listChanged&&r.sendToolListChanged()},a=n.inputSchema!==void 0,s={title:n.title,description:n.description,inputSchema:n.inputSchema,outputSchema:n.outputSchema,annotations:n.annotations,_meta:n._meta,enabled:!0,enable(){this.enabled=!0,i()},disable(){this.enabled=!1,i()},update(c){Object.assign(this,c),i()},remove(){r._registeredTools[t]===s&&(delete r._registeredTools[t],i())},handler:async(c,l)=>{if(!s.enabled)throw Error(`Tool ${t} is disabled`);let d;if(a){let m=s.inputSchema,f=m?await oz(m,c??{},`Invalid input for tool ${t}: `):c??{};d=await o(f,l)}else d=await o(l);return s.outputSchema&&!d.isError&&(d.structuredContent=await oz(s.outputSchema,d.structuredContent,`Invalid output for tool ${t}: `)),d}};return this._registeredTools[t]=s,!this._appCapabilities.tools&&!this.transport&&this.registerAppCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),i(),s}_toolHandlersInitialized=!1;ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(t,n)=>{let o=this._registeredTools[t.name];if(!o)throw Error(`Tool ${t.name} not found`);return o.handler(t.arguments,n)},this.onlisttools=async(t,n)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([o,r])=>r.enabled).map(async([o,r])=>{let i={name:o,title:r.title,description:r.description,inputSchema:r.inputSchema?await iz(r.inputSchema,"input"):{type:"object",properties:{}}};return r.outputSchema&&(i.outputSchema=await iz(r.outputSchema,"output")),r.annotations&&(i.annotations=r.annotations),r._meta&&(i._meta=r._meta),i}))}))}async sendToolListChanged(t={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:t})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(t){this.setEventHandler("toolinput",t)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(t){this.setEventHandler("toolinputpartial",t)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(t){this.setEventHandler("toolresult",t)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(t){this.setEventHandler("toolcancelled",t)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(t){this.setEventHandler("hostcontextchanged",t)}_onteardown;get onteardown(){return this._onteardown}set onteardown(t){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,t),this._onteardown=t,this.replaceRequestHandler("ui/resource-teardown",{params:pE.shape.params,result:fE},(n,o)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(n,o)})}_oncalltool;get oncalltool(){return this._oncalltool}set oncalltool(t){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,t),this._oncalltool=t,this.replaceRequestHandler("tools/call",(n,o)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(n.params,o)})}_onlisttools;get onlisttools(){return this._onlisttools}set onlisttools(t){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,t),this._onlisttools=t,this.replaceRequestHandler("tools/list",(n,o)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(n.params,o)})}assertCapabilityForMethod(t){switch(t){case"sampling/createMessage":if(!this._hostCapabilities?.sampling)throw Error(`Host does not support sampling (required for ${t})`);break}}assertRequestHandlerCapability(t){switch(t){case"tools/call":case"tools/list":if(!this._appCapabilities.tools)throw Error(`Client does not support tool capability (required for ${t})`);return;default:return}}assertNotificationCapability(t){}async callServerTool(t,n){if(this._assertInitialized("callServerTool"),typeof t=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${t}"). Did you mean: callServerTool({ name: "${t}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:t},{onprogress:()=>{},resetTimeoutOnProgress:!0,...n})}async readServerResource(t,n){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:t},n)}async listServerResources(t,n){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:t},n)}async createSamplingMessage(t,n){return this._assertInitialized("createSamplingMessage"),await this.request({method:"sampling/createMessage",params:t},n)}sendMessage(t,n){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:t},sE,n)}sendLog(t){return this.notification({method:"notifications/message",params:t})}updateModelContext(t,n){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:t},Or,n)}openLink(t,n){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:t},oE,n)}sendOpenLink=this.openLink;downloadFile(t,n){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:t},aE,n)}requestTeardown(t={}){return this.notification({method:"ui/notifications/request-teardown",params:t})}requestDisplayMode(t,n){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:t},vE,n)}sendSizeChanged(t){return this.notification({method:"ui/notifications/size-changed",params:t})}setupSizeChangedNotifications(){let t=!1,n=0,o=0,r=()=>{t||(t=!0,requestAnimationFrame(()=>{t=!1;let a=document.documentElement,s=a.style.height;a.style.height="max-content";let c=Math.ceil(a.getBoundingClientRect().height);a.style.height=s;let l=Math.ceil(window.innerWidth);(l!==n||c!==o)&&(n=l,o=c,this.sendSizeChanged({width:l,height:c}))}))};r();let i=new ResizeObserver(r);return i.observe(document.documentElement),i.observe(document.body),()=>i.disconnect()}async connect(t=new Nv(window.parent,window.parent),n){if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1;try{await super.connect(t);let o=await this.request({method:"ui/initialize",params:{appCapabilities:this._appCapabilities,appInfo:this._appInfo,protocolVersion:eE}},yE,n);if(o===void 0)throw Error(`Server sent invalid initialize result: ${o}`);this._hostCapabilities=o.hostCapabilities,this._hostInfo=o.hostInfo,this._hostContext=o.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,this.options?.autoResize&&this.setupSizeChangedNotifications()}catch(o){throw this.close(),o}}};function wE(e){return e==="inline"||e==="pip"}function Dd(e){let t=document.getElementById("inline-view");if(!t)return;let n=document.documentElement;if(wE(e?.displayMode))n.dataset.view="inline",t.hidden=!1;else{delete n.dataset.view,t.hidden=!0;let a=document.getElementById("inline-saved");a&&(a.hidden=!0)}let r=e?.theme,i=document.body;i?.dataset&&(r==="light"||r==="dark")&&(i.dataset.theme=r)}function Uv(){let e=document.getElementById("inline-saved");e&&(e.hidden=!1)}async function kE(e){if(!e.getHostContext()?.availableDisplayModes?.includes("fullscreen")){Uv();return}try{if((await e.requestDisplayMode({mode:"fullscreen"})).mode!=="fullscreen"){Uv(),Dd(e.getHostContext());return}let o=document.getElementById("inline-saved");o&&(o.hidden=!0),Dd({...e.getHostContext()??{},displayMode:"fullscreen"})}catch{Uv()}}var IE=window.parent;if(IE!==window){let e=new Ud({name:"mcp-context-card",version:"1.9.4"},{availableDisplayModes:["inline","pip","fullscreen"]},{autoResize:!0});e.addEventListener("hostcontextchanged",()=>{Dd(e.getHostContext())}),document.getElementById("open-full-card")?.addEventListener("click",()=>{kE(e)}),e.connect().then(()=>Dd(e.getHostContext())).catch(()=>{})}})();
@@ -1,7 +1,7 @@
1
1
  /** Server identity constants, in their own module so any file can import
2
2
  * them without pulling in the whole server. */
3
3
  export declare const NAME = "mcp-context-card";
4
- export declare const VERSION = "1.9.2";
4
+ export declare const VERSION = "1.9.4";
5
5
  export declare const SERVER_CARD_URI = "mcp-context-card://server-card";
6
6
  /** The server's registry identity: reverse-DNS, the same as server.json `name`.
7
7
  * It is also `serverInfo.name`, so a Server Card never contradicts the live
package/dist/constants.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** Server identity constants, in their own module so any file can import
2
2
  * them without pulling in the whole server. */
3
3
  export const NAME = "mcp-context-card";
4
- export const VERSION = "1.9.2";
4
+ export const VERSION = "1.9.4";
5
5
  export const SERVER_CARD_URI = "mcp-context-card://server-card";
6
6
  /** The server's registry identity: reverse-DNS, the same as server.json `name`.
7
7
  * It is also `serverInfo.name`, so a Server Card never contradicts the live
package/dist/server.d.ts CHANGED
@@ -32,10 +32,25 @@ export declare const ROOT: string;
32
32
  * `/.well-known/mcp/server-card` is kept as an alias). Built in server-card.ts.
33
33
  */
34
34
  export { serverCard } from "./server-card.js";
35
- /** Sent to every client at initialize. Hosts that support MCP Apps show the
36
- * card inline; for the rest, this steers the model to save the card as a
37
- * file instead of pasting a whole HTML page into the chat. */
35
+ /** How many roots are looked at; the rest of a longer list is ignored. */
36
+ export declare const MAX_ROOTS = 64;
37
+ /** The folder a root names, or undefined. Never throws, never touches the
38
+ * filesystem. The spec says a root uri MUST be file://; the Cursor editor
39
+ * sends a bare absolute path, so that is read as the same folder. Skipped:
40
+ * a non-string, another scheme, a relative path, a `..` segment (raw or
41
+ * %2e%2e, in either form), a file:// URI with a host, and UNC or device
42
+ * paths (\\host\share, //host/share), which on Windows would reach the
43
+ * network. */
44
+ export declare function rootPath(uri: unknown): string | undefined;
45
+ /** Sent to every client at initialize. Seeing the card is render_context_card
46
+ * (the host draws it, or the reply is the Markdown card). The user names a
47
+ * surface to change that: Markdown, or the browser (save_context_card, the
48
+ * only path that writes a file), so asking to see the card writes nothing.
49
+ * Pinned as text in test/server.test.ts: a change needs a goose desktop load check. */
38
50
  export declare const INSTRUCTIONS: string;
51
+ /** The one prompt: `show`. Clients that list prompts offer it as a command
52
+ * (Claude Code: /<server>:show); it asks the model to show the card. */
53
+ export declare const SHOW_PROMPT = "Show the context card.";
39
54
  export interface ServerOptions {
40
55
  /** Open a saved file for the person. Set only for a local (stdio) server;
41
56
  * over HTTP the browser would open on the server, so it stays unset. */
package/dist/server.js CHANGED
@@ -20,7 +20,7 @@
20
20
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
21
21
  import { CLIENT_CAPABILITIES_META_KEY, inputRequired, inputResponse, isInputRequiredResult, Server } from "@modelcontextprotocol/server";
22
22
  import { existsSync, statSync, writeFileSync } from "node:fs";
23
- import { dirname, isAbsolute, join, normalize, sep } from "node:path";
23
+ import { dirname, isAbsolute, join, resolve } from "node:path";
24
24
  import { fileURLToPath, pathToFileURL } from "node:url";
25
25
  import { findSection, parseAgentsMd } from "./agents-md.js";
26
26
  import { authorAgentsMd } from "./author.js";
@@ -42,8 +42,10 @@ export const ROOT = join(here, "..");
42
42
  */
43
43
  export { serverCard } from "./server-card.js";
44
44
  const text = (s) => ({ content: [{ type: "text", text: s }] });
45
- /** A roots/list result read leniently: any string uri, checked later by
46
- * rootDir(). A Standard Schema, so no schema library is needed. */
45
+ /** How many roots are looked at; the rest of a longer list is ignored. */
46
+ export const MAX_ROOTS = 64;
47
+ /** A roots/list result read leniently: entries kept as sent, each checked by
48
+ * rootPath(). A Standard Schema, so no schema library is needed. */
47
49
  const LENIENT_ROOTS = {
48
50
  "~standard": {
49
51
  version: 1,
@@ -52,20 +54,58 @@ const LENIENT_ROOTS = {
52
54
  const roots = value?.roots;
53
55
  if (!Array.isArray(roots))
54
56
  return { issues: [{ message: "roots/list result has no roots array" }] };
55
- return { value: { roots: roots.filter((r) => typeof r?.uri === "string") } };
57
+ return { value: { roots: roots.slice(0, MAX_ROOTS) } };
56
58
  },
57
59
  },
58
60
  };
59
- /** Sent to every client at initialize. Hosts that support MCP Apps show the
60
- * card inline; for the rest, this steers the model to save the card as a
61
- * file instead of pasting a whole HTML page into the chat. */
61
+ /** The folder a root names, or undefined. Never throws, never touches the
62
+ * filesystem. The spec says a root uri MUST be file://; the Cursor editor
63
+ * sends a bare absolute path, so that is read as the same folder. Skipped:
64
+ * a non-string, another scheme, a relative path, a `..` segment (raw or
65
+ * %2e%2e, in either form), a file:// URI with a host, and UNC or device
66
+ * paths (\\host\share, //host/share), which on Windows would reach the
67
+ * network. */
68
+ export function rootPath(uri) {
69
+ if (typeof uri !== "string" || uri === "" || uri.includes("\0"))
70
+ return undefined;
71
+ if (/(^|[\\/])(\.\.|%2e%2e)([\\/]|$)/i.test(uri))
72
+ return undefined;
73
+ let dir;
74
+ if (/^file:/i.test(uri)) {
75
+ try {
76
+ dir = fileURLToPath(uri);
77
+ }
78
+ catch {
79
+ return undefined; // a host on POSIX, or not a file URL
80
+ }
81
+ }
82
+ else if (isAbsolute(uri) || /^[a-z]:[\\/]/i.test(uri)) {
83
+ dir = uri;
84
+ }
85
+ else {
86
+ return undefined;
87
+ }
88
+ if (/^[\\/]{2}/.test(dir))
89
+ return undefined; // UNC, \\?\, \\.\ (a file:// host on Windows)
90
+ if (!isAbsolute(dir))
91
+ return undefined; // a drive path on POSIX
92
+ return resolve(dir);
93
+ }
94
+ /** Sent to every client at initialize. Seeing the card is render_context_card
95
+ * (the host draws it, or the reply is the Markdown card). The user names a
96
+ * surface to change that: Markdown, or the browser (save_context_card, the
97
+ * only path that writes a file), so asking to see the card writes nothing.
98
+ * Pinned as text in test/server.test.ts: a change needs a goose desktop load check. */
62
99
  export const INSTRUCTIONS = "This server publishes a project's context (AGENTS.md), memory (project.fafm) and identity (.well-known/fafa). " +
63
100
  "Read them with read_agents_md, recall and whoami; list_context_sources says what is published and where. " +
64
- "When the user wants to see the context card: hosts that support MCP Apps display it inline from render_context_card. " +
65
- "Otherwise, don't paste the card's HTML into the conversation. Call save_context_card: it writes context-card.html " +
66
- "into the project, opens it in the user's browser when this server runs locally, and returns the card as Markdown " +
67
- "with a link to the saved file. Show the user that Markdown as returned, including the link, so they see the card " +
68
- "in the chat and can open the full version in a browser.";
101
+ "To show the user the context card, call render_context_card. In a client that displays MCP Apps the card appears " +
102
+ "in the chat, so add nothing or one line. Anywhere else it returns the card as Markdown: show it as returned. " +
103
+ "When the user names where they want it, follow that: as Markdown or as text, pass format: \"markdown\"; " +
104
+ "in the browser or as a file, call save_context_card, which writes context-card.html into the project, opens it " +
105
+ "when this server runs locally, and returns a link. Never paste the card's HTML into the chat.";
106
+ /** The one prompt: `show`. Clients that list prompts offer it as a command
107
+ * (Claude Code: /<server>:show); it asks the model to show the card. */
108
+ export const SHOW_PROMPT = "Show the context card.";
69
109
  /** Theme / accent / expanded from tool arguments, shared by render and save. */
70
110
  function cardOptions(args) {
71
111
  const theme = args.theme;
@@ -107,7 +147,7 @@ function hostRendersApps(server, ctx, era) {
107
147
  export function createServer(root = ROOT, opts = {}) {
108
148
  // A JS caller can pass any string: normalise it; an unknown value falls back to the environment.
109
149
  const cardApp = toCardApp(opts.cardApp) ?? cardAppFromEnv();
110
- const server = new Server({ name: REGISTRY_NAME, title: TITLE, version: VERSION }, { capabilities: { tools: {}, resources: {} }, instructions: INSTRUCTIONS });
150
+ const server = new Server({ name: REGISTRY_NAME, title: TITLE, version: VERSION }, { capabilities: { tools: {}, resources: {}, prompts: {} }, instructions: INSTRUCTIONS });
111
151
  // ── Which project? ────────────────────────────────────────────────────
112
152
  // Resolved per call, not at startup: a client's roots are only known once
113
153
  // it has connected, and they can change (goose sends its session's working
@@ -124,26 +164,14 @@ export function createServer(root = ROOT, opts = {}) {
124
164
  return false;
125
165
  }
126
166
  };
127
- /** A root's folder. The spec says a root uri MUST be file://; some clients
128
- * (the Cursor editor) send a bare absolute path, so that is read as the
129
- * same folder. Anything else (another scheme, a relative path, a `..`
130
- * segment, a file:// URI with a host) is skipped, never fatal. */
131
- const rootDir = (uri) => {
132
- if (uri.startsWith("file:")) {
133
- try {
134
- return fileURLToPath(uri);
135
- }
136
- catch {
137
- return undefined;
138
- }
139
- }
140
- if (!isAbsolute(uri) || normalize(uri).split(sep).includes("..") || uri.split(/[\\/]/).includes(".."))
141
- return undefined;
142
- return normalize(uri);
143
- };
167
+ /** The first root that is an existing folder. Entries come from the
168
+ * client unvalidated (2026-07-28 inputResponses are not checked by the
169
+ * SDK), so anything may be here; nothing in it can fail the call. */
144
170
  const firstDir = (roots) => {
145
- for (const r of roots) {
146
- const dir = rootDir(r.uri);
171
+ if (!Array.isArray(roots))
172
+ return undefined;
173
+ for (const r of roots.slice(0, MAX_ROOTS)) {
174
+ const dir = rootPath(r?.uri);
147
175
  if (dir && isDir(dir))
148
176
  return dir;
149
177
  }
@@ -165,7 +193,7 @@ export function createServer(root = ROOT, opts = {}) {
165
193
  try {
166
194
  // Not server.listRoots(): its schema rejects a uri without file://, and
167
195
  // the Cursor editor sends bare paths. rootDir() does the checking.
168
- const { roots } = (await server.request({ method: "roots/list" }, LENIENT_ROOTS, { timeout: 5000 }));
196
+ const { roots } = await server.request({ method: "roots/list" }, LENIENT_ROOTS, { timeout: 5000 });
169
197
  return firstDir(roots);
170
198
  }
171
199
  catch {
@@ -248,6 +276,19 @@ export function createServer(root = ROOT, opts = {}) {
248
276
  server.setRequestHandler('resources/templates/list', async () => ({
249
277
  resourceTemplates: [],
250
278
  }));
279
+ // ── Prompts ─────────────────────────────────────────────────────────
280
+ // One word to teach: `show`. The server name supplies "context card".
281
+ server.setRequestHandler('prompts/list', async () => ({
282
+ prompts: [{ name: "show", title: "Show the context card", description: "Show this project's context card in the chat." }],
283
+ }));
284
+ server.setRequestHandler('prompts/get', async (req) => {
285
+ if (req.params.name !== "show")
286
+ throw new Error(`unknown prompt: ${req.params.name}`);
287
+ return {
288
+ description: "Show this project's context card in the chat.",
289
+ messages: [{ role: "user", content: { type: "text", text: SHOW_PROMPT } }],
290
+ };
291
+ });
251
292
  // ── Tools ───────────────────────────────────────────────────────────
252
293
  server.setRequestHandler('tools/list', async () => ({
253
294
  tools: [
@@ -446,7 +487,8 @@ export function createServer(root = ROOT, opts = {}) {
446
487
  // CARD_UI_URI (the mini card in a panel); anywhere else the reply is
447
488
  // the text card (Markdown). HTML only goes to a browser: save_context_card,
448
489
  // GET /card, the CLI. format: "markdown" asks for the text card anywhere.
449
- if (args.format !== "markdown" && hostRendersApps(server, ctx, opts.era)) {
490
+ const asMarkdown = String(args.format ?? "").trim().toLowerCase() === "markdown";
491
+ if (!asMarkdown && hostRendersApps(server, ctx, opts.era)) {
450
492
  const c = cardCounts(root);
451
493
  const n = (k, w) => `${k} ${w}${k === 1 ? "" : "s"}`;
452
494
  const context = c.agents ? `AGENTS.md with ${n(c.sections, "section")}` : "no AGENTS.md yet";
@@ -455,7 +497,7 @@ export function createServer(root = ROOT, opts = {}) {
455
497
  "This client displays MCP Apps, so the card is drawn here. If the user says they can't see it, " +
456
498
  "call save_context_card. For the card as Markdown, call render_context_card with format: \"markdown\".");
457
499
  }
458
- return text(`${renderCardText(root, { detail: args.detail === "full" ? "full" : "tldr" })}\n\n` +
500
+ return text(`${renderCardText(root, { detail: String(args.detail ?? "").trim().toLowerCase() === "full" ? "full" : "tldr" })}\n\n` +
459
501
  "_The original card: save_context_card writes context-card.html and, when this server runs locally, opens it in the browser._");
460
502
  }
461
503
  case "save_context_card": {
@@ -472,7 +514,7 @@ export function createServer(root = ROOT, opts = {}) {
472
514
  if (opened)
473
515
  opts.openFile(out);
474
516
  return text(`${saved}\n\n` +
475
- `${renderCardText(root, { detail: args.detail === "full" ? "full" : "tldr" })}\n\n` +
517
+ `${renderCardText(root, { detail: String(args.detail ?? "").trim().toLowerCase() === "full" ? "full" : "tldr" })}\n\n` +
476
518
  "_In a host that supports MCP Apps, this card shows inline._\n\n---\n\n" +
477
519
  (opened ? "Opened the full card in your browser.\n\n" : "") +
478
520
  `**[Open the full card in your browser](${pathToFileURL(out).href})**\n\n` +
@@ -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.9.2",
35
+ "version": "1.9.4",
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,7 +85,7 @@ 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.9.2, this repo's own files):
88
+ Served from `https://ctx.example.com` (1.9.4, this repo's own files):
89
89
 
90
90
  ```jsonc
91
91
  {
package/docs/TRANSPORT.md CHANGED
@@ -25,9 +25,15 @@ an older client gets 2025, unchanged from 1.x. The Server Card lists both.
25
25
 
26
26
  **A root's `uri` should be `file://`.** The spec says it MUST be; the Cursor
27
27
  editor (3.29.9) sends a bare absolute path (`/Users/me/project`). The server
28
- reads either as the same folder, and skips anything else (another scheme, a
29
- relative path, a `..` segment, a `file://` URI with a host) without failing
30
- the call.
28
+ reads either as the same folder (on Windows, a drive path such as `C:\proj`
29
+ too). It skips, on both protocol eras and without failing the call: a
30
+ non-string or malformed entry, another scheme, a relative path, a `..`
31
+ segment (raw or `%2e%2e`, in either form), a `file://` URI with a host other than `localhost`, and
32
+ UNC or device paths (`\\host\share`, `//host/share`), which on Windows would
33
+ reach the network. It looks at the first 64 roots and uses the first that is
34
+ an existing folder. A root is the client's choice of project, like
35
+ `MCP_CONTEXT_CARD_ROOT`: `remember`, `forget` and `save_context_card` write
36
+ there.
31
37
 
32
38
  **Roots differ by era.** On 2025 the server asks the client with a
33
39
  `roots/list` request; if the client fails to answer, the server falls back to
@@ -60,15 +66,17 @@ plain card. That page does not include the helper or the inline view.
60
66
  returns one line (the counts, and `save_context_card` in case the host
61
67
  doesn't draw it); for any other client, the card as Markdown (the same text
62
68
  `save_context_card` returns, with no file written). `format: "markdown"`
63
- returns that Markdown card for any client, MCP Apps or not. Over the stateless HTTP
69
+ returns that Markdown card for any client, MCP Apps or not; an MCP Apps host
70
+ still draws the card for that call, because the card belongs to the tool,
71
+ not to one call. Over the stateless HTTP
64
72
  transport a 2025 client's `initialize` capabilities don't reach the later
65
73
  `tools/call` (each POST is a fresh server), so a 2025 MCP Apps host there
66
74
  gets the Markdown card; it still draws the card from the tool's `_meta.ui`.
67
75
  The mini card and that line share one count (`cardCounts`); level-1
68
76
  headings, such as the `# AGENTS.md` title, are not sections. The server
69
- instructions are unchanged from 1.8.0 on purpose: goose
70
- desktop 1.52 stopped loading a build whose instructions changed (cause not
71
- found), and a test pins them.
77
+ instructions changed in 1.9.4, after a goose desktop 1.57 load check: goose
78
+ desktop 1.52 had stopped loading a build whose instructions changed (cause
79
+ not found; 1.57 loads one). A test pins them, so any change shows as a diff.
72
80
 
73
81
  After `ui/initialize`, `App.getHostContext()` is the host context
74
82
  (`McpUiHostContext` in `@modelcontextprotocol/ext-apps`). The fields this
package/docs/WIRING.md CHANGED
@@ -38,10 +38,6 @@
38
38
  the arg — e.g. `command: "node"`, `args: ["/path/to/node_modules/mcp-context-card/dist/bin.js"]`
39
39
  (or wherever `npm install -g` / your package manager put it; find it with
40
40
  `npm root -g` or `which mcp-context-card` after a global install).
41
- - **`@modelcontextprotocol/inspector` 2.x reports `prompts/list` as `{ "prompts": [] }`** —
42
- this server declares only `tools` + `resources`, so `prompts/list` returns
43
- `-32601` on the wire; the current Inspector CLI masks that as an empty list
44
- (raw JSON-RPC, or Inspector 0.21.x, shows the real `-32601`).
45
41
 
46
42
  ### Streamable HTTP (remote)
47
43
 
@@ -205,7 +205,7 @@ details.ctx-section>.md{padding:0 0 16px}
205
205
  </details>
206
206
  <div class="id">
207
207
  <h1>mcp-context-card</h1>
208
- <p class="title">MCP server · v1.9.2</p>
208
+ <p class="title">MCP server · v1.9.4</p>
209
209
  <p class="oneliner">The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity</p>
210
210
  </div>
211
211
  <div class="foot-front"><span class="domain">faf.one</span><div class="pills"><span class="pill">io.github.Wolfe-Jam</span><span class="pill accent">published</span><span class="pill">MIT</span></div></div>
@@ -223,7 +223,7 @@ details.ctx-section>.md{padding:0 0 16px}
223
223
  </div>
224
224
  </details>
225
225
  <div class="tabbar"><label for="t-about">About</label><label for="t-context">Context</label><label for="t-memory">Memory</label><label for="t-discovery">Discovery</label></div>
226
- <div class="panes"><div class="pane p-about"><p class="label">About</p><p class="lead">The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity — a base MCP on its own, or a drop-in extension for any existing MCP server, discoverable through the Server Card _meta block and a self-published ai-catalog.json.</p><table class="kv"><tbody><tr><th>Agent ID</th><td><code>urn:air:faf.one:agent:mcp-context-card</code></td></tr><tr><th>Publisher</th><td>io.github.Wolfe-Jam</td></tr><tr><th>Version</th><td>v1.9.2</td></tr><tr><th>Status</th><td>published</td></tr><tr><th>License</th><td>MIT</td></tr><tr><th>Package (npm)</th><td><code>mcp-context-card</code></td></tr></tbody></table></div><div class="pane p-context"><p class="label">Context — AGENTS.md</p><div class="ctx-nav"><ul class="toc"><li><a href="#setup">Setup</a></li><li><a href="#build">Build</a></li><li><a href="#test">Test</a></li><li><a href="#layout">Layout</a></li><li><a href="#conventions">Conventions</a></li><li><a href="#the-invariant">The invariant</a></li><li><a href="#safety">Safety</a></li><li><a href="#definition-of-done">Definition of done</a></li><li><a href="#authoring-this-file">Authoring this file</a></li></ul><button type="button" class="xall" hidden>Expand all</button></div>
226
+ <div class="panes"><div class="pane p-about"><p class="label">About</p><p class="lead">The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity — a base MCP on its own, or a drop-in extension for any existing MCP server, discoverable through the Server Card _meta block and a self-published ai-catalog.json.</p><table class="kv"><tbody><tr><th>Agent ID</th><td><code>urn:air:faf.one:agent:mcp-context-card</code></td></tr><tr><th>Publisher</th><td>io.github.Wolfe-Jam</td></tr><tr><th>Version</th><td>v1.9.4</td></tr><tr><th>Status</th><td>published</td></tr><tr><th>License</th><td>MIT</td></tr><tr><th>Package (npm)</th><td><code>mcp-context-card</code></td></tr></tbody></table></div><div class="pane p-context"><p class="label">Context — AGENTS.md</p><div class="ctx-nav"><ul class="toc"><li><a href="#setup">Setup</a></li><li><a href="#build">Build</a></li><li><a href="#test">Test</a></li><li><a href="#layout">Layout</a></li><li><a href="#conventions">Conventions</a></li><li><a href="#the-invariant">The invariant</a></li><li><a href="#safety">Safety</a></li><li><a href="#definition-of-done">Definition of done</a></li><li><a href="#authoring-this-file">Authoring this file</a></li></ul><button type="button" class="xall" hidden>Expand all</button></div>
227
227
  <div class="ctx-preamble md"><p><code>mcp-context-card</code> is the essential MCP server for a project's <strong>context</strong> (this file), <strong>memory</strong>, and <strong>identity</strong> — usable as your base MCP, or dropped into any existing MCP server as an extension. Discoverable to any MCP client through the two surfaces already in the ecosystem: the Server Card <code>_meta</code> block and <code>ai-catalog.json</code> sibling entries.</p>
228
228
  <p><code>read_agents_md</code> serves this file, section by section, over the same MCP connection.</p></div>
229
229
  <div class="ctx-body"><details class="ctx-section" id="setup"><summary>Setup</summary><div class="md"><pre><code class="language-bash">npm ci</code></pre>
@@ -205,7 +205,7 @@ details.ctx-section>.md{padding:0 0 16px}
205
205
  </details>
206
206
  <div class="id">
207
207
  <h1>mcp-context-card</h1>
208
- <p class="title">MCP server · v1.9.2</p>
208
+ <p class="title">MCP server · v1.9.4</p>
209
209
  <p class="oneliner">The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity</p>
210
210
  </div>
211
211
  <div class="foot-front"><span class="domain">faf.one</span><div class="pills"><span class="pill">io.github.Wolfe-Jam</span><span class="pill accent">published</span><span class="pill">MIT</span></div></div>
@@ -223,7 +223,7 @@ details.ctx-section>.md{padding:0 0 16px}
223
223
  </div>
224
224
  </details>
225
225
  <div class="tabbar"><label for="t-about">About</label><label for="t-context">Context</label><label for="t-memory">Memory</label><label for="t-discovery">Discovery</label></div>
226
- <div class="panes"><div class="pane p-about"><p class="label">About</p><p class="lead">The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity — a base MCP on its own, or a drop-in extension for any existing MCP server, discoverable through the Server Card _meta block and a self-published ai-catalog.json.</p><table class="kv"><tbody><tr><th>Agent ID</th><td><code>urn:air:faf.one:agent:mcp-context-card</code></td></tr><tr><th>Publisher</th><td>io.github.Wolfe-Jam</td></tr><tr><th>Version</th><td>v1.9.2</td></tr><tr><th>Status</th><td>published</td></tr><tr><th>License</th><td>MIT</td></tr><tr><th>Package (npm)</th><td><code>mcp-context-card</code></td></tr></tbody></table></div><div class="pane p-context"><p class="label">Context — AGENTS.md</p><div class="ctx-nav"><ul class="toc"><li><a href="#setup">Setup</a></li><li><a href="#build">Build</a></li><li><a href="#test">Test</a></li><li><a href="#layout">Layout</a></li><li><a href="#conventions">Conventions</a></li><li><a href="#the-invariant">The invariant</a></li><li><a href="#safety">Safety</a></li><li><a href="#definition-of-done">Definition of done</a></li><li><a href="#authoring-this-file">Authoring this file</a></li></ul><button type="button" class="xall" hidden>Expand all</button></div>
226
+ <div class="panes"><div class="pane p-about"><p class="label">About</p><p class="lead">The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity — a base MCP on its own, or a drop-in extension for any existing MCP server, discoverable through the Server Card _meta block and a self-published ai-catalog.json.</p><table class="kv"><tbody><tr><th>Agent ID</th><td><code>urn:air:faf.one:agent:mcp-context-card</code></td></tr><tr><th>Publisher</th><td>io.github.Wolfe-Jam</td></tr><tr><th>Version</th><td>v1.9.4</td></tr><tr><th>Status</th><td>published</td></tr><tr><th>License</th><td>MIT</td></tr><tr><th>Package (npm)</th><td><code>mcp-context-card</code></td></tr></tbody></table></div><div class="pane p-context"><p class="label">Context — AGENTS.md</p><div class="ctx-nav"><ul class="toc"><li><a href="#setup">Setup</a></li><li><a href="#build">Build</a></li><li><a href="#test">Test</a></li><li><a href="#layout">Layout</a></li><li><a href="#conventions">Conventions</a></li><li><a href="#the-invariant">The invariant</a></li><li><a href="#safety">Safety</a></li><li><a href="#definition-of-done">Definition of done</a></li><li><a href="#authoring-this-file">Authoring this file</a></li></ul><button type="button" class="xall" hidden>Expand all</button></div>
227
227
  <div class="ctx-preamble md"><p><code>mcp-context-card</code> is the essential MCP server for a project's <strong>context</strong> (this file), <strong>memory</strong>, and <strong>identity</strong> — usable as your base MCP, or dropped into any existing MCP server as an extension. Discoverable to any MCP client through the two surfaces already in the ecosystem: the Server Card <code>_meta</code> block and <code>ai-catalog.json</code> sibling entries.</p>
228
228
  <p><code>read_agents_md</code> serves this file, section by section, over the same MCP connection.</p></div>
229
229
  <div class="ctx-body"><details class="ctx-section" id="setup"><summary>Setup</summary><div class="md"><pre><code class="language-bash">npm ci</code></pre>
package/docs/card.html CHANGED
@@ -205,7 +205,7 @@ details.ctx-section>.md{padding:0 0 16px}
205
205
  </details>
206
206
  <div class="id">
207
207
  <h1>mcp-context-card</h1>
208
- <p class="title">MCP server · v1.9.2</p>
208
+ <p class="title">MCP server · v1.9.4</p>
209
209
  <p class="oneliner">The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity</p>
210
210
  </div>
211
211
  <div class="foot-front"><span class="domain">faf.one</span><div class="pills"><span class="pill">io.github.Wolfe-Jam</span><span class="pill accent">published</span><span class="pill">MIT</span></div></div>
@@ -223,7 +223,7 @@ details.ctx-section>.md{padding:0 0 16px}
223
223
  </div>
224
224
  </details>
225
225
  <div class="tabbar"><label for="t-about">About</label><label for="t-context">Context</label><label for="t-memory">Memory</label><label for="t-discovery">Discovery</label></div>
226
- <div class="panes"><div class="pane p-about"><p class="label">About</p><p class="lead">The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity — a base MCP on its own, or a drop-in extension for any existing MCP server, discoverable through the Server Card _meta block and a self-published ai-catalog.json.</p><table class="kv"><tbody><tr><th>Agent ID</th><td><code>urn:air:faf.one:agent:mcp-context-card</code></td></tr><tr><th>Publisher</th><td>io.github.Wolfe-Jam</td></tr><tr><th>Version</th><td>v1.9.2</td></tr><tr><th>Status</th><td>published</td></tr><tr><th>License</th><td>MIT</td></tr><tr><th>Package (npm)</th><td><code>mcp-context-card</code></td></tr></tbody></table></div><div class="pane p-context"><p class="label">Context — AGENTS.md</p><div class="ctx-nav"><ul class="toc"><li><a href="#setup">Setup</a></li><li><a href="#build">Build</a></li><li><a href="#test">Test</a></li><li><a href="#layout">Layout</a></li><li><a href="#conventions">Conventions</a></li><li><a href="#the-invariant">The invariant</a></li><li><a href="#safety">Safety</a></li><li><a href="#definition-of-done">Definition of done</a></li><li><a href="#authoring-this-file">Authoring this file</a></li></ul><button type="button" class="xall" hidden>Expand all</button></div>
226
+ <div class="panes"><div class="pane p-about"><p class="label">About</p><p class="lead">The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity — a base MCP on its own, or a drop-in extension for any existing MCP server, discoverable through the Server Card _meta block and a self-published ai-catalog.json.</p><table class="kv"><tbody><tr><th>Agent ID</th><td><code>urn:air:faf.one:agent:mcp-context-card</code></td></tr><tr><th>Publisher</th><td>io.github.Wolfe-Jam</td></tr><tr><th>Version</th><td>v1.9.4</td></tr><tr><th>Status</th><td>published</td></tr><tr><th>License</th><td>MIT</td></tr><tr><th>Package (npm)</th><td><code>mcp-context-card</code></td></tr></tbody></table></div><div class="pane p-context"><p class="label">Context — AGENTS.md</p><div class="ctx-nav"><ul class="toc"><li><a href="#setup">Setup</a></li><li><a href="#build">Build</a></li><li><a href="#test">Test</a></li><li><a href="#layout">Layout</a></li><li><a href="#conventions">Conventions</a></li><li><a href="#the-invariant">The invariant</a></li><li><a href="#safety">Safety</a></li><li><a href="#definition-of-done">Definition of done</a></li><li><a href="#authoring-this-file">Authoring this file</a></li></ul><button type="button" class="xall" hidden>Expand all</button></div>
227
227
  <div class="ctx-preamble md"><p><code>mcp-context-card</code> is the essential MCP server for a project's <strong>context</strong> (this file), <strong>memory</strong>, and <strong>identity</strong> — usable as your base MCP, or dropped into any existing MCP server as an extension. Discoverable to any MCP client through the two surfaces already in the ecosystem: the Server Card <code>_meta</code> block and <code>ai-catalog.json</code> sibling entries.</p>
228
228
  <p><code>read_agents_md</code> serves this file, section by section, over the same MCP connection.</p></div>
229
229
  <div class="ctx-body"><details class="ctx-section" id="setup"><summary>Setup</summary><div class="md"><pre><code class="language-bash">npm ci</code></pre>
Binary file
Binary file
Binary file
package/docs/index.html CHANGED
@@ -205,7 +205,7 @@ details.ctx-section>.md{padding:0 0 16px}
205
205
  </details>
206
206
  <div class="id">
207
207
  <h1>mcp-context-card</h1>
208
- <p class="title">MCP server · v1.9.2</p>
208
+ <p class="title">MCP server · v1.9.4</p>
209
209
  <p class="oneliner">The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity</p>
210
210
  </div>
211
211
  <div class="foot-front"><span class="domain">faf.one</span><div class="pills"><span class="pill">io.github.Wolfe-Jam</span><span class="pill accent">published</span><span class="pill">MIT</span></div></div>
@@ -223,7 +223,7 @@ details.ctx-section>.md{padding:0 0 16px}
223
223
  </div>
224
224
  </details>
225
225
  <div class="tabbar"><label for="t-about">About</label><label for="t-context">Context</label><label for="t-memory">Memory</label><label for="t-discovery">Discovery</label></div>
226
- <div class="panes"><div class="pane p-about"><p class="label">About</p><p class="lead">The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity — a base MCP on its own, or a drop-in extension for any existing MCP server, discoverable through the Server Card _meta block and a self-published ai-catalog.json.</p><table class="kv"><tbody><tr><th>Agent ID</th><td><code>urn:air:faf.one:agent:mcp-context-card</code></td></tr><tr><th>Publisher</th><td>io.github.Wolfe-Jam</td></tr><tr><th>Version</th><td>v1.9.2</td></tr><tr><th>Status</th><td>published</td></tr><tr><th>License</th><td>MIT</td></tr><tr><th>Package (npm)</th><td><code>mcp-context-card</code></td></tr></tbody></table></div><div class="pane p-context"><p class="label">Context — AGENTS.md</p><div class="ctx-nav"><ul class="toc"><li><a href="#setup">Setup</a></li><li><a href="#build">Build</a></li><li><a href="#test">Test</a></li><li><a href="#layout">Layout</a></li><li><a href="#conventions">Conventions</a></li><li><a href="#the-invariant">The invariant</a></li><li><a href="#safety">Safety</a></li><li><a href="#definition-of-done">Definition of done</a></li><li><a href="#authoring-this-file">Authoring this file</a></li></ul><button type="button" class="xall" hidden>Expand all</button></div>
226
+ <div class="panes"><div class="pane p-about"><p class="label">About</p><p class="lead">The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity — a base MCP on its own, or a drop-in extension for any existing MCP server, discoverable through the Server Card _meta block and a self-published ai-catalog.json.</p><table class="kv"><tbody><tr><th>Agent ID</th><td><code>urn:air:faf.one:agent:mcp-context-card</code></td></tr><tr><th>Publisher</th><td>io.github.Wolfe-Jam</td></tr><tr><th>Version</th><td>v1.9.4</td></tr><tr><th>Status</th><td>published</td></tr><tr><th>License</th><td>MIT</td></tr><tr><th>Package (npm)</th><td><code>mcp-context-card</code></td></tr></tbody></table></div><div class="pane p-context"><p class="label">Context — AGENTS.md</p><div class="ctx-nav"><ul class="toc"><li><a href="#setup">Setup</a></li><li><a href="#build">Build</a></li><li><a href="#test">Test</a></li><li><a href="#layout">Layout</a></li><li><a href="#conventions">Conventions</a></li><li><a href="#the-invariant">The invariant</a></li><li><a href="#safety">Safety</a></li><li><a href="#definition-of-done">Definition of done</a></li><li><a href="#authoring-this-file">Authoring this file</a></li></ul><button type="button" class="xall" hidden>Expand all</button></div>
227
227
  <div class="ctx-preamble md"><p><code>mcp-context-card</code> is the essential MCP server for a project's <strong>context</strong> (this file), <strong>memory</strong>, and <strong>identity</strong> — usable as your base MCP, or dropped into any existing MCP server as an extension. Discoverable to any MCP client through the two surfaces already in the ecosystem: the Server Card <code>_meta</code> block and <code>ai-catalog.json</code> sibling entries.</p>
228
228
  <p><code>read_agents_md</code> serves this file, section by section, over the same MCP connection.</p></div>
229
229
  <div class="ctx-body"><details class="ctx-section" id="setup"><summary>Setup</summary><div class="md"><pre><code class="language-bash">npm ci</code></pre>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-context-card",
3
- "version": "1.9.2",
3
+ "version": "1.9.4",
4
4
  "mcpName": "io.github.Wolfe-Jam/mcp-context-card",
5
5
  "description": "The essential MCP components for a project's context (AGENTS.md), cross-session memory, and identity — a base MCP on its own, or a drop-in extension for any existing MCP server. Discoverable through the Server Card _meta block and ai-catalog.json sibling entries.",
6
6
  "keywords": [
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.Wolfe-Jam/mcp-context-card",
4
4
  "title": "MCP Context Card",
5
5
  "description": "MCP server for a project's context (AGENTS.md), memory, and identity — base or drop-in extension.",
6
- "version": "1.9.2",
6
+ "version": "1.9.4",
7
7
  "repository": {
8
8
  "url": "https://github.com/Wolfe-Jam/mcp-context-card",
9
9
  "source": "github"
@@ -14,7 +14,7 @@
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "mcp-context-card",
17
- "version": "1.9.2",
17
+ "version": "1.9.4",
18
18
  "runtimeHint": "npx",
19
19
  "transport": {
20
20
  "type": "stdio"