@adia-ai/a2ui-mcp 0.6.5 → 0.6.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog — @adia-ai/a2ui-mcp
2
2
 
3
+ ## [0.6.7] — 2026-05-19
4
+
5
+ ### Added
6
+ - **`list_patterns` tool** — enumerate the full A2UI composition corpus
7
+ with optional `domain` / `category` filters. Was previously only
8
+ searchable by keyword via `search_chunks` / `search_patterns`.
9
+ - **`server_status` tool** — reports transport (stdio/http), sampling
10
+ capability, corpus stats (components, compositions, chunks), and
11
+ version. Useful for consumer health checks and observability.
12
+ - **`refine_ui` tool** — retry monolithic-engine generation with validation
13
+ errors fed back to the LLM. Closes the missing-refine gap for the
14
+ monolithic path (zettel already had `refine_composition`).
15
+
16
+ ### Fixed
17
+ - **HTTP transport body-parsing** — `transport.handleRequest(req, res)` was
18
+ not passing the express-parsed body; SDK then errored `-32700 Parse error`
19
+ on every request. Fixed by passing `req.body` as the third argument.
20
+ Surfaced by new HTTP smoke test.
21
+ - **Excluded `.ts` source files from npm tarball** — TypeScript sources
22
+ were shipping alongside the emitted `.js` (7 files in mcp/, 1 in compose/,
23
+ 16 in retrieval/). Now filtered via `files` field while keeping `.d.ts`
24
+ declarations.
25
+ - **Removed unused `sessionServer` instantiation in HTTP transport** —
26
+ previously the HTTP request handler created a per-session `McpServer`
27
+ instance but never used it (called `server.connect()` against the outer
28
+ server). Removed the dead allocation; comment updated to reflect that
29
+ the outer server is shared across sessions (tools are stateless).
30
+
31
+ ## [0.6.6] — 2026-05-18
32
+
33
+ ### Changed
34
+ - Lockstep version bump for §304 `<field-ui align>` prop.
3
35
  ## [0.6.5] — 2026-05-18
4
36
 
5
37
  ### Changed
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  MCP server wrapping [`@adia-ai/a2ui-compose`](../compose). Exposes the generation
4
4
  engine, component catalog, pattern library, validator, and training
5
- feedback loop as stdio tools for Claude Desktop, Claude Code, and any
6
- other MCP-speaking host.
5
+ feedback loop as MCP tools for Claude Desktop, Claude Code, Cursor,
6
+ Windsurf, and any deployed MCP-capable host.
7
7
 
8
8
  > Runtime only. Generation logic lives in `@adia-ai/a2ui-compose`; UI atoms in
9
9
  > [`@adia-ai/web-components`](../../web-components); the A2UI protocol runtime
@@ -11,6 +11,24 @@ other MCP-speaking host.
11
11
  > [`@adia-ai/a2ui-runtime`](../runtime); corpus in
12
12
  > [`@adia-ai/a2ui-corpus`](../corpus).
13
13
 
14
+ ## Two transports, one server
15
+
16
+ The server auto-selects transport based on environment:
17
+
18
+ | Mode | When | LLM source |
19
+ |---|---|---|
20
+ | **stdio** (default) | Local IDE tools — Claude Code, Cursor, Hermes | Host's LLM via MCP sampling (no API key needed) |
21
+ | **HTTP** (`MCP_HTTP_PORT`) | Deployed web service, remote MCP clients | `.env` API key required |
22
+
23
+ **stdio** is the right choice for developers working inside their editor.
24
+ The server requests LLM inference from the host (Claude Code, Cursor) via
25
+ MCP `sampling/createMessage` — no separate API key, same model, same token
26
+ budget the user is already paying for.
27
+
28
+ **HTTP** is the right choice for a deployed backend service where clients
29
+ connect remotely. Each client session gets its own transport instance.
30
+ API keys must be in `.env` since no sampling-capable host is present.
31
+
14
32
  ## Install
15
33
 
16
34
  ```bash
@@ -19,16 +37,22 @@ npm install -g @adia-ai/a2ui-mcp # global — exposes the `adiaui-mcp` bin
19
37
  npm install @adia-ai/a2ui-mcp # local — invoke via npx
20
38
  ```
21
39
 
22
- The package ships an `adiaui-mcp` executable + a `server.js` entry point. Most MCP hosts (Claude Desktop, Claude Code) invoke the binary directly via `command` + `args` in their MCP config.
40
+ The package ships an `adiaui-mcp` executable + a `server.js` entry point.
23
41
 
24
42
  ## Quick start
25
43
 
26
44
  ```bash
27
- adiaui-mcp # if globally installed
45
+ # stdio (local IDE — default)
46
+ adiaui-mcp # global install
28
47
  npx @adia-ai/a2ui-mcp # via npx
29
- node packages/a2ui/mcp/server.js # from a local checkout
48
+ node packages/a2ui/mcp/server.js # from local checkout
49
+
50
+ # HTTP (deployed service)
51
+ MCP_HTTP_PORT=3460 node packages/a2ui/mcp/server.js
52
+ # → POST/GET/DELETE http://0.0.0.0:3460/mcp
30
53
  ```
31
54
 
55
+
32
56
  Register with Claude Code (`.claude/settings.json`):
33
57
 
34
58
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/a2ui-mcp",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "AdiaUI A2UI MCP server. Exposes the compose engine over MCP with an engine selector for monolithic + zettel strategies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,9 @@
12
12
  "scripts/",
13
13
  "personas/",
14
14
  "README.md",
15
- "CHANGELOG.md"
15
+ "CHANGELOG.md",
16
+ "!**/*.ts",
17
+ "**/*.d.ts"
16
18
  ],
17
19
  "license": "MIT",
18
20
  "publishConfig": {
@@ -31,6 +33,10 @@
31
33
  "@adia-ai/a2ui-validator": "^0.6.0",
32
34
  "@adia-ai/a2ui-corpus": "^0.6.0",
33
35
  "@adia-ai/llm": "^0.6.0",
34
- "zod": "^3.24.0"
36
+ "zod": "^3.24.0",
37
+ "express": "^4.21.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/express": "^5.0.0"
35
41
  }
36
42
  }
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * smoke-extended.mjs — three additional MCP smokes beyond smoke-merged.mjs.
4
+ *
5
+ * Smoke 1: HTTP transport boots, initialize returns mcp-session-id header,
6
+ * then SIGTERM cleanup.
7
+ * Smoke 2: Client declares { capabilities: { sampling: {} } }; server_status
8
+ * reflects sampling=true. Skip if SDK plumbing can't surface it.
9
+ * Smoke 3: list_patterns, server_status, refine_ui appear in tools/list and
10
+ * each is callable without throwing.
11
+ *
12
+ * Exit codes:
13
+ * 0 — all smokes passed (or skipped with clear reason)
14
+ * 1 — at least one smoke failed
15
+ */
16
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
17
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
18
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
19
+ import { spawn } from 'node:child_process';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { dirname, join } from 'node:path';
22
+ import { setTimeout as delay } from 'node:timers/promises';
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const serverPath = join(__dirname, '..', 'server.js');
26
+
27
+ const results = [];
28
+ function record(name, status, detail = '') {
29
+ results.push({ name, status, detail });
30
+ const icon = status === 'PASS' ? '✓' : status === 'SKIP' ? '○' : '✗';
31
+ console.log(`${icon} ${name}: ${status}${detail ? ` — ${detail}` : ''}`);
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Smoke 1 — HTTP transport startup + initialize
36
+ // ---------------------------------------------------------------------------
37
+ async function smokeHttp() {
38
+ const PORT = 33460;
39
+ const child = spawn('node', [serverPath], {
40
+ env: { ...process.env, MCP_HTTP_PORT: String(PORT) },
41
+ stdio: ['ignore', 'pipe', 'pipe'],
42
+ });
43
+
44
+ // Cleanup helpers — guarantee child is killed even on uncaught errors.
45
+ let killed = false;
46
+ const cleanup = () => {
47
+ if (killed) return;
48
+ killed = true;
49
+ try { child.kill('SIGTERM'); } catch {}
50
+ // Force-kill after 2s if SIGTERM ignored.
51
+ setTimeout(() => { try { child.kill('SIGKILL'); } catch {} }, 2000).unref();
52
+ };
53
+ process.once('SIGINT', cleanup);
54
+ process.once('SIGTERM', cleanup);
55
+ process.once('exit', cleanup);
56
+
57
+ try {
58
+ // Wait for "HTTP transport ready" on stderr (server.js logs there).
59
+ const ready = await new Promise((resolve, reject) => {
60
+ let buf = '';
61
+ const timer = setTimeout(() => reject(new Error('timeout waiting for HTTP ready')), 15000);
62
+ child.stderr.on('data', (chunk) => {
63
+ buf += chunk.toString();
64
+ if (/HTTP transport ready/.test(buf)) {
65
+ clearTimeout(timer);
66
+ resolve(buf);
67
+ }
68
+ });
69
+ child.on('exit', (code) => {
70
+ clearTimeout(timer);
71
+ reject(new Error(`server exited early (code=${code}); stderr:\n${buf}`));
72
+ });
73
+ });
74
+
75
+ // Small grace period to let express bind.
76
+ await delay(100);
77
+
78
+ // Use the SDK's HTTP client transport to drive a real initialize handshake.
79
+ // We extract the session id by intercepting the underlying fetch — the
80
+ // transport itself doesn't expose the header directly, but we can sniff it
81
+ // by overriding global fetch for the duration of the connect.
82
+ let sniffedSessionId = null;
83
+ const realFetch = globalThis.fetch;
84
+ globalThis.fetch = async (...args) => {
85
+ const res = await realFetch(...args);
86
+ const sid = res.headers.get?.('mcp-session-id');
87
+ if (sid && !sniffedSessionId) sniffedSessionId = sid;
88
+ return res;
89
+ };
90
+
91
+ const httpTransport = new StreamableHTTPClientTransport(
92
+ new URL(`http://127.0.0.1:${PORT}/mcp`),
93
+ );
94
+ const client = new Client(
95
+ { name: 'smoke-extended-http', version: '0.0.1' },
96
+ { capabilities: {} },
97
+ );
98
+
99
+ try {
100
+ await client.connect(httpTransport);
101
+ } finally {
102
+ globalThis.fetch = realFetch;
103
+ }
104
+
105
+ if (!sniffedSessionId) {
106
+ record('smoke-1-http', 'FAIL',
107
+ 'initialize handshake succeeded but no mcp-session-id header observed');
108
+ } else {
109
+ record('smoke-1-http', 'PASS',
110
+ `session=${sniffedSessionId.slice(0, 8)}… initialize ok`);
111
+ }
112
+ try { await client.close(); } catch {}
113
+ } catch (e) {
114
+ // The current server.js calls `transport.handleRequest(req, res)` without
115
+ // passing the express-parsed body as the 3rd arg. The SDK then tries to
116
+ // re-read the already-consumed stream and emits a -32700 Parse error.
117
+ // Surface this clearly so the parent agent can patch server.js.
118
+ const hint = /Parse error: Invalid JSON/.test(e.message)
119
+ ? '\n → Likely fix: in packages/a2ui/mcp/server.js startHttp(), change\n `transport.handleRequest(req, res)` → `transport.handleRequest(req, res, req.body)`\n (express.json() consumes the stream before the SDK can re-read it).'
120
+ : '';
121
+ record('smoke-1-http', 'FAIL', e.message + hint);
122
+ } finally {
123
+ cleanup();
124
+ // Wait briefly for child exit so its log doesn't bleed into later smokes.
125
+ await new Promise((r) => {
126
+ if (child.exitCode !== null) return r();
127
+ child.once('exit', r);
128
+ setTimeout(r, 2500).unref();
129
+ });
130
+ }
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Smoke 2 — Sampling capability detection
135
+ // ---------------------------------------------------------------------------
136
+ async function smokeSampling() {
137
+ const transport = new StdioClientTransport({
138
+ command: 'node',
139
+ args: [serverPath],
140
+ stderr: 'ignore',
141
+ });
142
+ // The SDK Client constructor takes (info, options); options.capabilities
143
+ // is sent in the initialize handshake.
144
+ const client = new Client(
145
+ { name: 'smoke-extended-sampling', version: '0.0.1' },
146
+ { capabilities: { sampling: {} } },
147
+ );
148
+
149
+ try {
150
+ await client.connect(transport);
151
+ const r = await client.callTool({ name: 'server_status', arguments: {} });
152
+ const text = r.content?.[0]?.text ?? '';
153
+ let parsed;
154
+ try { parsed = JSON.parse(text); }
155
+ catch { record('smoke-2-sampling', 'FAIL', `non-JSON server_status: ${text.slice(0, 120)}`); return; }
156
+ if (parsed.sampling === true) {
157
+ record('smoke-2-sampling', 'PASS', `server_status.sampling=true (transport=${parsed.transport})`);
158
+ } else {
159
+ // Server didn't reflect the capability — this is a real finding, not a skip,
160
+ // because we explicitly declared { sampling: {} } in the client init options.
161
+ record('smoke-2-sampling', 'FAIL',
162
+ `client declared sampling but server_status.sampling=${parsed.sampling}. ` +
163
+ `Check resolveAdapter / capability propagation in server.js.`);
164
+ }
165
+ } catch (e) {
166
+ record('smoke-2-sampling', 'FAIL', e.message);
167
+ } finally {
168
+ try { await client.close(); } catch {}
169
+ }
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Smoke 3 — New tools registered & callable
174
+ // ---------------------------------------------------------------------------
175
+ async function smokeNewTools() {
176
+ const transport = new StdioClientTransport({
177
+ command: 'node',
178
+ args: [serverPath],
179
+ stderr: 'ignore',
180
+ });
181
+ const client = new Client(
182
+ { name: 'smoke-extended-tools', version: '0.0.1' },
183
+ { capabilities: {} },
184
+ );
185
+
186
+ const want = ['list_patterns', 'server_status', 'refine_ui'];
187
+ const failures = [];
188
+ try {
189
+ await client.connect(transport);
190
+ const tl = await client.listTools();
191
+ const names = new Set(tl.tools.map((t) => t.name));
192
+ const missing = want.filter((n) => !names.has(n));
193
+ if (missing.length > 0) {
194
+ record('smoke-3-tools', 'FAIL', `missing from tools/list: ${missing.join(', ')}`);
195
+ return;
196
+ }
197
+
198
+ // list_patterns — should return JSON with a `patterns` array.
199
+ {
200
+ const r = await client.callTool({ name: 'list_patterns', arguments: {} });
201
+ const text = r.content?.[0]?.text ?? '';
202
+ try {
203
+ const p = JSON.parse(text);
204
+ if (!Array.isArray(p.patterns)) failures.push(`list_patterns: no patterns array (got keys: ${Object.keys(p).join(',')})`);
205
+ } catch (e) {
206
+ failures.push(`list_patterns: non-JSON response (${e.message})`);
207
+ }
208
+ }
209
+
210
+ // server_status — should return JSON with version + transport + sampling fields.
211
+ {
212
+ const r = await client.callTool({ name: 'server_status', arguments: {} });
213
+ const text = r.content?.[0]?.text ?? '';
214
+ try {
215
+ const s = JSON.parse(text);
216
+ for (const k of ['version', 'transport', 'sampling', 'corpus']) {
217
+ if (!(k in s)) failures.push(`server_status: missing field "${k}"`);
218
+ }
219
+ } catch (e) {
220
+ failures.push(`server_status: non-JSON response (${e.message})`);
221
+ }
222
+ }
223
+
224
+ // refine_ui — invoke with minimal args. Without an LLM adapter / API keys
225
+ // this may return an error inside content (that's fine — tool didn't throw).
226
+ // A thrown JSON-RPC error is a hard failure.
227
+ try {
228
+ const r = await client.callTool({
229
+ name: 'refine_ui',
230
+ arguments: {
231
+ intent: 'smoke test',
232
+ previousMessages: [],
233
+ validationErrors: [{ code: 'smoke', message: 'noop' }],
234
+ },
235
+ });
236
+ const text = r.content?.[0]?.text ?? '';
237
+ // Accept either a parseable JSON object (success or {error:...}) or any
238
+ // string — what matters is that the tool ran without throwing at the
239
+ // transport layer.
240
+ if (!text) failures.push('refine_ui: empty content array');
241
+ } catch (e) {
242
+ // Allow LLM-adapter-related errors (no API key in CI) since the tool's
243
+ // schema-side is what we're smoking, not the LLM call.
244
+ const msg = String(e?.message || e);
245
+ if (!/adapter|api.?key|ANTHROPIC|OPENAI|provider|model/i.test(msg)) {
246
+ failures.push(`refine_ui: unexpected throw: ${msg}`);
247
+ } else {
248
+ console.log(` (refine_ui: tolerated LLM-adapter error — ${msg.slice(0, 80)})`);
249
+ }
250
+ }
251
+
252
+ if (failures.length === 0) {
253
+ record('smoke-3-tools', 'PASS', `${want.join(', ')} all callable`);
254
+ } else {
255
+ record('smoke-3-tools', 'FAIL', failures.join('; '));
256
+ }
257
+ } catch (e) {
258
+ record('smoke-3-tools', 'FAIL', e.message);
259
+ } finally {
260
+ try { await client.close(); } catch {}
261
+ }
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Run
266
+ // ---------------------------------------------------------------------------
267
+ console.log('=== MCP extended smoke ===');
268
+ await smokeHttp();
269
+ await smokeSampling();
270
+ await smokeNewTools();
271
+
272
+ const failed = results.filter((r) => r.status === 'FAIL');
273
+ console.log(`\nSummary: ${results.length - failed.length}/${results.length} passed (${failed.length} failed)`);
274
+ process.exit(failed.length === 0 ? 0 : 1);