@adia-ai/a2ui-mcp 0.6.6 → 0.6.8
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 +36 -0
- package/README.md +29 -5
- package/package.json +9 -3
- package/scripts/smoke-extended.mjs +274 -0
- package/server.js +83 -6
- package/tools/discovery.js +89 -0
- package/tools/refine.js +151 -0
- package/tools/corpus.ts +0 -112
- package/tools/feedback.ts +0 -73
- package/tools/synthesis.ts +0 -449
- package/tools/validation.ts +0 -153
- package/tools/zettel.ts +0 -98
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog — @adia-ai/a2ui-mcp
|
|
2
2
|
|
|
3
|
+
## [0.6.8] — 2026-05-20
|
|
4
|
+
|
|
5
|
+
- Lockstep version bump. Headlining work in `@adia-ai/web-components`
|
|
6
|
+
(FB-55 `.camelCaseProp=${expr}` two-layer name resolution + FB-57
|
|
7
|
+
SVG/MathML namespace-aware `mount()` + USAGE.md docs cluster) and
|
|
8
|
+
`@adia-ai/web-modules` (claims-ui F-001 chat-shell.js barrel-import
|
|
9
|
+
fix + F-006 required-CSS callouts). No source changes in this package.
|
|
10
|
+
|
|
11
|
+
## [0.6.7] — 2026-05-19
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **`list_patterns` tool** — enumerate the full A2UI composition corpus
|
|
15
|
+
with optional `domain` / `category` filters. Was previously only
|
|
16
|
+
searchable by keyword via `search_chunks` / `search_patterns`.
|
|
17
|
+
- **`server_status` tool** — reports transport (stdio/http), sampling
|
|
18
|
+
capability, corpus stats (components, compositions, chunks), and
|
|
19
|
+
version. Useful for consumer health checks and observability.
|
|
20
|
+
- **`refine_ui` tool** — retry monolithic-engine generation with validation
|
|
21
|
+
errors fed back to the LLM. Closes the missing-refine gap for the
|
|
22
|
+
monolithic path (zettel already had `refine_composition`).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **HTTP transport body-parsing** — `transport.handleRequest(req, res)` was
|
|
26
|
+
not passing the express-parsed body; SDK then errored `-32700 Parse error`
|
|
27
|
+
on every request. Fixed by passing `req.body` as the third argument.
|
|
28
|
+
Surfaced by new HTTP smoke test.
|
|
29
|
+
- **Excluded `.ts` source files from npm tarball** — TypeScript sources
|
|
30
|
+
were shipping alongside the emitted `.js` (7 files in mcp/, 1 in compose/,
|
|
31
|
+
16 in retrieval/). Now filtered via `files` field while keeping `.d.ts`
|
|
32
|
+
declarations.
|
|
33
|
+
- **Removed unused `sessionServer` instantiation in HTTP transport** —
|
|
34
|
+
previously the HTTP request handler created a per-session `McpServer`
|
|
35
|
+
instance but never used it (called `server.connect()` against the outer
|
|
36
|
+
server). Removed the dead allocation; comment updated to reflect that
|
|
37
|
+
the outer server is shared across sessions (tools are stateless).
|
|
38
|
+
|
|
3
39
|
## [0.6.6] — 2026-05-18
|
|
4
40
|
|
|
5
41
|
### 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
|
|
6
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.6.8",
|
|
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);
|
package/server.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import "../../../scripts/load-env.mjs";
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
7
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
5
9
|
import { z } from "zod";
|
|
6
10
|
import { generateUI } from "../compose/core/generator.js";
|
|
7
11
|
import {
|
|
@@ -36,10 +40,40 @@ import { registerValidationTools } from "./tools/validation.js";
|
|
|
36
40
|
import { registerFeedbackTools } from "./tools/feedback.js";
|
|
37
41
|
import { registerCorpusTools } from "./tools/corpus.js";
|
|
38
42
|
import { registerZettelTools } from "./tools/zettel.js";
|
|
43
|
+
import { registerDiscoveryTools } from "./tools/discovery.js";
|
|
44
|
+
import { registerRefineTools } from "./tools/refine.js";
|
|
39
45
|
const server = new McpServer({
|
|
40
46
|
name: "adia-ui",
|
|
41
|
-
version: "0.1.0"
|
|
47
|
+
version: "0.1.0",
|
|
48
|
+
capabilities: {
|
|
49
|
+
sampling: {}
|
|
50
|
+
// allows server to request LLM inference from the host (IDE/client)
|
|
51
|
+
}
|
|
42
52
|
});
|
|
53
|
+
async function resolveAdapter() {
|
|
54
|
+
const hasSampling = server.server?._clientCapabilities?.sampling;
|
|
55
|
+
if (hasSampling) {
|
|
56
|
+
return {
|
|
57
|
+
async complete({ messages, systemPrompt }) {
|
|
58
|
+
const result = await server.server.createMessage({
|
|
59
|
+
messages: messages.map((m) => ({
|
|
60
|
+
role: m.role,
|
|
61
|
+
content: { type: "text", text: m.content }
|
|
62
|
+
})),
|
|
63
|
+
...systemPrompt ? { systemPrompt } : {},
|
|
64
|
+
maxTokens: 32768
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
content: typeof result.content === "string" ? result.content : result.content?.text ?? "",
|
|
68
|
+
stopReason: result.stopReason ?? "end",
|
|
69
|
+
usage: { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const { createAdapter } = await import("../../llm/llm-bridge.js");
|
|
75
|
+
return createAdapter();
|
|
76
|
+
}
|
|
43
77
|
server.tool(
|
|
44
78
|
"plan_app_state",
|
|
45
79
|
`Analyze a natural language prompt and extract the top-level Generative UI Ontology structures (Intent, Domain, Tasks, Experience).
|
|
@@ -49,8 +83,7 @@ Use this tool BEFORE generating UI to ensure you have walked the Reasoning Ladde
|
|
|
49
83
|
prompt: z.string().describe('The natural language request (e.g., "Build a dashboard for incoming sales leads")')
|
|
50
84
|
},
|
|
51
85
|
async ({ prompt }) => {
|
|
52
|
-
const
|
|
53
|
-
const llm = await createAdapter();
|
|
86
|
+
const llm = await resolveAdapter();
|
|
54
87
|
const systemPrompt = `You are the A2UI Ontology Planner.
|
|
55
88
|
Given a user prompt, you must extract the Core App State using the 5-Gate Reasoning Ladder.
|
|
56
89
|
|
|
@@ -137,13 +170,15 @@ The generator knows 96+ UI patterns across 5 domains: forms, data, layout, agent
|
|
|
137
170
|
try {
|
|
138
171
|
const selectedEngine = engine ?? "monolithic";
|
|
139
172
|
const effectiveMode = selectedEngine === "zettel" ? "instant" : mode ?? "pro";
|
|
173
|
+
const llmAdapter = effectiveMode !== "instant" ? await resolveAdapter() : void 0;
|
|
140
174
|
const result = await generateUI({
|
|
141
175
|
intent,
|
|
142
176
|
engine: selectedEngine,
|
|
143
177
|
mode: effectiveMode,
|
|
144
178
|
sessionId,
|
|
145
|
-
context
|
|
179
|
+
context,
|
|
146
180
|
// Pass the ontology context down to the composer
|
|
181
|
+
...llmAdapter ? { llmAdapter } : {}
|
|
147
182
|
});
|
|
148
183
|
return {
|
|
149
184
|
content: [{
|
|
@@ -265,11 +300,53 @@ registerFeedbackTools(server);
|
|
|
265
300
|
registerCorpusTools(server);
|
|
266
301
|
registerZettelTools(server);
|
|
267
302
|
registerSynthesisTools(server);
|
|
268
|
-
|
|
303
|
+
registerDiscoveryTools(server);
|
|
304
|
+
registerRefineTools(server);
|
|
305
|
+
async function startStdio() {
|
|
269
306
|
const transport = new StdioServerTransport();
|
|
270
307
|
await server.connect(transport);
|
|
271
308
|
const catalog = await getCatalog();
|
|
272
309
|
const traits = getTraits();
|
|
273
|
-
console.error(`
|
|
310
|
+
console.error(`[adiaui-mcp] stdio transport ready (${catalog.totalTypes} components, ${traits.length} traits, ${_zettelBoot.compositionCount} compositions)`);
|
|
311
|
+
}
|
|
312
|
+
async function startHttp(port) {
|
|
313
|
+
const transports = /* @__PURE__ */ new Map();
|
|
314
|
+
const app = createMcpExpressApp({ host: "0.0.0.0" });
|
|
315
|
+
app.all("/mcp", async (req, res) => {
|
|
316
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
317
|
+
if (sessionId && transports.has(sessionId)) {
|
|
318
|
+
const existing = transports.get(sessionId);
|
|
319
|
+
await existing.handleRequest(req, res, req.body);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (req.method === "POST" && isInitializeRequest(req.body)) {
|
|
323
|
+
const id = randomUUID();
|
|
324
|
+
const transport = new StreamableHTTPServerTransport({
|
|
325
|
+
sessionIdGenerator: () => id,
|
|
326
|
+
onsessioninitialized: (sid) => transports.set(sid, transport)
|
|
327
|
+
});
|
|
328
|
+
transport.onclose = () => transports.delete(id);
|
|
329
|
+
await server.connect(transport);
|
|
330
|
+
await transport.handleRequest(req, res, req.body);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
res.status(400).json({ error: "Invalid request \u2014 missing session or not an initialize request" });
|
|
334
|
+
});
|
|
335
|
+
app.listen(port, () => {
|
|
336
|
+
const catalog = getCatalog();
|
|
337
|
+
console.error(`[adiaui-mcp] HTTP transport ready on http://0.0.0.0:${port}/mcp`);
|
|
338
|
+
console.error(`[adiaui-mcp] API keys required for pro/thinking mode (no sampling in HTTP mode)`);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async function main() {
|
|
342
|
+
const httpPort = typeof process !== "undefined" && process.env?.MCP_HTTP_PORT ? parseInt(process.env.MCP_HTTP_PORT, 10) : null;
|
|
343
|
+
if (httpPort) {
|
|
344
|
+
await startHttp(httpPort);
|
|
345
|
+
} else {
|
|
346
|
+
await startStdio();
|
|
347
|
+
}
|
|
274
348
|
}
|
|
275
349
|
main().catch(console.error);
|
|
350
|
+
export {
|
|
351
|
+
resolveAdapter
|
|
352
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getCatalog } from "../../retrieval/catalog.js";
|
|
3
|
+
import {
|
|
4
|
+
getAllCompositions
|
|
5
|
+
} from "../../compose/strategies/zettel/composition-library.js";
|
|
6
|
+
import { getChunk, getChunkIndex } from "../../corpus/scripts/chunk-library.js";
|
|
7
|
+
function registerDiscoveryTools(server) {
|
|
8
|
+
server.tool(
|
|
9
|
+
"list_patterns",
|
|
10
|
+
`List all composition patterns in the A2UI corpus. Optional filters narrow by domain (auth, settings, dashboard, etc.) or category (block, page, flow).`,
|
|
11
|
+
{
|
|
12
|
+
domain: z.string().optional().describe('Filter by domain (e.g. "forms", "data", "navigation")'),
|
|
13
|
+
category: z.string().optional().describe('Filter by category ("block", "page", "flow")')
|
|
14
|
+
},
|
|
15
|
+
async ({ domain, category }) => {
|
|
16
|
+
const all = getAllCompositions();
|
|
17
|
+
let filtered = all;
|
|
18
|
+
if (domain) filtered = filtered.filter((c) => c.domain === domain);
|
|
19
|
+
if (category) {
|
|
20
|
+
filtered = filtered.filter((c) => {
|
|
21
|
+
const raw = c.name ? getChunk(c.name) : null;
|
|
22
|
+
const k = raw?.kind ?? c.kind;
|
|
23
|
+
return k === category;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: JSON.stringify(
|
|
31
|
+
{
|
|
32
|
+
total: filtered.length,
|
|
33
|
+
patterns: filtered.map((c) => {
|
|
34
|
+
const raw = c.name ? getChunk(c.name) : null;
|
|
35
|
+
return {
|
|
36
|
+
name: c.name,
|
|
37
|
+
domain: c.domain,
|
|
38
|
+
kind: raw?.kind ?? c.kind ?? "composition",
|
|
39
|
+
description: c.description,
|
|
40
|
+
keywords: c.keywords ?? []
|
|
41
|
+
};
|
|
42
|
+
})
|
|
43
|
+
},
|
|
44
|
+
null,
|
|
45
|
+
2
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
server.tool(
|
|
53
|
+
"server_status",
|
|
54
|
+
`Returns operational status of the MCP server: transport, sampling capability, corpus stats, version.`,
|
|
55
|
+
{},
|
|
56
|
+
async () => {
|
|
57
|
+
const catalog = await getCatalog();
|
|
58
|
+
const compositionCount = getAllCompositions().length;
|
|
59
|
+
const chunkIndex = getChunkIndex();
|
|
60
|
+
const chunkCount = chunkIndex ? chunkIndex["unique_names"] ?? null : null;
|
|
61
|
+
const hasSampling = server.server?._clientCapabilities?.sampling ? true : false;
|
|
62
|
+
const transport = typeof process !== "undefined" && process.env?.MCP_HTTP_PORT ? "http" : "stdio";
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: JSON.stringify(
|
|
68
|
+
{
|
|
69
|
+
version: "0.1.0",
|
|
70
|
+
transport,
|
|
71
|
+
sampling: hasSampling,
|
|
72
|
+
corpus: {
|
|
73
|
+
totalComponents: catalog.totalTypes ?? null,
|
|
74
|
+
compositionCount,
|
|
75
|
+
chunkCount
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
null,
|
|
79
|
+
2
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
registerDiscoveryTools
|
|
89
|
+
};
|