@unthinkmedia/coherence-prototyper-mcp 1.0.0
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/README.md +138 -0
- package/dist/icon-browser.html +3749 -0
- package/dist/intent-app.html +3828 -0
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -0
- package/dist/server.js +1078 -0
- package/dist/server.js.map +1 -0
- package/dist/src/content.js +196 -0
- package/dist/src/content.js.map +1 -0
- package/dist/src/icon-browser.js +530 -0
- package/dist/src/icon-browser.js.map +1 -0
- package/dist/src/icon-data.js +288 -0
- package/dist/src/icon-data.js.map +1 -0
- package/dist/src/intent-store.js +166 -0
- package/dist/src/intent-store.js.map +1 -0
- package/dist/src/manifest-cache.js +95 -0
- package/dist/src/manifest-cache.js.map +1 -0
- package/dist/src/svg-cache.js +93 -0
- package/dist/src/svg-cache.js.map +1 -0
- package/dist/src/theme-cache.js +107 -0
- package/dist/src/theme-cache.js.map +1 -0
- package/dist/src/verification-report.js +473 -0
- package/dist/src/verification-report.js.map +1 -0
- package/dist/src/verification-scorecard-store.js +79 -0
- package/dist/src/verification-scorecard-store.js.map +1 -0
- package/dist/token-browser.html +3594 -0
- package/dist/verification-report.html +3780 -0
- package/package.json +37 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,1078 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coherence Prototyper MCP Server
|
|
3
|
+
*
|
|
4
|
+
* MCP Apps only — interactive UIs that render inline in Claude Desktop/Claude.ai.
|
|
5
|
+
* All reference tools (components, tokens, scaffolds, guides, etc.) are handled
|
|
6
|
+
* by VS Code skills which are always prioritized.
|
|
7
|
+
*/
|
|
8
|
+
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server";
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import fsp from "node:fs/promises";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { getTokensByCategory, getTokenCategories, getAllTokens, } from "./src/theme-cache.js";
|
|
15
|
+
import { createIntent, getIntent, listIntents, listExperimentFolders, updateIntent, deleteIntent, formatIntent, formatIntentSummary, } from "./src/intent-store.js";
|
|
16
|
+
import { searchIcons } from "./src/icon-data.js";
|
|
17
|
+
import { getVerificationReport, listVerificationExperiments, } from "./src/verification-scorecard-store.js";
|
|
18
|
+
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
|
19
|
+
export function createServer() {
|
|
20
|
+
const server = new McpServer({
|
|
21
|
+
name: "Coherence Prototyper",
|
|
22
|
+
version: "1.0.0",
|
|
23
|
+
});
|
|
24
|
+
// ════════════════════════════════════════════════════════
|
|
25
|
+
// MCP App: Design Intent — interactive intent capture UI
|
|
26
|
+
// ════════════════════════════════════════════════════════
|
|
27
|
+
const intentAppUri = "ui://coherence-prototyper/intent-app.html";
|
|
28
|
+
// Model-facing tool: opens the Intent App UI
|
|
29
|
+
registerAppTool(server, "design_intent", {
|
|
30
|
+
title: "Design Intent",
|
|
31
|
+
description: "Open the Design Intent app to capture What, Why, Success Criteria, and Non-Goals before prototyping. " +
|
|
32
|
+
"THIS IS MANDATORY — intent.json is the primary instruction source for the entire build. " +
|
|
33
|
+
"ALWAYS TRIGGER THIS TOOL when the user describes what they want to build — extract their prompt into the prefill fields " +
|
|
34
|
+
"(vision, problem, success criteria, constraints) so the intent form opens pre-populated with best guesses. " +
|
|
35
|
+
"The user's edits are AUTO-SAVED to intent.json in real-time — there are no Save/Accept buttons in the UI. " +
|
|
36
|
+
"After this tool returns, wait a moment for the user to review/edit the form, then ASK the user in chat: " +
|
|
37
|
+
"'Do you accept this intent?' — once they confirm, READ coherence-preview/src/experiments/<experimentId>/intent.json " +
|
|
38
|
+
"and use the full intent as PRIMARY INSTRUCTION SOURCE for building. " +
|
|
39
|
+
"If intent.json does not exist after calling this tool, DO NOT proceed with building — re-call this tool. " +
|
|
40
|
+
"Also tell the user they can view/edit intents anytime via the Intent button in the preview header or by calling this tool again. " +
|
|
41
|
+
"Also trigger when the user explicitly asks to open the intent form, manage intents, or says 'let me define the intent'.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
experimentId: z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Optional experiment folder name to view/edit a specific intent"),
|
|
47
|
+
prefillTitle: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Pre-fill the title field when the user has already described what they want to build"),
|
|
51
|
+
prefillVision: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("Pre-fill the vision field extracted from what the user has already said about the experiment"),
|
|
55
|
+
prefillProblem: z
|
|
56
|
+
.string()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("Pre-fill the problem statement extracted from the user's description"),
|
|
59
|
+
prefillSuccessCriteria: z
|
|
60
|
+
.array(z.string())
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Pre-fill success criteria extracted from the user's description"),
|
|
63
|
+
prefillNonGoals: z
|
|
64
|
+
.array(z.string())
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Pre-fill non-goals extracted from the user's description"),
|
|
67
|
+
prefillConstraints: z
|
|
68
|
+
.array(z.string())
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("Pre-fill constraints extracted from the user's description"),
|
|
71
|
+
prefillFigmaUrl: z
|
|
72
|
+
.string()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe("Figma file URL to use as design starting point."),
|
|
75
|
+
prefillFigmaMode: z
|
|
76
|
+
.enum(["import", "reference"])
|
|
77
|
+
.optional()
|
|
78
|
+
.describe("'import' = reproduce the Figma pixel-perfect using Coherence. 'reference' = analyze the Figma design and build something better."),
|
|
79
|
+
prefillFigmaContext: z
|
|
80
|
+
.string()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe("Detailed design spec extracted from the Figma file (exact layout, components, spacing, colors, typography, data content). This is the pixel-perfect blueprint the builder will follow."),
|
|
83
|
+
},
|
|
84
|
+
_meta: {
|
|
85
|
+
ui: { resourceUri: intentAppUri },
|
|
86
|
+
},
|
|
87
|
+
}, async ({ experimentId, prefillTitle, prefillVision, prefillProblem, prefillSuccessCriteria, prefillNonGoals, prefillConstraints, prefillFigmaUrl, prefillFigmaMode, prefillFigmaContext }) => {
|
|
88
|
+
const experiments = await listExperimentFolders();
|
|
89
|
+
// Build prefill object if any prefill params were provided
|
|
90
|
+
const hasPrefill = prefillTitle || prefillVision || prefillProblem ||
|
|
91
|
+
(prefillSuccessCriteria && prefillSuccessCriteria.length > 0) ||
|
|
92
|
+
(prefillNonGoals && prefillNonGoals.length > 0) ||
|
|
93
|
+
(prefillConstraints && prefillConstraints.length > 0) ||
|
|
94
|
+
prefillFigmaUrl || prefillFigmaContext;
|
|
95
|
+
const prefill = hasPrefill ? {
|
|
96
|
+
experimentId: experimentId ?? "",
|
|
97
|
+
title: prefillTitle ?? "",
|
|
98
|
+
vision: prefillVision ?? "",
|
|
99
|
+
problem: prefillProblem ?? "",
|
|
100
|
+
successCriteria: prefillSuccessCriteria ?? [],
|
|
101
|
+
nonGoals: prefillNonGoals ?? [],
|
|
102
|
+
constraints: prefillConstraints ?? [],
|
|
103
|
+
figmaUrl: prefillFigmaUrl ?? "",
|
|
104
|
+
figmaMode: prefillFigmaMode ?? "",
|
|
105
|
+
figmaContext: prefillFigmaContext ?? "",
|
|
106
|
+
} : null;
|
|
107
|
+
if (experimentId) {
|
|
108
|
+
const intent = await getIntent(experimentId);
|
|
109
|
+
if (!intent) {
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{ type: "text", text: `No intent found for "${experimentId}" yet. The Intent form is now open for the user to fill in — edits are auto-saved to intent.json in real-time. ASK the user in chat: "Do you accept this intent?" Once they confirm, read coherence-preview/src/experiments/${experimentId}/intent.json for the full intent document. If the file does not exist, re-call this tool.` },
|
|
113
|
+
],
|
|
114
|
+
structuredContent: { intents: [], experiments, ...(prefill ? { prefill } : {}) },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{ type: "text", text: formatIntent(intent) + "\n\n---\nThe user is reviewing this intent in the Intent MCP UI. Edits are auto-saved to intent.json in real-time. ASK the user in chat: 'Do you accept this intent?' Once they confirm, read coherence-preview/src/experiments/" + experimentId + "/intent.json for the finalized version. DO NOT start building until the user confirms acceptance in chat." },
|
|
120
|
+
],
|
|
121
|
+
structuredContent: { intents: [intent], experiments },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const intents = await listIntents();
|
|
125
|
+
let text;
|
|
126
|
+
if (intents.length === 0) {
|
|
127
|
+
text = "No design intents yet. The Intent form is now open for the user to define their intent — edits are auto-saved in real-time. ASK the user in chat: 'Do you accept this intent?' Once they confirm, read the intent.json file from the experiment folder for full context. If the file does not exist, re-call this tool.";
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
text = `# Design Intents (${intents.length})\n\n| Experiment | Title | Status | Updated |\n|------------|-------|--------|--------|\n`;
|
|
131
|
+
for (const i of intents) {
|
|
132
|
+
text += formatIntentSummary(i) + "\n";
|
|
133
|
+
}
|
|
134
|
+
text += "\n---\nThe user is reviewing intents in the Intent MCP UI. Edits are auto-saved in real-time. ASK the user in chat: 'Do you accept this intent?' Once they confirm, read the corresponding intent.json file for the finalized version. DO NOT start building until the user confirms acceptance in chat.";
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text", text }],
|
|
138
|
+
structuredContent: { intents, experiments, ...(prefill ? { prefill } : {}) },
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
// App-only tool: list intents (called by the UI)
|
|
142
|
+
registerAppTool(server, "intent_list_data", {
|
|
143
|
+
description: "List all design intents and available experiment folders",
|
|
144
|
+
inputSchema: {},
|
|
145
|
+
_meta: {
|
|
146
|
+
ui: {
|
|
147
|
+
resourceUri: intentAppUri,
|
|
148
|
+
visibility: ["app"],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}, async () => {
|
|
152
|
+
const intents = await listIntents();
|
|
153
|
+
const experiments = await listExperimentFolders();
|
|
154
|
+
return {
|
|
155
|
+
content: [
|
|
156
|
+
{ type: "text", text: `${intents.length} intents, ${experiments.length} experiments` },
|
|
157
|
+
],
|
|
158
|
+
structuredContent: { intents, experiments },
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
// App-only tool: save (create or update) an intent
|
|
162
|
+
registerAppTool(server, "intent_save_data", {
|
|
163
|
+
description: "Create or update a design intent. experimentId is the experiment folder name and serves as the key.",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
experimentId: z.string().describe("Experiment folder name (required — serves as the intent key)"),
|
|
166
|
+
title: z.string().optional(),
|
|
167
|
+
vision: z.string().optional(),
|
|
168
|
+
problem: z.string().optional(),
|
|
169
|
+
successCriteria: z.array(z.string()).optional(),
|
|
170
|
+
nonGoals: z.array(z.string()).optional(),
|
|
171
|
+
constraints: z.array(z.string()).optional(),
|
|
172
|
+
figmaUrl: z.string().optional().describe("Figma file URL used as design reference"),
|
|
173
|
+
figmaMode: z.enum(["import", "reference"]).optional().describe("'import' = pixel-perfect reproduction; 'reference' = analyze & improve"),
|
|
174
|
+
figmaContext: z.string().optional().describe("Extracted Figma design context"),
|
|
175
|
+
status: z
|
|
176
|
+
.enum(["draft", "active", "completed", "abandoned"])
|
|
177
|
+
.optional(),
|
|
178
|
+
},
|
|
179
|
+
_meta: {
|
|
180
|
+
ui: {
|
|
181
|
+
resourceUri: intentAppUri,
|
|
182
|
+
visibility: ["app"],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
}, async (args) => {
|
|
186
|
+
let intent;
|
|
187
|
+
const existing = await getIntent(args.experimentId);
|
|
188
|
+
if (existing) {
|
|
189
|
+
intent = await updateIntent(args.experimentId, args);
|
|
190
|
+
if (!intent) {
|
|
191
|
+
return {
|
|
192
|
+
isError: true,
|
|
193
|
+
content: [
|
|
194
|
+
{ type: "text", text: `Failed to update intent for "${args.experimentId}".` },
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
if (!args.vision) {
|
|
201
|
+
return {
|
|
202
|
+
isError: true,
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: "text",
|
|
206
|
+
text: "Vision and experimentId are required to create an intent.",
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
intent = await createIntent(args);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
content: [
|
|
215
|
+
{ type: "text", text: formatIntent(intent) },
|
|
216
|
+
],
|
|
217
|
+
structuredContent: { intent },
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
// App-only tool: delete an intent
|
|
221
|
+
registerAppTool(server, "intent_delete_data", {
|
|
222
|
+
description: "Delete a design intent (removes intent.json from the experiment folder)",
|
|
223
|
+
inputSchema: {
|
|
224
|
+
experimentId: z.string().describe("Experiment folder name whose intent to delete"),
|
|
225
|
+
},
|
|
226
|
+
_meta: {
|
|
227
|
+
ui: {
|
|
228
|
+
resourceUri: intentAppUri,
|
|
229
|
+
visibility: ["app"],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
}, async ({ experimentId }) => {
|
|
233
|
+
const success = await deleteIntent(experimentId);
|
|
234
|
+
return {
|
|
235
|
+
content: [
|
|
236
|
+
{
|
|
237
|
+
type: "text",
|
|
238
|
+
text: success
|
|
239
|
+
? `Intent for "${experimentId}" deleted.`
|
|
240
|
+
: `No intent found for "${experimentId}".`,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
// Resource: Intent App HTML
|
|
246
|
+
registerAppResource(server, "Design Intent", intentAppUri, {
|
|
247
|
+
description: "Interactive design intent capture form — define What, Why, Success Criteria, and Non-Goals before prototyping",
|
|
248
|
+
}, async () => {
|
|
249
|
+
let html;
|
|
250
|
+
try {
|
|
251
|
+
html = await fsp.readFile(path.join(DIST_DIR, "intent-app.html"), "utf-8");
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
html =
|
|
255
|
+
"<html><body><p>Intent app not built. Run <code>npm run build</code> in mcp-server/.</p></body></html>";
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
contents: [
|
|
259
|
+
{
|
|
260
|
+
uri: intentAppUri,
|
|
261
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
262
|
+
text: html,
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
// ════════════════════════════════════════════════════════
|
|
268
|
+
// MCP App: browse_verification_reports — Validation scorecard UI
|
|
269
|
+
// ════════════════════════════════════════════════════════
|
|
270
|
+
const verificationReportUri = "ui://coherence-prototyper/verification-report.html";
|
|
271
|
+
function computeVerificationDelta(latest, history) {
|
|
272
|
+
if (!latest || history.length < 2) {
|
|
273
|
+
return {
|
|
274
|
+
hasPrevious: false,
|
|
275
|
+
overallDeltaPercent: 0,
|
|
276
|
+
improved: [],
|
|
277
|
+
regressed: [],
|
|
278
|
+
unchanged: 0,
|
|
279
|
+
newCriteria: [],
|
|
280
|
+
removedCriteria: [],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const previous = history[history.length - 2];
|
|
284
|
+
const previousById = new Map(previous.criteria.map((item) => [item.id, item]));
|
|
285
|
+
const latestById = new Map(latest.criteria.map((item) => [item.id, item]));
|
|
286
|
+
const improved = [];
|
|
287
|
+
const regressed = [];
|
|
288
|
+
let unchanged = 0;
|
|
289
|
+
for (const item of latest.criteria) {
|
|
290
|
+
const prev = previousById.get(item.id);
|
|
291
|
+
if (!prev)
|
|
292
|
+
continue;
|
|
293
|
+
const delta = item.score - prev.score;
|
|
294
|
+
if (delta > 0)
|
|
295
|
+
improved.push({ id: item.id, delta });
|
|
296
|
+
else if (delta < 0)
|
|
297
|
+
regressed.push({ id: item.id, delta });
|
|
298
|
+
else
|
|
299
|
+
unchanged += 1;
|
|
300
|
+
}
|
|
301
|
+
const newCriteria = latest.criteria
|
|
302
|
+
.filter((item) => !previousById.has(item.id))
|
|
303
|
+
.map((item) => item.id);
|
|
304
|
+
const removedCriteria = previous.criteria
|
|
305
|
+
.filter((item) => !latestById.has(item.id))
|
|
306
|
+
.map((item) => item.id);
|
|
307
|
+
return {
|
|
308
|
+
hasPrevious: true,
|
|
309
|
+
overallDeltaPercent: latest.effectivenessPercent - previous.effectivenessPercent,
|
|
310
|
+
improved,
|
|
311
|
+
regressed,
|
|
312
|
+
unchanged,
|
|
313
|
+
newCriteria,
|
|
314
|
+
removedCriteria,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
registerAppTool(server, "browse_verification_reports", {
|
|
318
|
+
title: "Verification Report Browser",
|
|
319
|
+
description: "Open a formatted MCP App UI for success-criteria verification reports, including per-criterion scores, evidence, actionable feedback, and trend deltas across runs.",
|
|
320
|
+
inputSchema: {
|
|
321
|
+
experimentId: z
|
|
322
|
+
.string()
|
|
323
|
+
.optional()
|
|
324
|
+
.describe("Optional experiment id to open directly"),
|
|
325
|
+
},
|
|
326
|
+
_meta: {
|
|
327
|
+
ui: { resourceUri: verificationReportUri },
|
|
328
|
+
},
|
|
329
|
+
}, async ({ experimentId }) => {
|
|
330
|
+
const allReports = await listVerificationExperiments();
|
|
331
|
+
const selectedExperimentId = experimentId ?? allReports[0]?.experimentId ?? null;
|
|
332
|
+
// Only include the current experiment's report row
|
|
333
|
+
const reports = selectedExperimentId
|
|
334
|
+
? allReports.filter((r) => r.experimentId === selectedExperimentId)
|
|
335
|
+
: allReports;
|
|
336
|
+
const report = selectedExperimentId
|
|
337
|
+
? await getVerificationReport(selectedExperimentId)
|
|
338
|
+
: { latest: null, history: [] };
|
|
339
|
+
const delta = computeVerificationDelta(report.latest, report.history);
|
|
340
|
+
return {
|
|
341
|
+
content: [
|
|
342
|
+
{
|
|
343
|
+
type: "text",
|
|
344
|
+
text: selectedExperimentId
|
|
345
|
+
? `Opened verification report UI for "${selectedExperimentId}".`
|
|
346
|
+
: "Opened verification report UI. No reports found yet.",
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
structuredContent: {
|
|
350
|
+
reports,
|
|
351
|
+
selectedExperimentId,
|
|
352
|
+
latest: report.latest,
|
|
353
|
+
history: report.history,
|
|
354
|
+
delta,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
registerAppTool(server, "verification_report_get_data", {
|
|
359
|
+
description: "Load verification scorecard data for the MCP App UI",
|
|
360
|
+
inputSchema: {
|
|
361
|
+
experimentId: z
|
|
362
|
+
.string()
|
|
363
|
+
.optional()
|
|
364
|
+
.describe("Optional experiment id to load"),
|
|
365
|
+
},
|
|
366
|
+
_meta: {
|
|
367
|
+
ui: {
|
|
368
|
+
resourceUri: verificationReportUri,
|
|
369
|
+
visibility: ["app"],
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
}, async ({ experimentId }) => {
|
|
373
|
+
const allReports = await listVerificationExperiments();
|
|
374
|
+
const selectedExperimentId = experimentId ?? allReports[0]?.experimentId ?? null;
|
|
375
|
+
// Only include the current experiment's report row
|
|
376
|
+
const reports = selectedExperimentId
|
|
377
|
+
? allReports.filter((r) => r.experimentId === selectedExperimentId)
|
|
378
|
+
: allReports;
|
|
379
|
+
const report = selectedExperimentId
|
|
380
|
+
? await getVerificationReport(selectedExperimentId)
|
|
381
|
+
: { latest: null, history: [] };
|
|
382
|
+
const delta = computeVerificationDelta(report.latest, report.history);
|
|
383
|
+
return {
|
|
384
|
+
content: [
|
|
385
|
+
{
|
|
386
|
+
type: "text",
|
|
387
|
+
text: selectedExperimentId
|
|
388
|
+
? `Loaded verification data for "${selectedExperimentId}".`
|
|
389
|
+
: "No verification reports found yet.",
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
structuredContent: {
|
|
393
|
+
reports,
|
|
394
|
+
selectedExperimentId,
|
|
395
|
+
latest: report.latest,
|
|
396
|
+
history: report.history,
|
|
397
|
+
delta,
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
registerAppResource(server, "Verification Report Browser", verificationReportUri, { description: "Interactive success-criteria verification report viewer" }, async () => {
|
|
402
|
+
let html;
|
|
403
|
+
try {
|
|
404
|
+
html = await fsp.readFile(path.join(DIST_DIR, "verification-report.html"), "utf-8");
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
html =
|
|
408
|
+
"<html><body><p>Verification report app not built. Run <code>npm run build</code> in mcp-server/.</p></body></html>";
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
contents: [
|
|
412
|
+
{
|
|
413
|
+
uri: verificationReportUri,
|
|
414
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
415
|
+
text: html,
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
// ════════════════════════════════════════════════════════
|
|
421
|
+
// MCP App: browse_design_tokens — Visual token browser
|
|
422
|
+
// ════════════════════════════════════════════════════════
|
|
423
|
+
const tokenBrowserUri = "ui://coherence-prototyper/token-browser.html";
|
|
424
|
+
registerAppTool(server, "browse_design_tokens", {
|
|
425
|
+
title: "Design Token Browser",
|
|
426
|
+
description: "Open an interactive visual browser for Coherence design tokens — colors, spacing, typography, shadows, and more. Renders swatches and values inline.",
|
|
427
|
+
inputSchema: {
|
|
428
|
+
category: z
|
|
429
|
+
.string()
|
|
430
|
+
.optional()
|
|
431
|
+
.describe("Optional category to pre-filter: color-palette, foreground, background, brand, status, stroke, typography, spacing, border, shadow, animation, focus"),
|
|
432
|
+
},
|
|
433
|
+
_meta: {
|
|
434
|
+
ui: { resourceUri: tokenBrowserUri },
|
|
435
|
+
},
|
|
436
|
+
}, async ({ category }) => {
|
|
437
|
+
const tokens = category
|
|
438
|
+
? await getTokensByCategory(category)
|
|
439
|
+
: await getAllTokens();
|
|
440
|
+
const categories = await getTokenCategories();
|
|
441
|
+
return {
|
|
442
|
+
content: [
|
|
443
|
+
{
|
|
444
|
+
type: "text",
|
|
445
|
+
text: `Showing ${tokens.length} design tokens${category ? ` in category "${category}"` : ""}.`,
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
structuredContent: {
|
|
449
|
+
tokens: tokens.slice(0, 200),
|
|
450
|
+
categories,
|
|
451
|
+
activeCategory: category ?? null,
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
registerAppResource(server, "Design Token Browser", tokenBrowserUri, { description: "Interactive Coherence design token browser with visual swatches" }, async () => {
|
|
456
|
+
let html;
|
|
457
|
+
try {
|
|
458
|
+
html = await fsp.readFile(path.join(DIST_DIR, "token-browser.html"), "utf-8");
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
html = getFallbackTokenBrowserHtml();
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
contents: [
|
|
465
|
+
{
|
|
466
|
+
uri: tokenBrowserUri,
|
|
467
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
468
|
+
text: html,
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
};
|
|
472
|
+
});
|
|
473
|
+
// ════════════════════════════════════════════════════════
|
|
474
|
+
// MCP App: browse_icons — Visual icon browser
|
|
475
|
+
// ════════════════════════════════════════════════════════
|
|
476
|
+
const iconBrowserUri = "ui://coherence-prototyper/icon-browser.html";
|
|
477
|
+
registerAppTool(server, "browse_icons", {
|
|
478
|
+
title: "Icon Browser",
|
|
479
|
+
description: "Open an interactive visual browser for all Azure portal icons, Coherence/Fluent UI icons, and curated icon mappings. " +
|
|
480
|
+
"Shows icon images in a searchable grid. ALWAYS extract the icon keyword from the user's request and pass it as the `query` param " +
|
|
481
|
+
"(e.g. 'Help me find a Home icon' → query='Home'). The UI opens pre-filtered so the user can visually browse and click \"Use this icon\" " +
|
|
482
|
+
"to confirm their selection. After this tool returns, tell the user to browse and select their icon in the UI. " +
|
|
483
|
+
"Once the user confirms their selection, READ mcp-server/.icon-selection.json for the full icon details (name, source, url, usage snippet) " +
|
|
484
|
+
"and use that icon in subsequent code. If the file doesn't exist, the user hasn't selected yet — ask them to pick one.",
|
|
485
|
+
inputSchema: {
|
|
486
|
+
query: z
|
|
487
|
+
.string()
|
|
488
|
+
.optional()
|
|
489
|
+
.describe("Search query extracted from user's request — e.g. 'Home', 'storage', 'key vault'. ALWAYS extract this from the user's message."),
|
|
490
|
+
source: z
|
|
491
|
+
.enum(["curated", "cui-builtin", "azure-portal", "icon-collection"])
|
|
492
|
+
.optional()
|
|
493
|
+
.describe("Optional source filter: curated (friendly aliases), cui-builtin (Fluent UI), azure-portal (full inventory)"),
|
|
494
|
+
category: z
|
|
495
|
+
.string()
|
|
496
|
+
.optional()
|
|
497
|
+
.describe("Optional category to pre-filter, e.g. 'Networking', 'Compute', 'Security', 'Fluent UI — Simple'"),
|
|
498
|
+
},
|
|
499
|
+
_meta: {
|
|
500
|
+
ui: { resourceUri: iconBrowserUri },
|
|
501
|
+
},
|
|
502
|
+
}, async ({ query, source, category }) => {
|
|
503
|
+
const result = await searchIcons({ query, source, category, limit: 500 });
|
|
504
|
+
return {
|
|
505
|
+
content: [
|
|
506
|
+
{
|
|
507
|
+
type: "text",
|
|
508
|
+
text: `Showing ${result.icons.length} of ${result.total} icons${query ? ` matching "${query}"` : ""}${source ? ` from ${source}` : ""}${category ? ` in "${category}"` : ""}. The Icon Browser is now open — the user can browse, search, and click "Use this icon" to select one. After they select, read mcp-server/.icon-selection.json for the chosen icon details.`,
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
structuredContent: {
|
|
512
|
+
icons: result.icons,
|
|
513
|
+
total: result.total,
|
|
514
|
+
sources: result.sources,
|
|
515
|
+
categories: result.categories,
|
|
516
|
+
prefillQuery: query ?? null,
|
|
517
|
+
prefillSource: source ?? null,
|
|
518
|
+
prefillCategory: category ?? null,
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
// App-only tool: save icon selection (called by the UI when user clicks "Use this icon")
|
|
523
|
+
registerAppTool(server, "icon_select_data", {
|
|
524
|
+
description: "Save the user's icon selection to .icon-selection.json",
|
|
525
|
+
inputSchema: {
|
|
526
|
+
id: z.string().describe("Icon id"),
|
|
527
|
+
name: z.string().describe("Icon display name"),
|
|
528
|
+
source: z.string().describe("Icon source: curated, cui-builtin, azure-portal, icon-collection"),
|
|
529
|
+
category: z.string().describe("Icon category"),
|
|
530
|
+
url: z.string().nullable().describe("SVG URL or null for cui-builtin icons"),
|
|
531
|
+
cuiName: z.string().optional().describe("CuiIcon name attribute for cui-builtin icons"),
|
|
532
|
+
usage: z.string().describe("Code snippet showing how to use this icon"),
|
|
533
|
+
},
|
|
534
|
+
_meta: {
|
|
535
|
+
ui: {
|
|
536
|
+
resourceUri: iconBrowserUri,
|
|
537
|
+
visibility: ["app"],
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
}, async (args) => {
|
|
541
|
+
const selection = {
|
|
542
|
+
...args,
|
|
543
|
+
selectedAt: new Date().toISOString(),
|
|
544
|
+
};
|
|
545
|
+
const selectionPath = path.join(import.meta.dirname, ".icon-selection.json");
|
|
546
|
+
await fsp.writeFile(selectionPath, JSON.stringify(selection, null, 2));
|
|
547
|
+
return {
|
|
548
|
+
content: [
|
|
549
|
+
{
|
|
550
|
+
type: "text",
|
|
551
|
+
text: `Icon "${args.name}" selected. Usage: ${args.usage}`,
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
structuredContent: { selection },
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
registerAppResource(server, "Icon Browser", iconBrowserUri, { description: "Interactive visual browser for Azure & Coherence icons" }, async () => {
|
|
558
|
+
let html;
|
|
559
|
+
try {
|
|
560
|
+
html = await fsp.readFile(path.join(DIST_DIR, "icon-browser.html"), "utf-8");
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
html =
|
|
564
|
+
"<html><body><p>Icon browser not built. Run <code>npm run build</code> in mcp-server/.</p></body></html>";
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
contents: [
|
|
568
|
+
{
|
|
569
|
+
uri: iconBrowserUri,
|
|
570
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
571
|
+
text: html,
|
|
572
|
+
},
|
|
573
|
+
],
|
|
574
|
+
};
|
|
575
|
+
});
|
|
576
|
+
// ════════════════════════════════════════════════════════
|
|
577
|
+
// Reference tools: Experiment browsing (works remotely via GitHub API)
|
|
578
|
+
// ════════════════════════════════════════════════════════
|
|
579
|
+
const GITHUB_REPO = "unthinkmedia/vibe-azure";
|
|
580
|
+
const EXPERIMENTS_PATH = "coherence-preview/src/experiments";
|
|
581
|
+
const MAIN_TSX_PATH = "coherence-preview/src/main.tsx";
|
|
582
|
+
const PATTERNS_PATH = "coherence-preview/src/patterns";
|
|
583
|
+
async function githubFetch(apiPath) {
|
|
584
|
+
const headers = {
|
|
585
|
+
Accept: "application/vnd.github.v3+json",
|
|
586
|
+
"User-Agent": "coherence-prototyper-mcp",
|
|
587
|
+
};
|
|
588
|
+
if (process.env.GITHUB_TOKEN) {
|
|
589
|
+
headers.Authorization = `token ${process.env.GITHUB_TOKEN}`;
|
|
590
|
+
}
|
|
591
|
+
const res = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/${apiPath}`, { headers });
|
|
592
|
+
if (!res.ok)
|
|
593
|
+
throw new Error(`GitHub API ${res.status}: ${res.statusText}`);
|
|
594
|
+
return res.json();
|
|
595
|
+
}
|
|
596
|
+
async function githubGetFileContent(filePath) {
|
|
597
|
+
const data = await githubFetch(`contents/${filePath}`);
|
|
598
|
+
if (data.encoding === "base64" && data.content) {
|
|
599
|
+
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
600
|
+
}
|
|
601
|
+
throw new Error(`Unexpected response for ${filePath}`);
|
|
602
|
+
}
|
|
603
|
+
// Parse main.tsx to extract experiment metadata
|
|
604
|
+
function parseExperimentsFromMainTsx(source) {
|
|
605
|
+
const results = [];
|
|
606
|
+
// Match each object in the experiments array
|
|
607
|
+
const entryRegex = /\{\s*id:\s*'([^']+)',\s*title:\s*'([^']*)',\s*description:\s*'([^']*)'[^}]*?(?:date:\s*'([^']*)')?[^}]*?(?:tags:\s*\[([^\]]*)\])?[^}]*\}/g;
|
|
608
|
+
let match;
|
|
609
|
+
while ((match = entryRegex.exec(source)) !== null) {
|
|
610
|
+
const tags = match[5]
|
|
611
|
+
? match[5].split(",").map((t) => t.trim().replace(/^'|'$/g, "")).filter(Boolean)
|
|
612
|
+
: undefined;
|
|
613
|
+
results.push({
|
|
614
|
+
id: match[1],
|
|
615
|
+
title: match[2],
|
|
616
|
+
description: match[3],
|
|
617
|
+
date: match[4] || undefined,
|
|
618
|
+
tags: tags && tags.length > 0 ? tags : undefined,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
return results;
|
|
622
|
+
}
|
|
623
|
+
server.tool("list_experiments", "List all experiments in the coherence-preview gallery, with their titles, descriptions, dates, and tags. " +
|
|
624
|
+
"Fetches live metadata from the GitHub repo so it works from any workspace. " +
|
|
625
|
+
"Use this to discover existing experiments, find examples of specific page types, or reference prior work.", {
|
|
626
|
+
tags: z.array(z.string()).optional().describe("Filter by tags (e.g. ['side-panel', 'overview'])"),
|
|
627
|
+
query: z.string().optional().describe("Search filter — matches against id, title, description"),
|
|
628
|
+
}, async ({ tags, query }) => {
|
|
629
|
+
const mainTsx = await githubGetFileContent(MAIN_TSX_PATH);
|
|
630
|
+
let experiments = parseExperimentsFromMainTsx(mainTsx);
|
|
631
|
+
if (tags && tags.length > 0) {
|
|
632
|
+
experiments = experiments.filter((e) => e.tags && tags.some((t) => e.tags.includes(t)));
|
|
633
|
+
}
|
|
634
|
+
if (query) {
|
|
635
|
+
const q = query.toLowerCase();
|
|
636
|
+
experiments = experiments.filter((e) => e.id.includes(q) ||
|
|
637
|
+
e.title.toLowerCase().includes(q) ||
|
|
638
|
+
e.description.toLowerCase().includes(q));
|
|
639
|
+
}
|
|
640
|
+
let text = `# Experiments (${experiments.length})\n\n`;
|
|
641
|
+
text += "| ID | Title | Date | Tags |\n|-----|-------|------|------|\n";
|
|
642
|
+
for (const e of experiments) {
|
|
643
|
+
text += `| ${e.id} | ${e.title} | ${e.date ?? ""} | ${e.tags?.join(", ") ?? ""} |\n`;
|
|
644
|
+
}
|
|
645
|
+
return { content: [{ type: "text", text }] };
|
|
646
|
+
});
|
|
647
|
+
server.tool("get_experiment", "Get the full source files of a specific experiment from the GitHub repo. " +
|
|
648
|
+
"Returns index.tsx, data.ts, styles.ts, and intent.json (if they exist). " +
|
|
649
|
+
"Use this to study how existing experiments are structured, learn conventions, or reference patterns.", {
|
|
650
|
+
experimentId: z.string().describe("Experiment folder name (e.g. 'logic-app-designer')"),
|
|
651
|
+
files: z
|
|
652
|
+
.array(z.string())
|
|
653
|
+
.optional()
|
|
654
|
+
.describe("Specific files to fetch (default: ['index.tsx', 'data.ts', 'styles.ts', 'intent.json'])"),
|
|
655
|
+
}, async ({ experimentId, files }) => {
|
|
656
|
+
const filesToFetch = files ?? ["index.tsx", "data.ts", "styles.ts", "intent.json"];
|
|
657
|
+
const results = [];
|
|
658
|
+
for (const file of filesToFetch) {
|
|
659
|
+
try {
|
|
660
|
+
const content = await githubGetFileContent(`${EXPERIMENTS_PATH}/${experimentId}/${file}`);
|
|
661
|
+
results.push({ file, content });
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
// File doesn't exist — skip
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (results.length === 0) {
|
|
668
|
+
return {
|
|
669
|
+
content: [{ type: "text", text: `No files found for experiment "${experimentId}".` }],
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
let text = `# Experiment: ${experimentId}\n\n`;
|
|
673
|
+
for (const r of results) {
|
|
674
|
+
const ext = r.file.split(".").pop() ?? "";
|
|
675
|
+
text += `## ${r.file}\n\n\`\`\`${ext === "json" ? "json" : "tsx"}\n${r.content}\n\`\`\`\n\n`;
|
|
676
|
+
}
|
|
677
|
+
return { content: [{ type: "text", text }] };
|
|
678
|
+
});
|
|
679
|
+
server.tool("get_pattern", "Get the source code of a shared pattern or scaffold component from coherence-preview/src/patterns/. " +
|
|
680
|
+
"Use this to read PageHeader, CopilotSuggestions, ScaffoldBrowseBlade, or any other shared pattern " +
|
|
681
|
+
"when building experiments from a remote workspace that doesn't have the repo cloned.", {
|
|
682
|
+
filename: z.string().describe("Pattern filename (e.g. 'PageHeader.tsx', 'ScaffoldBrowseBlade.tsx', 'azure-icons.ts')"),
|
|
683
|
+
}, async ({ filename }) => {
|
|
684
|
+
try {
|
|
685
|
+
const content = await githubGetFileContent(`${PATTERNS_PATH}/${filename}`);
|
|
686
|
+
const ext = filename.split(".").pop() ?? "";
|
|
687
|
+
return {
|
|
688
|
+
content: [{ type: "text", text: `# Pattern: ${filename}\n\n\`\`\`${ext === "json" ? "json" : "tsx"}\n${content}\n\`\`\`` }],
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// Try listing available patterns
|
|
693
|
+
try {
|
|
694
|
+
const listing = await githubFetch(`contents/${PATTERNS_PATH}`);
|
|
695
|
+
const files = listing.map((f) => f.name).join(", ");
|
|
696
|
+
return {
|
|
697
|
+
content: [{ type: "text", text: `Pattern "${filename}" not found. Available patterns: ${files}` }],
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
return {
|
|
702
|
+
content: [{ type: "text", text: `Pattern "${filename}" not found.` }],
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
// ════════════════════════════════════════════════════════
|
|
708
|
+
// Workspace scaffolding: local preview for standalone workspaces
|
|
709
|
+
// ════════════════════════════════════════════════════════
|
|
710
|
+
server.tool("init_workspace", "Initialize a standalone workspace for local experiment development and preview. " +
|
|
711
|
+
"Writes package.json, vite.config.ts, tsconfig.json, index.html, src/preview.tsx, and shared patterns " +
|
|
712
|
+
"directly to the target directory. Run this once in a new workspace before building experiments.", {
|
|
713
|
+
targetDir: z
|
|
714
|
+
.string()
|
|
715
|
+
.describe("Absolute path to the workspace directory to initialize"),
|
|
716
|
+
installDeps: z
|
|
717
|
+
.boolean()
|
|
718
|
+
.optional()
|
|
719
|
+
.describe("If true, also run npm install after scaffolding (default: false)"),
|
|
720
|
+
}, async ({ targetDir, installDeps }) => {
|
|
721
|
+
const instructions = [];
|
|
722
|
+
const created = [];
|
|
723
|
+
// Fetch the list of all patterns from the repo
|
|
724
|
+
let patternFiles = [];
|
|
725
|
+
try {
|
|
726
|
+
const listing = await githubFetch(`contents/${PATTERNS_PATH}`);
|
|
727
|
+
patternFiles = listing
|
|
728
|
+
.filter((f) => f.type === "file")
|
|
729
|
+
.map((f) => f.name);
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
instructions.push("⚠️ Could not fetch pattern list from GitHub. You may need a GITHUB_TOKEN for API access.");
|
|
733
|
+
}
|
|
734
|
+
// Fetch each pattern's content
|
|
735
|
+
const patterns = [];
|
|
736
|
+
for (const pf of patternFiles) {
|
|
737
|
+
try {
|
|
738
|
+
const content = await githubGetFileContent(`${PATTERNS_PATH}/${pf}`);
|
|
739
|
+
patterns.push({ name: pf, content });
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
// skip
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const packageJson = JSON.stringify({
|
|
746
|
+
name: "coherence-experiment",
|
|
747
|
+
private: true,
|
|
748
|
+
type: "module",
|
|
749
|
+
scripts: {
|
|
750
|
+
dev: "vite --port 5175",
|
|
751
|
+
build: "vite build",
|
|
752
|
+
},
|
|
753
|
+
dependencies: {
|
|
754
|
+
"@charm-ux/cui": "^0.0.1-alpha.69",
|
|
755
|
+
"@types/react": "^19.2.14",
|
|
756
|
+
"@types/react-dom": "^19.2.3",
|
|
757
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
758
|
+
react: "^19.2.4",
|
|
759
|
+
"react-dom": "^19.2.4",
|
|
760
|
+
typescript: "^5.9.3",
|
|
761
|
+
vite: "^7.3.1",
|
|
762
|
+
},
|
|
763
|
+
}, null, 2);
|
|
764
|
+
// Try to copy .npmrc from the local monorepo (contains private registry config for @charm-ux/cui)
|
|
765
|
+
let npmrc = "";
|
|
766
|
+
const monorepoNpmrc = path.resolve(import.meta.dirname, "../../coherence-preview/.npmrc");
|
|
767
|
+
if (existsSync(monorepoNpmrc)) {
|
|
768
|
+
try {
|
|
769
|
+
npmrc = await fsp.readFile(monorepoNpmrc, "utf-8");
|
|
770
|
+
}
|
|
771
|
+
catch { /* fall through */ }
|
|
772
|
+
}
|
|
773
|
+
if (!npmrc) {
|
|
774
|
+
// Fallback: try fetching from GitHub (won't work if gitignored)
|
|
775
|
+
try {
|
|
776
|
+
npmrc = await githubGetFileContent("coherence-preview/.npmrc");
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
instructions.push("⚠️ Could not find .npmrc for @charm-ux registry. Copy it from a team member or the monorepo's coherence-preview/.npmrc.");
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const viteConfig = `import { defineConfig } from 'vite';
|
|
783
|
+
import react from '@vitejs/plugin-react';
|
|
784
|
+
|
|
785
|
+
export default defineConfig({
|
|
786
|
+
plugins: [react()],
|
|
787
|
+
resolve: {
|
|
788
|
+
alias: {
|
|
789
|
+
'@charm-ux/cui/react': '@charm-ux/cui/dist/react',
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
`;
|
|
794
|
+
const tsconfig = JSON.stringify({
|
|
795
|
+
compilerOptions: {
|
|
796
|
+
target: "ES2020",
|
|
797
|
+
module: "ESNext",
|
|
798
|
+
moduleResolution: "bundler",
|
|
799
|
+
jsx: "react-jsx",
|
|
800
|
+
strict: true,
|
|
801
|
+
esModuleInterop: true,
|
|
802
|
+
skipLibCheck: true,
|
|
803
|
+
forceConsistentCasingInFileNames: true,
|
|
804
|
+
resolveJsonModule: true,
|
|
805
|
+
isolatedModules: true,
|
|
806
|
+
noEmit: true,
|
|
807
|
+
paths: {
|
|
808
|
+
"../../patterns/*": ["./src/patterns/*"],
|
|
809
|
+
"../copilot-button": ["./src/patterns/CopilotSuggestions"],
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
include: ["src"],
|
|
813
|
+
}, null, 2);
|
|
814
|
+
const indexHtml = `<!DOCTYPE html>
|
|
815
|
+
<html lang="en">
|
|
816
|
+
<head>
|
|
817
|
+
<meta charset="UTF-8" />
|
|
818
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
819
|
+
<title>Experiment Preview</title>
|
|
820
|
+
</head>
|
|
821
|
+
<body>
|
|
822
|
+
<div id="root"></div>
|
|
823
|
+
<script type="module" src="/src/preview.tsx"></script>
|
|
824
|
+
</body>
|
|
825
|
+
</html>
|
|
826
|
+
`;
|
|
827
|
+
const previewTsx = `import { createRoot } from 'react-dom/client';
|
|
828
|
+
import { Suspense, lazy, useState, useEffect } from 'react';
|
|
829
|
+
import '@charm-ux/cui/dist/themes/cui/theme.css';
|
|
830
|
+
import '@charm-ux/cui/dist/themes/cui/reset.css';
|
|
831
|
+
|
|
832
|
+
// Auto-discover experiments: add lazy imports here
|
|
833
|
+
const experiments: Record<string, { title: string; component: React.LazyExoticComponent<any> }> = {};
|
|
834
|
+
|
|
835
|
+
// Scan for experiments — the builder skill will add entries above
|
|
836
|
+
// Example:
|
|
837
|
+
// experiments['my-experiment'] = {
|
|
838
|
+
// title: 'My Experiment',
|
|
839
|
+
// component: lazy(() => import('./experiments/my-experiment')),
|
|
840
|
+
// };
|
|
841
|
+
|
|
842
|
+
function App() {
|
|
843
|
+
const [id, setId] = useState(window.location.hash.slice(1) || '');
|
|
844
|
+
|
|
845
|
+
useEffect(() => {
|
|
846
|
+
const onHash = () => setId(window.location.hash.slice(1) || '');
|
|
847
|
+
window.addEventListener('hashchange', onHash);
|
|
848
|
+
return () => window.removeEventListener('hashchange', onHash);
|
|
849
|
+
}, []);
|
|
850
|
+
|
|
851
|
+
const entry = experiments[id];
|
|
852
|
+
if (!entry) {
|
|
853
|
+
return (
|
|
854
|
+
<div style={{ padding: 40, fontFamily: 'Segoe UI, sans-serif' }}>
|
|
855
|
+
<h1>Experiment Preview</h1>
|
|
856
|
+
{Object.keys(experiments).length === 0 ? (
|
|
857
|
+
<p>No experiments yet. Ask Copilot to <strong>"build me an Azure page"</strong>.</p>
|
|
858
|
+
) : (
|
|
859
|
+
<ul>
|
|
860
|
+
{Object.entries(experiments).map(([eid, e]) => (
|
|
861
|
+
<li key={eid}><a href={\`#\${eid}\`}>{e.title}</a></li>
|
|
862
|
+
))}
|
|
863
|
+
</ul>
|
|
864
|
+
)}
|
|
865
|
+
</div>
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const Comp = entry.component;
|
|
870
|
+
return (
|
|
871
|
+
<Suspense fallback={<div style={{ padding: 40 }}>Loading...</div>}>
|
|
872
|
+
<Comp />
|
|
873
|
+
</Suspense>
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
createRoot(document.getElementById('root')!).render(<App />);
|
|
878
|
+
`;
|
|
879
|
+
// Helper to write a file (creating parent dirs as needed)
|
|
880
|
+
const writeFile = async (relPath, content) => {
|
|
881
|
+
const fullPath = path.join(targetDir, relPath);
|
|
882
|
+
await fsp.mkdir(path.dirname(fullPath), { recursive: true });
|
|
883
|
+
await fsp.writeFile(fullPath, content, "utf-8");
|
|
884
|
+
created.push(relPath);
|
|
885
|
+
};
|
|
886
|
+
await writeFile("package.json", packageJson);
|
|
887
|
+
if (npmrc) {
|
|
888
|
+
await writeFile(".npmrc", npmrc);
|
|
889
|
+
}
|
|
890
|
+
await writeFile("vite.config.ts", viteConfig);
|
|
891
|
+
await writeFile("tsconfig.json", tsconfig);
|
|
892
|
+
await writeFile("index.html", indexHtml);
|
|
893
|
+
await writeFile("src/preview.tsx", previewTsx);
|
|
894
|
+
if (patterns.length > 0) {
|
|
895
|
+
for (const p of patterns) {
|
|
896
|
+
await writeFile(`src/patterns/${p.name}`, p.content);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
// Create experiments directory
|
|
900
|
+
const expDir = path.join(targetDir, "src/experiments");
|
|
901
|
+
if (!existsSync(expDir)) {
|
|
902
|
+
await fsp.mkdir(expDir, { recursive: true });
|
|
903
|
+
created.push("src/experiments/");
|
|
904
|
+
}
|
|
905
|
+
let text = `# ✅ Workspace Initialized\n\n`;
|
|
906
|
+
text += `**Directory:** ${targetDir}\n\n`;
|
|
907
|
+
text += `**Files created:** ${created.length}\n\n`;
|
|
908
|
+
text += created.map(f => `- ${f}`).join("\n") + "\n\n";
|
|
909
|
+
if (installDeps) {
|
|
910
|
+
text += `## Running npm install...\n\n`;
|
|
911
|
+
try {
|
|
912
|
+
const { execSync } = await import("child_process");
|
|
913
|
+
execSync("npm install", { cwd: targetDir, stdio: "pipe" });
|
|
914
|
+
text += `✅ Dependencies installed.\n\n`;
|
|
915
|
+
}
|
|
916
|
+
catch (e) {
|
|
917
|
+
text += `⚠️ npm install failed: ${e.message}\n\n`;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
text += `## Next Steps\n\n1. \`cd ${targetDir}\`\n2. \`npm install\`\n3. \`npm run dev\` → preview at http://localhost:5175\n`;
|
|
922
|
+
}
|
|
923
|
+
if (instructions.length > 0) {
|
|
924
|
+
text += `\n## Warnings\n\n${instructions.join("\n")}\n`;
|
|
925
|
+
}
|
|
926
|
+
return { content: [{ type: "text", text }] };
|
|
927
|
+
});
|
|
928
|
+
return server;
|
|
929
|
+
}
|
|
930
|
+
/** Inline fallback if the Vite-built HTML isn't available */
|
|
931
|
+
function getFallbackTokenBrowserHtml() {
|
|
932
|
+
return `<!DOCTYPE html>
|
|
933
|
+
<html lang="en">
|
|
934
|
+
<head>
|
|
935
|
+
<meta charset="UTF-8" />
|
|
936
|
+
<title>Design Token Browser</title>
|
|
937
|
+
<style>
|
|
938
|
+
:root {
|
|
939
|
+
--bg: #fff; --fg: #1a1a1a; --border: #e0e0e0; --accent: #0078d4;
|
|
940
|
+
--card-bg: #f5f5f5; --swatch-border: #ddd;
|
|
941
|
+
}
|
|
942
|
+
[data-theme="dark"] {
|
|
943
|
+
--bg: #1e1e1e; --fg: #e0e0e0; --border: #333; --accent: #4da6ff;
|
|
944
|
+
--card-bg: #2d2d2d; --swatch-border: #555;
|
|
945
|
+
}
|
|
946
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
947
|
+
body {
|
|
948
|
+
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
|
|
949
|
+
background: var(--bg); color: var(--fg); padding: 16px;
|
|
950
|
+
}
|
|
951
|
+
h1 { font-size: 18px; font-weight: 600; margin-bottom: 12px; }
|
|
952
|
+
.toolbar {
|
|
953
|
+
display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap;
|
|
954
|
+
}
|
|
955
|
+
.toolbar input {
|
|
956
|
+
flex: 1; min-width: 200px; padding: 6px 10px; border: 1px solid var(--border);
|
|
957
|
+
border-radius: 6px; font-size: 13px; background: var(--bg); color: var(--fg);
|
|
958
|
+
}
|
|
959
|
+
.toolbar select {
|
|
960
|
+
padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px;
|
|
961
|
+
font-size: 13px; background: var(--bg); color: var(--fg);
|
|
962
|
+
}
|
|
963
|
+
.count { font-size: 13px; color: #888; margin-bottom: 12px; }
|
|
964
|
+
.tokens {
|
|
965
|
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
966
|
+
gap: 8px;
|
|
967
|
+
}
|
|
968
|
+
.token {
|
|
969
|
+
display: flex; align-items: center; gap: 10px;
|
|
970
|
+
padding: 8px 12px; border-radius: 6px; background: var(--card-bg);
|
|
971
|
+
border: 1px solid var(--border); font-size: 12px;
|
|
972
|
+
}
|
|
973
|
+
.swatch {
|
|
974
|
+
width: 28px; height: 28px; border-radius: 4px; border: 1px solid var(--swatch-border);
|
|
975
|
+
flex-shrink: 0;
|
|
976
|
+
}
|
|
977
|
+
.token-info { overflow: hidden; }
|
|
978
|
+
.token-name { font-weight: 600; font-size: 12px; word-break: break-all; }
|
|
979
|
+
.token-value { color: #888; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
980
|
+
.category-chip {
|
|
981
|
+
display: inline-block; padding: 2px 8px; border-radius: 10px;
|
|
982
|
+
font-size: 11px; background: var(--accent); color: #fff; margin-top: 2px;
|
|
983
|
+
}
|
|
984
|
+
</style>
|
|
985
|
+
</head>
|
|
986
|
+
<body>
|
|
987
|
+
<h1>Coherence Design Tokens</h1>
|
|
988
|
+
<div class="toolbar">
|
|
989
|
+
<input id="search" type="text" placeholder="Search tokens..." />
|
|
990
|
+
<select id="category-filter"><option value="">All categories</option></select>
|
|
991
|
+
</div>
|
|
992
|
+
<div class="count" id="count"></div>
|
|
993
|
+
<div class="tokens" id="tokens"></div>
|
|
994
|
+
|
|
995
|
+
<script type="module">
|
|
996
|
+
import { App } from "@modelcontextprotocol/ext-apps";
|
|
997
|
+
const app = new App({ name: "Token Browser", version: "1.0.0" });
|
|
998
|
+
|
|
999
|
+
let allTokens = [];
|
|
1000
|
+
let allCategories = [];
|
|
1001
|
+
let activeCategory = "";
|
|
1002
|
+
|
|
1003
|
+
const searchEl = document.getElementById("search");
|
|
1004
|
+
const filterEl = document.getElementById("category-filter");
|
|
1005
|
+
const countEl = document.getElementById("count");
|
|
1006
|
+
const tokensEl = document.getElementById("tokens");
|
|
1007
|
+
|
|
1008
|
+
function isColorValue(val) {
|
|
1009
|
+
return /^(#|rgb|hsl|oklch|color\(|var\()/.test(val.trim()) ||
|
|
1010
|
+
/^(transparent|currentColor|inherit)$/i.test(val.trim());
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function render() {
|
|
1014
|
+
const q = searchEl.value.toLowerCase();
|
|
1015
|
+
const cat = filterEl.value;
|
|
1016
|
+
const filtered = allTokens.filter(t => {
|
|
1017
|
+
if (cat && t.category !== cat) return false;
|
|
1018
|
+
if (q && !t.name.toLowerCase().includes(q) && !t.value.toLowerCase().includes(q)) return false;
|
|
1019
|
+
return true;
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
countEl.textContent = filtered.length + " tokens" + (cat ? " in " + cat : "") + (q ? " matching "" + q + """ : "");
|
|
1023
|
+
tokensEl.innerHTML = filtered.slice(0, 200).map(t => {
|
|
1024
|
+
const showSwatch = isColorValue(t.value) || t.category === "color-palette" || t.category === "foreground" || t.category === "background" || t.category === "brand" || t.category === "status" || t.category === "stroke";
|
|
1025
|
+
return '<div class="token">' +
|
|
1026
|
+
(showSwatch ? '<div class="swatch" style="background:' + t.value.replace(/"/g, '') + '"></div>' : '') +
|
|
1027
|
+
'<div class="token-info">' +
|
|
1028
|
+
'<div class="token-name">' + t.name + '</div>' +
|
|
1029
|
+
'<div class="token-value">' + t.value + '</div>' +
|
|
1030
|
+
'<span class="category-chip">' + t.category + '</span>' +
|
|
1031
|
+
'</div></div>';
|
|
1032
|
+
}).join("");
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function populateCategories() {
|
|
1036
|
+
filterEl.innerHTML = '<option value="">All categories</option>';
|
|
1037
|
+
for (const c of allCategories) {
|
|
1038
|
+
const opt = document.createElement("option");
|
|
1039
|
+
opt.value = c.category;
|
|
1040
|
+
opt.textContent = c.category + " (" + c.count + ")";
|
|
1041
|
+
if (c.category === activeCategory) opt.selected = true;
|
|
1042
|
+
filterEl.appendChild(opt);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
app.ontoolresult = (result) => {
|
|
1047
|
+
const data = result.structuredContent;
|
|
1048
|
+
if (data) {
|
|
1049
|
+
allTokens = data.tokens || [];
|
|
1050
|
+
allCategories = data.categories || [];
|
|
1051
|
+
activeCategory = data.activeCategory || "";
|
|
1052
|
+
populateCategories();
|
|
1053
|
+
if (activeCategory) filterEl.value = activeCategory;
|
|
1054
|
+
render();
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
searchEl.addEventListener("input", render);
|
|
1059
|
+
filterEl.addEventListener("change", render);
|
|
1060
|
+
|
|
1061
|
+
// Theme support
|
|
1062
|
+
app.onhostcontextchanged = (ctx) => {
|
|
1063
|
+
if (ctx.theme) {
|
|
1064
|
+
document.documentElement.setAttribute("data-theme", ctx.theme);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
app.connect().then(() => {
|
|
1069
|
+
const ctx = app.getHostContext();
|
|
1070
|
+
if (ctx?.theme) {
|
|
1071
|
+
document.documentElement.setAttribute("data-theme", ctx.theme);
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
</script>
|
|
1075
|
+
</body>
|
|
1076
|
+
</html>`;
|
|
1077
|
+
}
|
|
1078
|
+
//# sourceMappingURL=server.js.map
|