@adia-ai/a2ui-mcp 0.1.3 → 0.2.1

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/server.js CHANGED
@@ -26,7 +26,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
26
26
  import { z } from 'zod';
27
27
 
28
28
  // ── Import from a2ui ──
29
- import { generateUI } from '../compose/engine/generator.js';
29
+ import { generateUI } from '../compose/core/generator.js';
30
30
  import { validateSchema } from '../validator/validator.js';
31
31
  import { validateMessages as validateCatalogMessages } from '../validator/catalog-validator.js';
32
32
  import {
@@ -52,11 +52,11 @@ import {
52
52
  getAllCompositions as getAllZettelCompositions,
53
53
  searchAll as searchZettelAll,
54
54
  getGraph as getZettelGraph,
55
- } from '../compose/engines/zettel/fragment-library.js';
55
+ } from '../compose/strategies/zettel/fragment-library.js';
56
56
  import {
57
57
  resolveComposition as resolveZettelComposition,
58
58
  templateToMessages as zettelTemplateToMessages,
59
- } from '../compose/engines/zettel/composer.js';
59
+ } from '../compose/strategies/zettel/composer.js';
60
60
  // Zettel bootstrap is still needed for get_fragment/resolve_composition tools;
61
61
  // the generate_ui tool now dispatches through the unified registry in gen-ui.
62
62
 
@@ -74,6 +74,16 @@ import {
74
74
  searchChunks as searchGenUIChunks,
75
75
  } from '../corpus/scripts/chunk-library.js';
76
76
 
77
+ // ── Inline-tool deps (transpiler / wiring / patterns / feedback) ──
78
+ import { transpileHTML } from '../compose/transpiler/transpiler.js';
79
+ import { getWiringCatalog } from '../retrieval/wiring-catalog.js';
80
+ import { registerPattern } from '../retrieval/pattern-library.js';
81
+ import { FeedbackCollector } from '../retrieval/feedback/feedback.js';
82
+ import { feedbackStore } from '../retrieval/feedback/feedback-store.js';
83
+
84
+ // ── Tools extracted to tools/ for modularity ──
85
+ import { registerSynthesisTools } from './tools/synthesis.js';
86
+
77
87
  const _chunkIndex = getChunkIndex();
78
88
  if (_chunkIndex) {
79
89
  console.error(
@@ -279,9 +289,6 @@ server.tool(
279
289
 
280
290
  // ── Transpiler & Wiring Tools ──
281
291
 
282
- import { transpileHTML } from '../compose/transpiler/transpiler.js';
283
- import { getWiringCatalog } from '../retrieval/wiring-catalog.js';
284
-
285
292
  server.tool(
286
293
  'convert_html',
287
294
  'Convert HTML markup to A2UI flat adjacency component messages. Maps HTML tags to AdiaUI components, infers layout from styles, enforces Card content model.',
@@ -311,9 +318,6 @@ server.tool(
311
318
 
312
319
  // ── Pattern & Feedback Tools ──
313
320
 
314
- import { registerPattern } from '../retrieval/pattern-library.js';
315
- import { FeedbackCollector } from '../retrieval/feedback.js';
316
-
317
321
  const feedbackCollector = new FeedbackCollector();
318
322
 
319
323
  server.tool(
@@ -378,8 +382,6 @@ server.tool(
378
382
 
379
383
  // ── Quality metrics tool ──
380
384
 
381
- import { feedbackStore } from '../retrieval/feedback-store.js';
382
-
383
385
  server.tool(
384
386
  'get_quality_metrics',
385
387
  'Get aggregated quality metrics from the feedback store: avg score, thumb-up rate, per-domain breakdown, training gaps.',
@@ -656,405 +658,16 @@ Pair with \`get_chunk\` to fetch full records for any of the returned names.`,
656
658
  },
657
659
  );
658
660
 
659
- // ── Chunk-aware composition synthesizer (Phase C.2) ──────────────────
660
- // Mix-and-match: when retrieval returns no strong match, the LLM picks a
661
- // page chunk + binds block/panel chunks to its slots. Validator checks
662
- // chunk names + kind contracts; composer materializes to HTML.
661
+ // ── Chunk-aware composition + multi-turn refinement ──────────────────
662
+ // `compose_from_chunks`, `refine_composition`, `get_state`, `report_issue`
663
+ // share the LLM bridge + state-cache + issue-reporter + chunk-refiner
664
+ // stack. Extracted to tools/synthesis.js.
663
665
  //
664
- // Spec: docs/specs/genui-chunk-marker.md (§ "Harvester contract", future:
665
- // composition reasoning). Plan: docs/plans/training-pipeline-chunk-harvest-2026-04-27.md.
666
-
667
- import { composeFromIntent as composeFromChunksImpl } from '../compose/engines/zettel/chunk-synthesizer.js';
668
- import { composeFromPlan, validatePlan } from '../compose/engines/zettel/chunk-composer.js';
669
- import { createAdapter as createLLMAdapter } from '../compose/llm/llm-bridge.js';
670
-
671
- // ── Multi-turn architecture (Phase A) ────────────────────────────────
672
- // Spec: docs/specs/genui-multiturn-architecture.md (Draft v0.1.0).
673
- // Plan: docs/plans/genui-multiturn-rollout-2026-04-28.md (Phase A scoped).
674
-
675
- import {
676
- getStateCache,
677
- mintStateId,
678
- mintNextStateId,
679
- } from '../compose/engines/zettel/state-cache.js';
680
- import {
681
- reportIssue as reportIssueImpl,
682
- autoReport,
683
- createIssueAccumulator,
684
- } from '../compose/engines/zettel/issue-reporter.js';
685
- import {
686
- refineFromIntent,
687
- applyOps,
688
- opsToA2UI,
689
- validateOps,
690
- } from '../compose/engines/zettel/chunk-refiner.js';
691
-
692
- const stateCache = getStateCache();
693
-
694
- const ENGINE_VERSION_INFO = {
695
- mcp: '0.1.0',
696
- corpus: '0.0.6',
697
- engine: 'zettel',
698
- llm_adapter: 'anthropic',
699
- model: process.env.ANTHROPIC_MODEL || 'claude-opus-4-7',
700
- };
701
-
702
- server.tool(
703
- 'compose_from_chunks',
704
- `Compose a UI page from training chunks — retrieval-first, synthesis-fallback.
705
-
706
- Mix-and-match composition for intents that don't have a 1:1 chunk match. Workflow:
707
- 1. Pure-retrieval tier: if \`search_chunks\` returns a strong direct match, return
708
- that chunk's HTML immediately (no LLM call).
709
- 2. Synthesis tier: when retrieval is weak, the LLM picks a page-kind chunk and
710
- binds block/panel chunks to its named slots. Output validated against the
711
- chunk catalog (slot names exist, bound chunks exist, kinds match).
712
-
713
- Returns the composed HTML string + a binding plan describing which chunks plug
714
- where. Useful when the prompt is novel ("dashboard with KPI grid + funnel +
715
- country list") and no exact chunk has all those parts together — the LLM mixes
716
- and matches from the corpus.
717
-
718
- Two-call mode also available via \`plan\` parameter — pass a pre-baked binding
719
- plan to skip the LLM call and just materialize HTML.`,
720
- {
721
- intent: z.string().optional().describe('Natural-language description of what to build (uses LLM synthesis)'),
722
- plan: z.object({
723
- page: z.string(),
724
- slot_bindings: z.record(z.union([z.string(), z.array(z.string())])),
725
- }).optional().describe('Pre-baked binding plan (skips LLM, materializes directly)'),
726
- max_attempts: z.number().int().min(1).max(5).default(2).describe('LLM retry budget for synthesis'),
727
- },
728
- async ({ intent, plan, max_attempts }) => {
729
- if (plan) {
730
- const validation = validatePlan(plan);
731
- if (!validation.ok) {
732
- return {
733
- isError: true,
734
- content: [{ type: 'text', text: JSON.stringify({ error: 'invalid plan', errors: validation.errors }, null, 2) }],
735
- };
736
- }
737
- const result = composeFromPlan(plan);
738
- const state_id = mintStateId(intent || plan.page || 'plan', 1);
739
- stateCache.set(state_id, {
740
- state_id,
741
- intent: intent || `(plan) ${plan.page}`,
742
- plan: result.plan,
743
- html: result.html,
744
- source: 'plan',
745
- ops_history: [],
746
- parent_state_id: null,
747
- created_at: new Date().toISOString(),
748
- });
749
- return {
750
- content: [{ type: 'text', text: JSON.stringify({
751
- state_id,
752
- html: result.html,
753
- plan: result.plan,
754
- warnings: result.warnings,
755
- source: 'plan',
756
- }, null, 2) }],
757
- };
758
- }
759
-
760
- if (!intent) {
761
- return {
762
- isError: true,
763
- content: [{ type: 'text', text: JSON.stringify({ error: 'must provide either intent or plan' }, null, 2) }],
764
- };
765
- }
766
-
767
- try {
768
- const llmAdapter = await createLLMAdapter();
769
- const result = await composeFromChunksImpl({
770
- intent,
771
- llmAdapter,
772
- maxAttempts: max_attempts,
773
- });
774
- const state_id = mintStateId(intent, 1);
775
- stateCache.set(state_id, {
776
- state_id,
777
- intent,
778
- plan: result.plan,
779
- html: result.html,
780
- source: result.source,
781
- score: result.score,
782
- ops_history: [],
783
- parent_state_id: null,
784
- warnings: result.warnings,
785
- synthesis: result.synthesis,
786
- created_at: new Date().toISOString(),
787
- });
788
- return {
789
- content: [{ type: 'text', text: JSON.stringify({
790
- state_id,
791
- html: result.html,
792
- plan: result.plan,
793
- source: result.source,
794
- score: result.score,
795
- warnings: result.warnings,
796
- synthesis: result.synthesis ? { attempts: result.synthesis.attempts } : undefined,
797
- }, null, 2) }],
798
- };
799
- } catch (e) {
800
- return {
801
- isError: true,
802
- content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }],
803
- };
804
- }
805
- },
806
- );
807
-
808
- // ── Multi-turn refinement tools (Phase A) ─────────────────────────────
809
- // Spec: docs/specs/genui-multiturn-architecture.md §3.
810
-
811
- server.tool(
812
- 'refine_composition',
813
- `Refine an existing chunk-composed UI based on a natural-language intent or an explicit op-list.
814
-
815
- Use when the user wants to modify an *existing* UI. Triggers on "change", "update", "modify", "add to", "remove from", "this", "it", "the X". Requires \`state_id\` from a prior \`compose_from_chunks\` call.
816
-
817
- Two modes:
818
- - **Intent-driven** — pass \`intent\`. Engine runs two-pass synthesis (locator pass identifies which slots to modify; modifier pass emits chunk-plan ops). Validator-driven retry on op-validation failure.
819
- - **Explicit ops** — pass \`ops\` directly. Skips the LLM entirely; engine applies + materializes.
820
-
821
- Returns a new \`state_id\` (versioned chain from the parent), the A2UI op-list applied, the post-op HTML, and a delta summary. Failed ops are reported in \`ops_failed\` with reasons.
822
-
823
- For *fresh creation* use \`compose_from_chunks\`, not this tool.`,
824
- {
825
- state_id: z.string().describe('State id from a prior compose_from_chunks or refine_composition call'),
826
- intent: z.string().optional().describe('Natural-language description of what to change (e.g. "add a country list to page-content")'),
827
- ops: z.array(z.any()).optional().describe('Pre-computed chunk-plan ops to apply directly (skips the LLM)'),
828
- max_attempts: z.number().int().min(1).max(5).default(2).describe('Validator retry budget for synthesis'),
829
- },
830
- async ({ state_id, intent, ops, max_attempts }) => {
831
- const priorState = stateCache.get(state_id);
832
- if (!priorState) {
833
- await autoReport(
834
- 'cache-miss-on-known-state',
835
- { state_id, tool: 'refine_composition' },
836
- { cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
837
- );
838
- return {
839
- isError: true,
840
- content: [{ type: 'text', text: JSON.stringify({
841
- error: 'state_id not found in cache',
842
- hint: 'state cache is in-memory and bounded; re-run compose_from_chunks to mint a fresh state_id',
843
- state_id,
844
- }, null, 2) }],
845
- };
846
- }
847
-
848
- if (!intent && !ops) {
849
- return {
850
- isError: true,
851
- content: [{ type: 'text', text: JSON.stringify({ error: 'must provide either intent or ops' }, null, 2) }],
852
- };
853
- }
854
-
855
- const issueAccumulator = createIssueAccumulator();
856
- const issueCtx = { cache: stateCache, versionInfo: ENGINE_VERSION_INFO };
857
- const startedAt = Date.now();
858
-
859
- try {
860
- let resolvedOps;
861
- let delta_summary = '';
862
- let synthesis = null;
863
- let warnings = [];
864
-
865
- if (ops && Array.isArray(ops)) {
866
- // Explicit ops path — validate then apply
867
- const validation = validateOps(ops, priorState);
868
- if (!validation.ok) {
869
- await issueAccumulator.flush(issueCtx);
870
- return {
871
- isError: true,
872
- content: [{ type: 'text', text: JSON.stringify({
873
- error: 'ops failed validation',
874
- errors: validation.errors,
875
- }, null, 2) }],
876
- };
877
- }
878
- resolvedOps = ops;
879
- delta_summary = `applied ${ops.length} explicit op(s)`;
880
- } else {
881
- // Intent path — two-pass synthesis with stub-friendly LLM bridge
882
- const llmAdapter = await createLLMAdapter();
883
- const refined = await refineFromIntent({
884
- priorState,
885
- intent,
886
- llmAdapter,
887
- maxAttempts: max_attempts,
888
- issueAccumulator,
889
- });
890
- resolvedOps = refined.ops;
891
- delta_summary = refined.delta_summary || '';
892
- synthesis = refined.synthesis;
893
- warnings = refined.warnings;
894
-
895
- if (resolvedOps.length === 0) {
896
- // Synthesizer gave up. Auto-fires already accumulated.
897
- await issueAccumulator.flush(issueCtx);
898
- const childId = mintNextStateId(state_id, (priorState.version || 1) + 1);
899
- return {
900
- content: [{ type: 'text', text: JSON.stringify({
901
- state_id: childId,
902
- ops_applied: [],
903
- ops_failed: [],
904
- delta_summary: '',
905
- warnings,
906
- synthesis: synthesis ? { attempts: synthesis.attempts, targeted: synthesis.targeted } : null,
907
- html: priorState.html,
908
- }, null, 2) }],
909
- };
910
- }
911
- }
912
-
913
- const applied = await applyOps({ priorState, ops: resolvedOps });
914
-
915
- if (applied.ops_failed.length > 0) {
916
- issueAccumulator.add('ops-failed-after-apply', {
917
- state_id,
918
- tool: 'refine_composition',
919
- intent,
920
- });
921
- }
922
-
923
- const a2uiMessages = opsToA2UI(applied.ops_applied, applied.newState);
924
-
925
- const parentVersion = priorState.version || 1;
926
- const newVersion = parentVersion + 1;
927
- const newStateId = mintNextStateId(state_id, newVersion);
928
-
929
- stateCache.set(newStateId, {
930
- state_id: newStateId,
931
- intent: intent || `(ops) ${priorState.intent}`,
932
- plan: applied.newState.plan,
933
- html: applied.newState.html,
934
- source: 'refinement',
935
- version: newVersion,
936
- ops_history: [...(priorState.ops_history || []), ...a2uiMessages],
937
- parent_state_id: state_id,
938
- warnings: applied.newState.warnings,
939
- delta_summary,
940
- synthesis,
941
- created_at: new Date().toISOString(),
942
- duration_ms: Date.now() - startedAt,
943
- });
944
-
945
- await issueAccumulator.flush(issueCtx);
946
-
947
- return {
948
- content: [{ type: 'text', text: JSON.stringify({
949
- state_id: newStateId,
950
- ops_applied: a2uiMessages,
951
- ops_failed: applied.ops_failed,
952
- delta_summary,
953
- warnings: [...warnings, ...(applied.newState.warnings || [])],
954
- synthesis: synthesis ? { attempts: synthesis.attempts, targeted: synthesis.targeted, locatedTargets: synthesis.locatedTargets } : null,
955
- html: applied.newState.html,
956
- }, null, 2) }],
957
- };
958
- } catch (e) {
959
- await issueAccumulator.flush(issueCtx);
960
- return {
961
- isError: true,
962
- content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }],
963
- };
964
- }
965
- },
966
- );
967
-
968
- server.tool(
969
- 'get_state',
970
- `Inspect a cached composition state by state_id.
666
+ // Spec: docs/specs/genui-multiturn-architecture.md (Phase A) +
667
+ // docs/specs/genui-chunk-marker.md.
971
668
 
972
- Returns the full cache entry including the materialized HTML, the chunk binding plan, the chronological ops history (every refinement applied to this state's lineage), and the parent state_id (chain-back to the originating compose_from_chunks call).
669
+ registerSynthesisTools(server);
973
670
 
974
- Useful for debugging refinement sequences, replaying a state's history, or verifying that a state_id is still cached before issuing a refine_composition call.
975
-
976
- Auto-fires a low-severity \`cache-miss-on-known-state\` issue when the state_id is not in the cache (the cache is bounded LRU; long-paused conversations may evict their state).`,
977
- {
978
- state_id: z.string().describe('State id from a prior compose_from_chunks or refine_composition call'),
979
- },
980
- async ({ state_id }) => {
981
- const entry = stateCache.peek(state_id);
982
- if (!entry) {
983
- await autoReport(
984
- 'cache-miss-on-known-state',
985
- { state_id, tool: 'get_state' },
986
- { cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
987
- );
988
- return {
989
- isError: true,
990
- content: [{ type: 'text', text: JSON.stringify({
991
- error: 'state_id not found in cache',
992
- state_id,
993
- }, null, 2) }],
994
- };
995
- }
996
- return {
997
- content: [{ type: 'text', text: JSON.stringify({
998
- state_id: entry.state_id,
999
- intent: entry.intent,
1000
- plan: entry.plan,
1001
- html: entry.html,
1002
- source: entry.source,
1003
- version: entry.version || 1,
1004
- parent_state_id: entry.parent_state_id || null,
1005
- ops_history: entry.ops_history || [],
1006
- warnings: entry.warnings || [],
1007
- created_at: entry.created_at,
1008
- }, null, 2) }],
1009
- };
1010
- },
1011
- );
1012
-
1013
- server.tool(
1014
- 'report_issue',
1015
- `File a structured issue ticket when something is wrong with the gen-UI output, the tool surface, or the training data.
1016
-
1017
- Use when:
1018
- (a) the user explicitly says the output is broken / wrong / missing,
1019
- (b) you cannot satisfy the user's intent after retrying,
1020
- (c) you detect a mismatch between requested and produced output that you cannot fix.
1021
-
1022
- Include \`state_id\` for full trace attachment (input + output + LLM prompts/responses + validator results, when available in the cache).
1023
-
1024
- Do NOT call this for ordinary clarification or for output the user has not yet seen.
1025
-
1026
- Issue files land at \`.brain/audit-history/issues/<issue_id>.json\` (immutable; resolution lands in a sidecar file). Severity taxonomy matches the project's coherence-audit vocabulary: blocker = contract violation; drift = quality erosion; nit = cosmetic.`,
1027
- {
1028
- type: z.enum(['bug', 'training-gap', 'protocol-gap', 'ux-feedback']).describe('Issue category'),
1029
- severity: z.enum(['blocker', 'drift', 'nit']).describe('Severity tier'),
1030
- title: z.string().max(80).describe('One-line title (≤ 80 chars)'),
1031
- body: z.string().describe('Markdown body — observed vs expected, repro steps'),
1032
- state_id: z.string().optional().describe('State id from a prior tool call; auto-attaches the trace'),
1033
- trace: z.enum(['full', 'summary', 'none']).optional().describe('Trace depth (default: summary if state_id provided, else none)'),
1034
- suggested_owner: z.enum(['synthesis', 'retrieval', 'validator', 'chunk-corpus', 'mcp-protocol', 'unknown']).optional().describe('Best-guess owner for triage'),
1035
- tags: z.array(z.string()).optional().describe('Free-form tags for filtering'),
1036
- },
1037
- async ({ type, severity, title, body, state_id, trace, suggested_owner, tags }) => {
1038
- try {
1039
- const result = await reportIssueImpl(
1040
- { type, severity, title, body, state_id, trace, suggested_owner, tags },
1041
- {
1042
- cache: stateCache,
1043
- versionInfo: ENGINE_VERSION_INFO,
1044
- reporter: 'llm',
1045
- }
1046
- );
1047
- return {
1048
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1049
- };
1050
- } catch (e) {
1051
- return {
1052
- isError: true,
1053
- content: [{ type: 'text', text: JSON.stringify({ error: e.message }, null, 2) }],
1054
- };
1055
- }
1056
- },
1057
- );
1058
671
 
1059
672
  // ── Start ──
1060
673