@adia-ai/mcp 0.8.37 → 0.8.39
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 +44 -27
- package/README.md +94 -29
- package/TOOLS.md +178 -16
- package/bin/adia-mcp +17 -8
- package/factory/public-surface.json +20 -0
- package/factory/resources/data-wiring.md +108 -0
- package/factory/resources/pattern-index.md +786 -0
- package/factory/resources/shell-selection.md +86 -0
- package/factory/resources/token-pairing-laws.md +68 -0
- package/factory/server.d.ts +12 -0
- package/factory/server.js +72 -0
- package/factory/tools/factory.js +277 -0
- package/factory/vendor/MANIFEST.json +29 -0
- package/factory/vendor/adia-contract-check.mjs +432 -0
- package/factory/vendor/adia-info +312 -0
- package/factory/vendor/adia-lint +396 -0
- package/factory/vendor/adia-probe.mjs +413 -0
- package/factory/vendor/adia-scaffold +801 -0
- package/factory/vendor/record-lint +278 -0
- package/gen-ui/server.d.ts +1 -1
- package/gen-ui/tools/corpus.js +1 -1
- package/gen-ui/tools/discovery.js +82 -0
- package/gen-ui/tools/feedback.js +1 -1
- package/package.json +13 -3
- package/protocol/server.d.ts +3 -2
- package/protocol/server.js +1 -1
- package/protocol/tools/protocol.js +123 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""record-lint — mechanical gate for app-planning's Orientation Record.
|
|
3
|
+
|
|
4
|
+
The record is app-planning's typed deliverable (SKILL.md §The Orientation
|
|
5
|
+
Record) and the app-planning-agent → screen-composition-agent handoff artifact. Until
|
|
6
|
+
this linter existed, nothing checked a record's shape — and the record's
|
|
7
|
+
three in-tree consumers had already drifted from the contract (the
|
|
8
|
+
factory-audit finding this closes, gh#259 Wave 2).
|
|
9
|
+
|
|
10
|
+
Checks (the contract's mechanizable slice — shape, enums, evidence,
|
|
11
|
+
route-legality; whether a SIGNAL is genuinely load-bearing stays the
|
|
12
|
+
model's judgment):
|
|
13
|
+
1. Required lines present: Rendering mode / Project shape / Shell /
|
|
14
|
+
Task / → Route / Verify target / Open questions. Screen plan is
|
|
15
|
+
conditional (start mode) — its absence is never an error.
|
|
16
|
+
2. Axis enums: mode ∈ {SPA, SSR, hybrid} · shape ∈ {single-surface,
|
|
17
|
+
rollup, shared-foundation} · shell ∈ {admin, chat, editor, simple,
|
|
18
|
+
embed, none}.
|
|
19
|
+
3. Every axis line carries a non-empty `— signal:` clause that isn't a
|
|
20
|
+
bare restatement of the value.
|
|
21
|
+
4. Route names only skills that exist in this plugin (the task table's
|
|
22
|
+
roster), or app-planning itself.
|
|
23
|
+
5. Every Open-questions entry names its fallback (`fallback:`).
|
|
24
|
+
6. Verify target non-empty and not TBD-ish.
|
|
25
|
+
7. Domain Plan (gh#1207, REQ-02) — CONDITIONAL: absent is never an
|
|
26
|
+
error (spec-shaped input only, mirrors Screen plan's conditionality).
|
|
27
|
+
When a `## Domain Plan` heading IS present, its required sub-fields
|
|
28
|
+
(Intent / Domain / Roles / Tasks / Decisions / Wireframe) must each
|
|
29
|
+
appear non-empty, and Wireframe must name a checkpoint verdict
|
|
30
|
+
(PASSED or FAILED).
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
record-lint <file> # lint a file containing a record
|
|
34
|
+
record-lint - # lint stdin
|
|
35
|
+
record-lint selftest # embedded fixtures; exit 0 iff all pass
|
|
36
|
+
Exit 1 on findings, 0 clean. Stdlib only (Python 3.8+).
|
|
37
|
+
"""
|
|
38
|
+
import re
|
|
39
|
+
import sys
|
|
40
|
+
|
|
41
|
+
AXES = {
|
|
42
|
+
'Rendering mode': {'SPA', 'SSR', 'hybrid'},
|
|
43
|
+
'Project shape': {'single-surface', 'rollup', 'shared-foundation'},
|
|
44
|
+
'Shell': {'admin', 'chat', 'editor', 'simple', 'embed', 'none'},
|
|
45
|
+
}
|
|
46
|
+
REQUIRED = ['Rendering mode', 'Project shape', 'Shell', 'Task', '→ Route', 'Verify target', 'Open questions']
|
|
47
|
+
ROUTE_SKILLS = {
|
|
48
|
+
'app-planning', 'project-scaffolding', 'screen-composition', 'shell-selection', 'host-wiring',
|
|
49
|
+
'data-wiring', 'llm-wiring', 'gen-ui-wiring', 'surface-qa', 'app-migration',
|
|
50
|
+
}
|
|
51
|
+
TBD_RE = re.compile(r'^\s*(tbd|todo|\?+|—|-)\s*$', re.I)
|
|
52
|
+
|
|
53
|
+
# gh#1207 REQ-02 — the Orientation Record's CONDITIONAL Domain Plan block
|
|
54
|
+
# (spec-shaped input only; app-planning SKILL.md §The Orientation Record).
|
|
55
|
+
# Shape (sketch: .claude/docs/specs/factory-roster-completion.md):
|
|
56
|
+
# ## Domain Plan (rungs 0–19, ladder vX)
|
|
57
|
+
# - Intent: ... - Domain: ... - Roles: ... - Tasks: ... - Decisions: ...
|
|
58
|
+
# - Wireframe: ... checkpoint PASSED|FAILED
|
|
59
|
+
DOMAIN_PLAN_HEADING_RE = re.compile(r'^##\s*Domain Plan\b.*$', re.M)
|
|
60
|
+
DOMAIN_PLAN_FIELDS = ['Intent', 'Domain', 'Roles', 'Tasks', 'Decisions', 'Wireframe']
|
|
61
|
+
DOMAIN_PLAN_FIELD_RE = re.compile(
|
|
62
|
+
r'-\s*(' + '|'.join(DOMAIN_PLAN_FIELDS) + r')\s*:[ \t]*(.*?)'
|
|
63
|
+
r'(?=(?:[ \t]+-\s*(?:' + '|'.join(DOMAIN_PLAN_FIELDS) + r')\s*:)|\n|$)'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def lint(text):
|
|
68
|
+
findings = []
|
|
69
|
+
lines = {}
|
|
70
|
+
for raw in text.splitlines():
|
|
71
|
+
m = re.match(r'^\s*(→ Route|[A-Z][\w ]+?):\s*(.*)$', raw)
|
|
72
|
+
if m:
|
|
73
|
+
lines.setdefault(m.group(1).strip(), []).append(m.group(2).strip())
|
|
74
|
+
|
|
75
|
+
for label in REQUIRED:
|
|
76
|
+
if label not in lines:
|
|
77
|
+
findings.append(f"missing line: '{label}:'")
|
|
78
|
+
|
|
79
|
+
for axis, enum in AXES.items():
|
|
80
|
+
for value in lines.get(axis, []):
|
|
81
|
+
head = value.split('—')[0].strip()
|
|
82
|
+
sig = re.search(r'—\s*signal:\s*(.*)$', value)
|
|
83
|
+
if head not in enum:
|
|
84
|
+
findings.append(f"{axis}: '{head}' not in {sorted(enum)}")
|
|
85
|
+
if not sig or not sig.group(1).strip():
|
|
86
|
+
findings.append(f"{axis}: no '— signal:' clause (Evidence gate)")
|
|
87
|
+
elif head and head.lower() in sig.group(1).strip().lower() and len(sig.group(1).strip()) <= len(head) + 12:
|
|
88
|
+
findings.append(f"{axis}: signal restates the value ('{sig.group(1).strip()}')")
|
|
89
|
+
|
|
90
|
+
for value in lines.get('→ Route', []):
|
|
91
|
+
# kebab tokens that are either current factory skills or legacy
|
|
92
|
+
# adia-* names (the latter flag as not-a-factory-skill below, so an
|
|
93
|
+
# un-migrated record fails loudly instead of passing silently)
|
|
94
|
+
tokens = set(re.findall(r'[a-z][a-z0-9-]*[a-z0-9]', value))
|
|
95
|
+
named = {t for t in tokens if t in ROUTE_SKILLS or t.startswith('adia-')}
|
|
96
|
+
for skill in named - ROUTE_SKILLS:
|
|
97
|
+
findings.append(f"Route: '{skill}' is not a factory skill (Route-legal gate)")
|
|
98
|
+
if not named:
|
|
99
|
+
findings.append("Route: names no skill")
|
|
100
|
+
|
|
101
|
+
for value in lines.get('Verify target', []):
|
|
102
|
+
if not value or TBD_RE.match(value):
|
|
103
|
+
findings.append("Verify target: empty or TBD")
|
|
104
|
+
|
|
105
|
+
for value in lines.get('Open questions', []):
|
|
106
|
+
if value and not TBD_RE.match(value) and value.lower() not in ('none', 'blank if none', ''):
|
|
107
|
+
if 'fallback' not in value.lower():
|
|
108
|
+
findings.append(f"Open questions: entry lacks a named fallback: '{value[:50]}'")
|
|
109
|
+
|
|
110
|
+
findings.extend(_lint_domain_plan(text))
|
|
111
|
+
return findings
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _lint_domain_plan(text):
|
|
115
|
+
"""gh#1207 REQ-02 — conditional Domain Plan block. Absence is never a
|
|
116
|
+
finding (never required); presence gates its own required sub-fields."""
|
|
117
|
+
heading = DOMAIN_PLAN_HEADING_RE.search(text)
|
|
118
|
+
if not heading:
|
|
119
|
+
return []
|
|
120
|
+
start = heading.end()
|
|
121
|
+
next_heading = re.search(r'\n##\s', text[start:])
|
|
122
|
+
block = text[start:start + next_heading.start()] if next_heading else text[start:]
|
|
123
|
+
|
|
124
|
+
found = {}
|
|
125
|
+
for fm in DOMAIN_PLAN_FIELD_RE.finditer(block):
|
|
126
|
+
found.setdefault(fm.group(1), fm.group(2).strip())
|
|
127
|
+
|
|
128
|
+
findings = []
|
|
129
|
+
for field in DOMAIN_PLAN_FIELDS:
|
|
130
|
+
if field not in found:
|
|
131
|
+
findings.append(f"Domain Plan: missing '{field}:' field")
|
|
132
|
+
elif not found[field]:
|
|
133
|
+
findings.append(f"Domain Plan: '{field}:' is empty")
|
|
134
|
+
|
|
135
|
+
wireframe = found.get('Wireframe', '')
|
|
136
|
+
if wireframe and not re.search(r'\b(PASSED|FAILED)\b', wireframe, re.I):
|
|
137
|
+
findings.append("Domain Plan: 'Wireframe:' names no checkpoint verdict (PASSED/FAILED)")
|
|
138
|
+
|
|
139
|
+
return findings
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
GOOD = """\
|
|
143
|
+
Rendering mode: SPA — signal: no framework dep in package.json; index.html present
|
|
144
|
+
Project shape: single-surface — signal: one entry under app/claims/
|
|
145
|
+
Shell: admin — signal: sidebar + topbar + command palette in the brief
|
|
146
|
+
Task: build a claims-review screen — signal: the request
|
|
147
|
+
Screen plan: 1. Claims list — reviewer scans and opens a claim
|
|
148
|
+
→ Route: project-scaffolding, screen-composition, in order per the task table
|
|
149
|
+
Verify target: composed screen renders; adia-lint clean; browser gate
|
|
150
|
+
Open questions: auth model unclear — fallback: mock session until the API lands
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
BAD = """\
|
|
154
|
+
Rendering mode: Static — signal: SPA
|
|
155
|
+
Shell: admin
|
|
156
|
+
→ Route: adia-scaffolding
|
|
157
|
+
Verify target: TBD
|
|
158
|
+
Open questions: auth model unclear
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
# gh#1207 REQ-02 — GOOD_WITH_DOMAIN_PLAN proves a spec-shaped record with a
|
|
162
|
+
# complete, well-formed Domain Plan block stays clean (the block is
|
|
163
|
+
# conditional, never required — GOOD above already proves a plain record
|
|
164
|
+
# with NO block is clean). Uses the spec sketch's own compressed
|
|
165
|
+
# "field: value - field: value" line shape for Roles/Tasks to prove the
|
|
166
|
+
# parser handles it, not just one-field-per-line.
|
|
167
|
+
GOOD_WITH_DOMAIN_PLAN = GOOD + """
|
|
168
|
+
## Domain Plan (rungs 0–19, ladder v1)
|
|
169
|
+
- Intent: reviewers triage claims fast · cut time-to-resolution
|
|
170
|
+
- Domain: claim, adjuster, SLA · open-claims-count, avg-resolution-hours
|
|
171
|
+
- Roles: adjuster, supervisor - Tasks: triage queue · escalate stuck claims
|
|
172
|
+
- Decisions: assign / escalate / close, on priority + SLA risk
|
|
173
|
+
- Wireframe: sidebar + queue + drawer, D1-D6 all ≥ 3, checkpoint PASSED
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
# BAD_DOMAIN_PLAN: axis lines are fine (reuses GOOD) but the Domain Plan
|
|
177
|
+
# block is missing Decisions entirely, leaves Roles empty, and never names
|
|
178
|
+
# a checkpoint verdict — each must be caught independently.
|
|
179
|
+
BAD_DOMAIN_PLAN = GOOD + """
|
|
180
|
+
## Domain Plan (rungs 0–19, ladder v1)
|
|
181
|
+
- Intent: reviewers triage claims fast · cut time-to-resolution
|
|
182
|
+
- Domain: claim, adjuster, SLA · open-claims-count, avg-resolution-hours
|
|
183
|
+
- Roles:
|
|
184
|
+
- Tasks: triage queue · escalate stuck claims
|
|
185
|
+
- Wireframe: sidebar + queue + drawer, D1-D6 all ≥ 3
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
# gh#1215 review F1 — the spec sketch (above, "Shape") separates compressed
|
|
189
|
+
# fields with a SINGLE space; the boundary lookahead must accept one-or-more
|
|
190
|
+
# spaces/tabs, not two-or-more, or a sketch-faithful record falsely fails.
|
|
191
|
+
GOOD_SINGLE_SPACE_PLAN = GOOD + """
|
|
192
|
+
## Domain Plan (rungs 0–19, ladder v1)
|
|
193
|
+
- Intent: reviewers triage claims fast - Domain: claim, adjuster, SLA - Roles: adjuster, supervisor - Tasks: triage queue - Decisions: assign / escalate, on SLA risk
|
|
194
|
+
- Wireframe: sidebar + queue + drawer, checkpoint PASSED
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
GOOD_SINGLE_TAB_PLAN = GOOD + """
|
|
198
|
+
## Domain Plan (rungs 0–19, ladder v1)
|
|
199
|
+
- Intent: reviewers triage claims fast - Domain: claim, adjuster, SLA - Roles: adjuster, supervisor - Tasks: triage queue - Decisions: assign / escalate, on SLA risk
|
|
200
|
+
- Wireframe: sidebar + queue + drawer, checkpoint PASSED
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _selftest():
|
|
205
|
+
import io
|
|
206
|
+
|
|
207
|
+
fails = []
|
|
208
|
+
if lint(GOOD):
|
|
209
|
+
fails.append(f"good fixture flagged: {lint(GOOD)}")
|
|
210
|
+
bad = lint(BAD)
|
|
211
|
+
expect = ['missing line', "not in", "no '— signal:'", 'not a factory skill', 'empty or TBD', 'lacks a named fallback']
|
|
212
|
+
for e in expect:
|
|
213
|
+
if not any(e in f for f in bad):
|
|
214
|
+
fails.append(f"bad fixture missed: {e} (got {bad})")
|
|
215
|
+
|
|
216
|
+
# gh#1207 REQ-02 — Domain Plan block: conditional-pass + presence-checks.
|
|
217
|
+
if lint(GOOD_WITH_DOMAIN_PLAN):
|
|
218
|
+
fails.append(f"good-with-domain-plan fixture flagged: {lint(GOOD_WITH_DOMAIN_PLAN)}")
|
|
219
|
+
# gh#1215 review F1 — single-space and single-tab compressed records
|
|
220
|
+
# (the sketch's own separator) must parse clean.
|
|
221
|
+
for name, fixture in (('single-space', GOOD_SINGLE_SPACE_PLAN), ('single-tab', GOOD_SINGLE_TAB_PLAN)):
|
|
222
|
+
got = lint(fixture)
|
|
223
|
+
if got:
|
|
224
|
+
fails.append(f'{name} compressed Domain Plan fixture flagged: {got}')
|
|
225
|
+
bad_plan = lint(BAD_DOMAIN_PLAN)
|
|
226
|
+
expect_plan = ["missing 'Decisions:'", "'Roles:' is empty", "names no checkpoint verdict"]
|
|
227
|
+
for e in expect_plan:
|
|
228
|
+
if not any(e in f for f in bad_plan):
|
|
229
|
+
fails.append(f"bad-domain-plan fixture missed: {e} (got {bad_plan})")
|
|
230
|
+
|
|
231
|
+
# REQ-05 (gh#1136): -h/--help exits 0 and prints usage — never a crash.
|
|
232
|
+
for flag in ('-h', '--help'):
|
|
233
|
+
saved_out = sys.stdout
|
|
234
|
+
sys.stdout = io.StringIO()
|
|
235
|
+
try:
|
|
236
|
+
rc_help = main(['record-lint', flag])
|
|
237
|
+
help_out = sys.stdout.getvalue()
|
|
238
|
+
finally:
|
|
239
|
+
sys.stdout = saved_out
|
|
240
|
+
if rc_help != 0:
|
|
241
|
+
fails.append(f'{flag} did not exit 0')
|
|
242
|
+
if not help_out.startswith('usage:'):
|
|
243
|
+
fails.append(f'{flag} did not print usage')
|
|
244
|
+
|
|
245
|
+
if fails:
|
|
246
|
+
print('selftest FAIL: ' + ' | '.join(fails), file=sys.stderr)
|
|
247
|
+
return 1
|
|
248
|
+
print(f'selftest OK — good fixture clean, bad fixture {len(bad)} findings, Domain Plan bad fixture {len(bad_plan)} findings, -h/--help contract holds')
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
_USAGE = 'usage: record-lint <file> | record-lint - | record-lint selftest'
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def main(argv):
|
|
256
|
+
if len(argv) > 1 and argv[1] == 'selftest':
|
|
257
|
+
return _selftest()
|
|
258
|
+
# REQ-05 (gh#1136, factory-dx-ws5-consumer-verify): -h/--help must print
|
|
259
|
+
# usage and exit 0. Previously unhandled — `-h` fell through to the
|
|
260
|
+
# positional-file branch and crashed with an uncaught FileNotFoundError
|
|
261
|
+
# (exit 1, a traceback dumped to stderr), the gh#1122 class taken further.
|
|
262
|
+
if len(argv) > 1 and argv[1] in ('-h', '--help'):
|
|
263
|
+
print(_USAGE)
|
|
264
|
+
return 0
|
|
265
|
+
text = sys.stdin.read() if (len(argv) < 2 or argv[1] == '-') else open(argv[1], encoding='utf-8').read()
|
|
266
|
+
findings = lint(text)
|
|
267
|
+
if findings:
|
|
268
|
+
print(f'record-lint · {len(findings)} finding(s):')
|
|
269
|
+
for f in findings:
|
|
270
|
+
print(f' {f}')
|
|
271
|
+
print('Contract: app-planning SKILL.md §The Orientation Record.')
|
|
272
|
+
return 1
|
|
273
|
+
print('record-lint · clean')
|
|
274
|
+
return 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == '__main__':
|
|
278
|
+
sys.exit(main(sys.argv))
|
package/gen-ui/server.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Builds the AdiaUI generation MCP server (
|
|
4
|
+
* Builds the AdiaUI generation MCP server (31 tools: generation, discovery,
|
|
5
5
|
* retrieval, synthesis, validation, feedback/eval) with no transport
|
|
6
6
|
* attached. Exported for tests and for `scripts/build/generate-mcp-tools-md.mjs`;
|
|
7
7
|
* this file's own top-level `main()` call (unconditional, at module scope —
|
package/gen-ui/tools/corpus.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "@adia-ai/gen-ui/corpus/chunk-library";
|
|
8
8
|
const SEARCH_CHUNKS_DESCRIPTION = `Search the gen-UI training-chunk corpus by keyword.
|
|
9
9
|
|
|
10
|
-
The chunk corpus comes from \`packages/gen-ui/corpus/chunks/\` \u2014 JSON records
|
|
10
|
+
The chunk corpus comes from \`packages/gen-ui/engine/corpus/chunks/\` \u2014 JSON records
|
|
11
11
|
extracted from every \`[data-chunk]\` element in site/pages/* and the corpus
|
|
12
12
|
exemplars. There are three kinds:
|
|
13
13
|
- block (default): atomic UI fragment (KPI grid, sign-in form, table)
|
|
@@ -4,7 +4,89 @@ import {
|
|
|
4
4
|
getAllCompositions
|
|
5
5
|
} from "@adia-ai/gen-ui/compose/strategies/zettel/composition-library";
|
|
6
6
|
import { getChunk, getChunkIndex } from "@adia-ai/gen-ui/corpus/chunk-library";
|
|
7
|
+
let _tierIndex = null;
|
|
8
|
+
async function loadTierIndex() {
|
|
9
|
+
if (_tierIndex) return _tierIndex;
|
|
10
|
+
const mod = await import("@adia-ai/a2ui/catalog/tiers", { with: { type: "json" } });
|
|
11
|
+
_tierIndex = mod.default ?? mod;
|
|
12
|
+
return _tierIndex;
|
|
13
|
+
}
|
|
14
|
+
const GET_CATALOG_TIERS_DESCRIPTION = `Enumerate the AdiaUI catalog's tier ladder (ADR-0050): L0 primitives, L1 widgets, L2 layouts, L3 shells, L4 flows. Every tier-N entry is a declared composition of tier-(N-1) entries, so \`composes\` is the ladder edge you follow to expand an entry into the rung below.
|
|
15
|
+
|
|
16
|
+
Called with no arguments it returns the ladder summary \u2014 per tier: entry unit, which tier it composes, whether it is populated or still reserved, and the entry count. Pass \`tier\` to list that tier's entry names; pass \`tier\` + \`name\` for one entry's full record.
|
|
17
|
+
|
|
18
|
+
This is the CONTRACT view (what exists and how it composes), served from \`@adia-ai/a2ui/catalog\`. For per-component prop schemas use \`get_component_map\` / \`lookup_component\`; for corpus fuel (harvested chunks) use \`search_chunks\`.`;
|
|
7
19
|
function registerDiscoveryTools(server) {
|
|
20
|
+
server.tool(
|
|
21
|
+
"get_catalog_tiers",
|
|
22
|
+
GET_CATALOG_TIERS_DESCRIPTION,
|
|
23
|
+
{
|
|
24
|
+
tier: z.enum(["L0", "L1", "L2", "L3", "L4"]).optional().describe("List one tier's entries instead of the ladder summary"),
|
|
25
|
+
name: z.string().optional().describe("With `tier`, return this single entry's full record")
|
|
26
|
+
},
|
|
27
|
+
async ({ tier, name }) => {
|
|
28
|
+
const index = await loadTierIndex();
|
|
29
|
+
if (!tier) {
|
|
30
|
+
const ladder = Object.fromEntries(
|
|
31
|
+
Object.entries(index.tiers).map(([id, t]) => [
|
|
32
|
+
id,
|
|
33
|
+
{ unit: t.unit, composesTier: t.composesTier, status: t.status, count: t.count }
|
|
34
|
+
])
|
|
35
|
+
);
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: JSON.stringify({ catalogId: index.catalogId, ladder }, null, 2)
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const rung = index.tiers[tier];
|
|
46
|
+
if (!rung) {
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text: `Unknown tier "${tier}"` }],
|
|
49
|
+
isError: true
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (name) {
|
|
53
|
+
const entry = rung.entries[name];
|
|
54
|
+
if (!entry) {
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: `No ${tier} entry named "${name}". ${rung.count} entries in this tier.`
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
isError: true
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: JSON.stringify({ tier, name, entry }, null, 2) }]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: JSON.stringify(
|
|
74
|
+
{
|
|
75
|
+
tier,
|
|
76
|
+
unit: rung.unit,
|
|
77
|
+
composesTier: rung.composesTier,
|
|
78
|
+
status: rung.status,
|
|
79
|
+
count: rung.count,
|
|
80
|
+
entries: Object.keys(rung.entries).sort()
|
|
81
|
+
},
|
|
82
|
+
null,
|
|
83
|
+
2
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
);
|
|
8
90
|
server.tool(
|
|
9
91
|
"list_patterns",
|
|
10
92
|
`List all composition patterns in the A2UI corpus. Optional filters narrow by domain (auth, settings, dashboard, etc.) or category (block, page, flow).`,
|
package/gen-ui/tools/feedback.js
CHANGED
|
@@ -11,7 +11,7 @@ function registerFeedbackTools(server) {
|
|
|
11
11
|
// TOOLS.md, which no MCP client could read. TOOLS.md is generated from here now.
|
|
12
12
|
`Submit structured feedback for a generation execution. Used by the evolution engine to learn from each generation.
|
|
13
13
|
|
|
14
|
-
Persists the rating to \`packages/gen-ui/corpus/feedback/<date>.jsonl\` through the shared \`submitFeedback\` path \u2014 the same one the gen-UI gallery's thumbs affordance posts to (gh#668). Optional \`engine\` / \`strategy\` / \`score\` / \`source\` context is carried onto the stored rating so human signal can rank weak domains.`,
|
|
14
|
+
Persists the rating to \`packages/gen-ui/engine/corpus/feedback/<date>.jsonl\` through the shared \`submitFeedback\` path \u2014 the same one the gen-UI gallery's thumbs affordance posts to (gh#668). Optional \`engine\` / \`strategy\` / \`score\` / \`source\` context is carried onto the stored rating so human signal can rank weak domains.`,
|
|
15
15
|
{
|
|
16
16
|
executionId: z.string().describe("Execution ID from generate_ui"),
|
|
17
17
|
rating: z.number().min(1).max(5).describe("Overall quality 1-5 (>=4 counts as a thumbs-up)"),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/mcp",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "AdiaUI's
|
|
3
|
+
"version": "0.8.39",
|
|
4
|
+
"description": "AdiaUI's three MCP servers, one npm package (gh#1240, ADR-0051). `adia-mcp gen-ui` — the 31-tool generation server (compose engine, corpus, retrieval, feedback/eval loop). `adia-mcp protocol` — the 5-tool A2UI protocol server (validate + registry introspection + the L0/L1 catalog-contract rungs, no generation system, no model client). `adia-mcp factory` — the 7-tool adia-factory server (orient, scaffold, audit, surface QA for building adia-ui apps from any MCP harness; no generation system, no model client). ADR-0048 §3's distinct-server decision is unchanged; only the distribution unified.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"adia-mcp": "./bin/adia-mcp"
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
"types": "./protocol/server.d.ts",
|
|
17
17
|
"import": "./protocol/server.js",
|
|
18
18
|
"default": "./protocol/server.js"
|
|
19
|
+
},
|
|
20
|
+
"./factory/server.js": {
|
|
21
|
+
"types": "./factory/server.d.ts",
|
|
22
|
+
"import": "./factory/server.js",
|
|
23
|
+
"default": "./factory/server.js"
|
|
19
24
|
}
|
|
20
25
|
},
|
|
21
26
|
"files": [
|
|
@@ -26,6 +31,11 @@
|
|
|
26
31
|
"gen-ui/tools/",
|
|
27
32
|
"protocol/server.js",
|
|
28
33
|
"protocol/tools/",
|
|
34
|
+
"factory/server.js",
|
|
35
|
+
"factory/tools/",
|
|
36
|
+
"factory/vendor/",
|
|
37
|
+
"factory/resources/",
|
|
38
|
+
"factory/public-surface.json",
|
|
29
39
|
"README.md",
|
|
30
40
|
"TOOLS.md",
|
|
31
41
|
"CHANGELOG.md",
|
|
@@ -41,7 +51,7 @@
|
|
|
41
51
|
"repository": {
|
|
42
52
|
"type": "git",
|
|
43
53
|
"url": "git+https://github.com/adiahealth/gen-ui-kit.git",
|
|
44
|
-
"directory": "packages/mcp"
|
|
54
|
+
"directory": "packages/gen-ui/mcp"
|
|
45
55
|
},
|
|
46
56
|
"dependencies": {
|
|
47
57
|
"@adia-ai/a2ui": "^0.8.0",
|
package/protocol/server.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Builds the A2UI protocol MCP server (
|
|
5
|
-
* get_registry_map, get_wiring_registry, protocol_status)
|
|
4
|
+
* Builds the A2UI protocol MCP server (5 tools: validate_document,
|
|
5
|
+
* get_registry_map, get_wiring_registry, get_catalog_ladder, protocol_status)
|
|
6
|
+
* with no transport
|
|
6
7
|
* attached. Exported for tests and for `scripts/build/generate-mcp-tools-md.mjs`;
|
|
7
8
|
* the module's own top-level `isEntryPoint()` guard is what actually starts a
|
|
8
9
|
* stdio transport when this file is run directly (`node server.js`), not when
|
package/protocol/server.js
CHANGED
|
@@ -16,7 +16,7 @@ async function main() {
|
|
|
16
16
|
const server = createServer();
|
|
17
17
|
const transport = new StdioServerTransport();
|
|
18
18
|
await server.connect(transport);
|
|
19
|
-
console.error("[a2ui-protocol-mcp] stdio transport ready (
|
|
19
|
+
console.error("[a2ui-protocol-mcp] stdio transport ready (5 protocol tools)");
|
|
20
20
|
}
|
|
21
21
|
function isEntryPoint() {
|
|
22
22
|
if (typeof process === "undefined" || !process.argv[1]) return false;
|
|
@@ -10,6 +10,11 @@ This is the registry view \u2014 the authoritative answer to "what types exist a
|
|
|
10
10
|
const GET_WIRING_REGISTRY_DESCRIPTION = `Get the A2UI wiring registry: controller types, action-handler names, and data-URI resolver schemes the runtime can resolve.
|
|
11
11
|
|
|
12
12
|
Read live from the runtime's wiringRegistry, so it cannot drift from what the renderer will actually accept. The richer authoring knowledge base (UI event payloads, refresh strategies, value sources, association types) is generation-side \u2014 see gen-ui-mcp's \`get_wiring_catalog\` tool instead.`;
|
|
13
|
+
const GET_CATALOG_LADDER_DESCRIPTION = `Enumerate the AdiaUI catalog contract's served rungs: L0 primitives (component vocabulary) and L1 widgets (named, versioned, chat-scale functional units \u2014 a Row of stat cards, a sign-in card, a tool-call accordion). An L1 entry carries its A2UI \`template\`, so this is the tool to call before generating a document by hand: find the widget, take its template, fill in the copy.
|
|
14
|
+
|
|
15
|
+
Called with no arguments it returns the ladder summary \u2014 every rung with its entry unit, the rung it composes, populated/reserved status, and entry count. Pass \`tier\` ("L0" or "L1") for that rung's entry names; pass \`tier\` + \`name\` for one entry's full record (\`composes\`, \`template\`, \`keywords\`, \`domain\`, provenance).
|
|
16
|
+
|
|
17
|
+
Scope, per ADR-0050's serving map: this server serves **L0 + L1** \u2014 the rungs a chat agent enumerates to emit A2UI. L2 layouts, L3 shells, and L4 flows are developer-facing and belong to the factory surface; \`adia-mcp gen-ui\`'s \`get_catalog_tiers\` serves the whole ladder for retrieval. The ladder's one law is that every tier-N entry is a declared composition of tier-(N-1) entries, so \`composes\` is the edge you follow down a rung \u2014 an L1 entry's \`composes\` names L0 types you can validate with \`validate_document\`.`;
|
|
13
18
|
const PROTOCOL_STATUS_DESCRIPTION = `Returns operational status of this A2UI protocol MCP server: transport and protocol-registry stats. Reports on the protocol server only \u2014 gen-ui-mcp's \`server_status\` tool reports its own corpus-side status separately.`;
|
|
14
19
|
function buildRegistryView() {
|
|
15
20
|
const byTag = /* @__PURE__ */ new Map();
|
|
@@ -25,6 +30,14 @@ function buildRegistryView() {
|
|
|
25
30
|
})).sort((a, b) => a.type.localeCompare(b.type));
|
|
26
31
|
return { totalTypes: registry.size, totalTags: byTag.size, entries };
|
|
27
32
|
}
|
|
33
|
+
const SERVED_TIERS = ["L0", "L1"];
|
|
34
|
+
let _tierIndex = null;
|
|
35
|
+
async function loadTierIndex() {
|
|
36
|
+
if (_tierIndex) return _tierIndex;
|
|
37
|
+
const mod = await import("@adia-ai/a2ui/catalog/tiers", { with: { type: "json" } });
|
|
38
|
+
_tierIndex = mod.default ?? mod;
|
|
39
|
+
return _tierIndex;
|
|
40
|
+
}
|
|
28
41
|
function buildWiringView() {
|
|
29
42
|
return {
|
|
30
43
|
controllers: [...wiringRegistry.controllers.keys()].sort(),
|
|
@@ -68,6 +81,116 @@ function registerProtocolTools(server) {
|
|
|
68
81
|
return { content: [{ type: "text", text: JSON.stringify(buildWiringView(), null, 2) }] };
|
|
69
82
|
}
|
|
70
83
|
);
|
|
84
|
+
server.tool(
|
|
85
|
+
"get_catalog_ladder",
|
|
86
|
+
GET_CATALOG_LADDER_DESCRIPTION,
|
|
87
|
+
{
|
|
88
|
+
tier: z.enum(["L0", "L1"]).optional().describe("List one served rung's entry names instead of the ladder summary"),
|
|
89
|
+
name: z.string().optional().describe("With `tier` (required alongside it), return this single entry's full record")
|
|
90
|
+
},
|
|
91
|
+
async ({ tier, name }) => {
|
|
92
|
+
if (name && !tier) {
|
|
93
|
+
return {
|
|
94
|
+
content: [
|
|
95
|
+
{
|
|
96
|
+
type: "text",
|
|
97
|
+
text: `\`name\` needs a \`tier\`: entry names are unique only within a rung. Retry with { tier: "L1", name: "${name}" } (or "L0"), or omit \`name\` for the ladder summary.`
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
isError: true
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
let index;
|
|
104
|
+
try {
|
|
105
|
+
index = await loadTierIndex();
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: `Catalog tier index unavailable: ${e.message}` }],
|
|
110
|
+
isError: true
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (!tier) {
|
|
114
|
+
const ladder = Object.fromEntries(
|
|
115
|
+
Object.entries(index.tiers).map(([id, t]) => [
|
|
116
|
+
id,
|
|
117
|
+
{
|
|
118
|
+
unit: t.unit,
|
|
119
|
+
composesTier: t.composesTier,
|
|
120
|
+
status: t.status,
|
|
121
|
+
count: t.count,
|
|
122
|
+
servedHere: SERVED_TIERS.includes(id)
|
|
123
|
+
}
|
|
124
|
+
])
|
|
125
|
+
);
|
|
126
|
+
return {
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: "text",
|
|
130
|
+
text: JSON.stringify(
|
|
131
|
+
{
|
|
132
|
+
catalogId: index.catalogId,
|
|
133
|
+
servedTiers: [...SERVED_TIERS],
|
|
134
|
+
servingMap: "protocol server: L0+L1 (this tool) \xB7 gen-ui server: all tiers, for retrieval (get_catalog_tiers) \xB7 factory server: L2-L4, developer-facing",
|
|
135
|
+
ladder
|
|
136
|
+
},
|
|
137
|
+
null,
|
|
138
|
+
2
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const rung = index.tiers[tier];
|
|
145
|
+
if (!rung) {
|
|
146
|
+
return {
|
|
147
|
+
content: [
|
|
148
|
+
{
|
|
149
|
+
type: "text",
|
|
150
|
+
text: `Tier "${tier}" is absent from the catalog tier index (${Object.keys(index.tiers).join(", ")} present) \u2014 the index may predate it.`
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
isError: true
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (name) {
|
|
157
|
+
const entry = rung.entries[name];
|
|
158
|
+
if (!entry) {
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: "text",
|
|
163
|
+
text: `No ${tier} entry named "${name}". ${rung.count} entries in this tier \u2014 call this tool with just \`tier\` for the names.`
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
isError: true
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
content: [{ type: "text", text: JSON.stringify({ tier, name, entry }, null, 2) }]
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
content: [
|
|
175
|
+
{
|
|
176
|
+
type: "text",
|
|
177
|
+
text: JSON.stringify(
|
|
178
|
+
{
|
|
179
|
+
tier,
|
|
180
|
+
unit: rung.unit,
|
|
181
|
+
composesTier: rung.composesTier,
|
|
182
|
+
status: rung.status,
|
|
183
|
+
count: rung.count,
|
|
184
|
+
entries: Object.keys(rung.entries).sort()
|
|
185
|
+
},
|
|
186
|
+
null,
|
|
187
|
+
2
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
);
|
|
71
194
|
server.tool(
|
|
72
195
|
"protocol_status",
|
|
73
196
|
PROTOCOL_STATUS_DESCRIPTION,
|