@aprovan/mcp-app-server 0.1.0-dev.c1ac1dc

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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +20 -0
  2. package/E2E_TESTING.md +198 -0
  3. package/LICENSE +373 -0
  4. package/README.md +134 -0
  5. package/dist/index.d.ts +108 -0
  6. package/dist/index.js +1592 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/server.d.ts +2 -0
  9. package/dist/server.js +1841 -0
  10. package/dist/server.js.map +1 -0
  11. package/package.json +52 -0
  12. package/src/__tests__/cache.test.ts +119 -0
  13. package/src/__tests__/cdn-plugin.test.ts +86 -0
  14. package/src/__tests__/compile.test.ts +93 -0
  15. package/src/__tests__/e2e-pipeline.test.ts +417 -0
  16. package/src/__tests__/live-update.test.ts +158 -0
  17. package/src/__tests__/local-backend.test.ts +100 -0
  18. package/src/__tests__/memory-backend.ts +144 -0
  19. package/src/__tests__/registry-backend.test.ts +256 -0
  20. package/src/__tests__/services.test.ts +153 -0
  21. package/src/__tests__/shim.test.ts +132 -0
  22. package/src/__tests__/widget-store.test.ts +183 -0
  23. package/src/compiler/cache.ts +68 -0
  24. package/src/compiler/cdn-plugin.ts +197 -0
  25. package/src/compiler/compile.ts +306 -0
  26. package/src/compiler/index.ts +3 -0
  27. package/src/hello-world.html +79 -0
  28. package/src/html.d.ts +4 -0
  29. package/src/index.ts +641 -0
  30. package/src/live-update.ts +149 -0
  31. package/src/reference-widgets/live-dashboard.ts +162 -0
  32. package/src/registry-backend.ts +304 -0
  33. package/src/server.ts +178 -0
  34. package/src/services.ts +251 -0
  35. package/src/shim.ts +247 -0
  36. package/src/widget-store/index.ts +10 -0
  37. package/src/widget-store/local-backend.ts +77 -0
  38. package/src/widget-store/store.ts +198 -0
  39. package/src/widget-store/types.ts +50 -0
  40. package/tsconfig.json +10 -0
  41. package/tsup.config.ts +14 -0
package/dist/index.js ADDED
@@ -0,0 +1,1592 @@
1
+ // src/index.ts
2
+ import {
3
+ createProjectFromFiles
4
+ } from "@aprovan/patchwork-compiler";
5
+ import {
6
+ registerAppTool,
7
+ registerAppResource,
8
+ RESOURCE_MIME_TYPE
9
+ } from "@modelcontextprotocol/ext-apps/server";
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { z as z2 } from "zod";
12
+
13
+ // src/compiler/compile.ts
14
+ import { randomUUID } from "crypto";
15
+ import { mkdir, writeFile, rm, readFile } from "fs/promises";
16
+ import { join, resolve, dirname } from "path";
17
+ import { fileURLToPath } from "url";
18
+ import {
19
+ createCompiler,
20
+ loadImage
21
+ } from "@aprovan/patchwork-compiler";
22
+ import react from "@vitejs/plugin-react";
23
+ import { build } from "vite";
24
+ import { viteSingleFile } from "vite-plugin-singlefile";
25
+
26
+ // src/shim.ts
27
+ var DEFAULT_EXT_APPS_VERSION = "^1.7.3";
28
+ function generateServiceShim(options) {
29
+ const {
30
+ namespaces,
31
+ extAppsVersion = DEFAULT_EXT_APPS_VERSION
32
+ } = options;
33
+ if (namespaces.length === 0) return "";
34
+ const namespacesJson = JSON.stringify(namespaces);
35
+ const importUrl = `https://esm.sh/@modelcontextprotocol/ext-apps@${extAppsVersion}`;
36
+ return `
37
+ import { App } from '${importUrl}';
38
+
39
+ const __patchwork_app = new App({ name: 'patchwork-widget', version: '0.1.0' });
40
+ const __patchwork_ready = __patchwork_app.connect().catch(function(err) {
41
+ console.error('[patchwork] Failed to connect to host:', err);
42
+ });
43
+
44
+ function __patchwork_createNamespaceProxy(namespace) {
45
+ return new Proxy({}, {
46
+ get: function(target, prop) {
47
+ if (typeof prop !== 'string') return undefined;
48
+ return function __patchwork_serviceCall(args) {
49
+ var toolName = namespace + '__' + prop;
50
+ return __patchwork_ready.then(function() {
51
+ return __patchwork_app.callServerTool({ name: toolName, arguments: args || {} });
52
+ }).then(function(result) {
53
+ if (result.isError) {
54
+ var errorMsg = result.content && result.content[0] && result.content[0].text
55
+ ? result.content[0].text
56
+ : 'Service call failed: ' + namespace + '.' + prop;
57
+ throw new Error(errorMsg);
58
+ }
59
+ var textContent = result.content && result.content.find(function(c) { return c.type === 'text'; });
60
+ if (textContent) {
61
+ try { return JSON.parse(textContent.text); }
62
+ catch (e) { return textContent.text; }
63
+ }
64
+ return result;
65
+ });
66
+ };
67
+ }
68
+ });
69
+ }
70
+
71
+ var __patchwork_namespaces = ${namespacesJson};
72
+ for (var __i = 0; __i < __patchwork_namespaces.length; __i++) {
73
+ var __ns = __patchwork_namespaces[__i];
74
+ window[__ns] = __patchwork_createNamespaceProxy(__ns);
75
+ }
76
+ `;
77
+ }
78
+ function generateLiveUpdateShim(options = {}) {
79
+ const { extAppsVersion = DEFAULT_EXT_APPS_VERSION } = options;
80
+ const importUrl = `https://esm.sh/@modelcontextprotocol/ext-apps@${extAppsVersion}`;
81
+ return `
82
+ import { App } from '${importUrl}';
83
+
84
+ // Shared App instance \u2014 the service shim may have already created this.
85
+ if (!window.__patchwork_app) {
86
+ window.__patchwork_app = new App({ name: 'patchwork-widget', version: '0.1.0' });
87
+ window.__patchwork_ready = window.__patchwork_app.connect().catch(function(err) {
88
+ console.error('[patchwork] Failed to connect to host:', err);
89
+ });
90
+ }
91
+
92
+ (function() {
93
+ var __app = window.__patchwork_app;
94
+ var __ready = window.__patchwork_ready;
95
+
96
+ // Per-stream state: { seq, callbacks }
97
+ var __streams = {};
98
+ // MCP session ID captured after connect
99
+ var __sessionId = null;
100
+
101
+ function __parseResult(result) {
102
+ if (!result || !result.content) return result;
103
+ var textContent = result.content.find(function(c) { return c.type === 'text'; });
104
+ if (textContent) {
105
+ try { return JSON.parse(textContent.text); }
106
+ catch (e) { return textContent.text; }
107
+ }
108
+ return result;
109
+ }
110
+
111
+ function __pollStream(stream) {
112
+ var state = __streams[stream];
113
+ if (!state) return;
114
+ var afterSeq = state.seq;
115
+ __app.callServerTool({ name: 'poll_updates', arguments: { stream: stream, after_seq: afterSeq } })
116
+ .then(function(result) {
117
+ var parsed = __parseResult(result);
118
+ if (!parsed || !parsed.success || !parsed.events || !parsed.events.length) return;
119
+ var events = parsed.events;
120
+ for (var i = 0; i < events.length; i++) {
121
+ var ev = events[i];
122
+ if (ev.seq > state.seq) {
123
+ state.seq = ev.seq;
124
+ }
125
+ for (var j = 0; j < state.callbacks.length; j++) {
126
+ try { state.callbacks[j](ev.data, ev.seq, stream); }
127
+ catch (cbErr) { console.error('[patchwork] subscribe callback error:', cbErr); }
128
+ }
129
+ }
130
+ })
131
+ .catch(function(err) {
132
+ console.warn('[patchwork] poll_updates failed for stream ' + stream + ':', err);
133
+ });
134
+ }
135
+
136
+ // When tools/list_changed fires, poll all subscribed streams.
137
+ __app.setNotificationHandler(
138
+ { method: 'notifications/tools/list_changed', params: {} },
139
+ function() {
140
+ var streamNames = Object.keys(__streams);
141
+ for (var i = 0; i < streamNames.length; i++) {
142
+ __pollStream(streamNames[i]);
143
+ }
144
+ return Promise.resolve();
145
+ }
146
+ );
147
+
148
+ // Expose the public patchwork API on window.
149
+ window.patchwork = {
150
+ /**
151
+ * Subscribe to a named data stream.
152
+ *
153
+ * @param {string} stream - Stream name (e.g. "price_feed").
154
+ * @param {function} callback - Called with (data, seq, stream) for each event.
155
+ * @returns {function} Unsubscribe function.
156
+ */
157
+ subscribe: function(stream, callback) {
158
+ if (!__streams[stream]) {
159
+ __streams[stream] = { seq: 0, callbacks: [] };
160
+ // Tell the server about this subscription once the App is connected.
161
+ __ready.then(function() {
162
+ var args = { stream: stream };
163
+ if (__sessionId) args.session_id = __sessionId;
164
+ return __app.callServerTool({ name: 'subscribe_stream', arguments: args });
165
+ }).then(function(result) {
166
+ var parsed = __parseResult(result);
167
+ if (parsed && typeof parsed.seq === 'number') {
168
+ // Start polling from the server's current seq.
169
+ if (__streams[stream]) {
170
+ __streams[stream].seq = parsed.seq;
171
+ }
172
+ }
173
+ }).catch(function(err) {
174
+ console.warn('[patchwork] subscribe_stream failed:', err);
175
+ });
176
+ }
177
+ __streams[stream].callbacks.push(callback);
178
+ return function unsubscribe() {
179
+ if (!__streams[stream]) return;
180
+ var idx = __streams[stream].callbacks.indexOf(callback);
181
+ if (idx !== -1) __streams[stream].callbacks.splice(idx, 1);
182
+ };
183
+ },
184
+
185
+ /**
186
+ * Push the widget's current state into the conversation context.
187
+ * The model will include this in its next response.
188
+ *
189
+ * @param {string|Array} content - Text string or array of MCP ContentBlock objects.
190
+ */
191
+ updateContext: function(content) {
192
+ return __ready.then(function() {
193
+ var params;
194
+ if (typeof content === 'string') {
195
+ params = { content: [{ type: 'text', text: content }] };
196
+ } else if (Array.isArray(content)) {
197
+ params = { content: content };
198
+ } else if (content && typeof content === 'object') {
199
+ params = { structuredContent: content };
200
+ } else {
201
+ params = { content: [{ type: 'text', text: String(content) }] };
202
+ }
203
+ return __app.request({ method: 'ui/update-model-context', params: params }, {});
204
+ });
205
+ },
206
+
207
+ /**
208
+ * Fire a client event as an MCP tool call. Use this to surface user
209
+ * interactions so the LLM can observe and react to them.
210
+ *
211
+ * @param {string} toolName - MCP tool name on the server.
212
+ * @param {object} args - Tool arguments.
213
+ */
214
+ fireEvent: function(toolName, args) {
215
+ return __ready.then(function() {
216
+ return __app.callServerTool({ name: toolName, arguments: args || {} });
217
+ }).then(function(result) {
218
+ return __parseResult(result);
219
+ });
220
+ },
221
+ };
222
+
223
+ // Capture the session ID from the host context on connect.
224
+ __ready.then(function() {
225
+ // The host context is available via getHostContext(); we can't get the
226
+ // session ID directly, but it's passed in subscribe_stream args from
227
+ // user code. Expose a setter so widgets can supply it explicitly.
228
+ window.patchwork._setSessionId = function(id) { __sessionId = id; };
229
+ }).catch(function() {});
230
+ })();
231
+ `;
232
+ }
233
+
234
+ // src/compiler/cache.ts
235
+ import { createHash } from "crypto";
236
+ var MAX_CACHE_SIZE = 256;
237
+ var cache = /* @__PURE__ */ new Map();
238
+ function computeCacheKey(source, manifest) {
239
+ const parts = [];
240
+ if (typeof source === "string") {
241
+ parts.push(source);
242
+ } else {
243
+ const sortedFiles = Array.from(source.files.entries()).sort(
244
+ ([a], [b]) => a.localeCompare(b)
245
+ );
246
+ for (const [path, file] of sortedFiles) {
247
+ parts.push(`${path}::${file.content}`);
248
+ }
249
+ parts.push(`entry:${source.entry}`);
250
+ }
251
+ parts.push(JSON.stringify(manifest));
252
+ return createHash("sha256").update(parts.join("\n")).digest("hex").slice(0, 16);
253
+ }
254
+ function get(hash) {
255
+ return cache.get(hash);
256
+ }
257
+ function set(hash, entry) {
258
+ if (cache.size >= MAX_CACHE_SIZE) {
259
+ const entries = Array.from(cache.entries());
260
+ entries.sort((a, b) => a[1].createdAt - b[1].createdAt);
261
+ const toRemove = entries.slice(0, Math.floor(MAX_CACHE_SIZE / 4));
262
+ for (const [key] of toRemove) {
263
+ cache.delete(key);
264
+ }
265
+ }
266
+ cache.set(hash, entry);
267
+ }
268
+ function has(hash) {
269
+ return cache.has(hash);
270
+ }
271
+ function allEntries() {
272
+ return Array.from(cache.entries());
273
+ }
274
+
275
+ // src/compiler/cdn-plugin.ts
276
+ var ESM_SH_BASE = "https://esm.sh";
277
+ var REACT_EXPORTS = [
278
+ "useState",
279
+ "useEffect",
280
+ "useCallback",
281
+ "useMemo",
282
+ "useRef",
283
+ "useContext",
284
+ "useReducer",
285
+ "useLayoutEffect",
286
+ "useId",
287
+ "createContext",
288
+ "createElement",
289
+ "cloneElement",
290
+ "createRef",
291
+ "forwardRef",
292
+ "lazy",
293
+ "memo",
294
+ "Fragment",
295
+ "Suspense",
296
+ "StrictMode",
297
+ "Component",
298
+ "PureComponent",
299
+ "Children",
300
+ "isValidElement"
301
+ ];
302
+ var REACT_DOM_EXPORTS = [
303
+ "createPortal",
304
+ "flushSync",
305
+ "render",
306
+ "hydrate",
307
+ "unmountComponentAtNode"
308
+ ];
309
+ function toEsmShUrl(packageName, version, subpath, deps) {
310
+ let url = `${ESM_SH_BASE}/${packageName}`;
311
+ if (version) url += `@${version}`;
312
+ if (subpath) url += `/${subpath}`;
313
+ if (deps && Object.keys(deps).length > 0) {
314
+ const depsStr = Object.entries(deps).map(([name, ver]) => `${name}@${ver}`).join(",");
315
+ url += `?deps=${depsStr}`;
316
+ }
317
+ return url;
318
+ }
319
+ function parseImportPath(importPath) {
320
+ if (importPath.startsWith("@")) {
321
+ const parts2 = importPath.split("/");
322
+ if (parts2.length >= 2) {
323
+ const packageName = `${parts2[0]}/${parts2[1]}`;
324
+ const subpath = parts2.slice(2).join("/");
325
+ return { packageName, subpath: subpath || void 0 };
326
+ }
327
+ }
328
+ const parts = importPath.split("/");
329
+ return { packageName: parts[0] ?? "", subpath: parts.slice(1).join("/") || void 0 };
330
+ }
331
+ function isBareImport(path) {
332
+ return !(path.startsWith(".") || path.startsWith("/") || path.startsWith("http://") || path.startsWith("https://"));
333
+ }
334
+ function matchAlias(importPath, aliases) {
335
+ for (const [pattern, target] of Object.entries(aliases)) {
336
+ if (pattern.endsWith("/*")) {
337
+ const prefix = pattern.slice(0, -2);
338
+ if (importPath === prefix || importPath.startsWith(prefix + "/")) {
339
+ return target;
340
+ }
341
+ }
342
+ if (importPath === pattern) {
343
+ return target;
344
+ }
345
+ }
346
+ return null;
347
+ }
348
+ function patchworkCdnPlugin(options) {
349
+ const { imageConfig } = options;
350
+ const globals = imageConfig.framework?.globals ?? {};
351
+ const deps = imageConfig.framework?.deps ?? {};
352
+ const aliases = imageConfig.aliases ?? {};
353
+ const packages = options.packages ?? {};
354
+ const globalsSet = new Set(Object.keys(globals));
355
+ return {
356
+ name: "patchwork-cdn",
357
+ enforce: "pre",
358
+ resolveId(source) {
359
+ if (!isBareImport(source)) return null;
360
+ const aliasTarget = matchAlias(source, aliases);
361
+ if (aliasTarget) {
362
+ const { packageName: packageName2, subpath: subpath2 } = parseImportPath(aliasTarget);
363
+ if (globalsSet.has(packageName2)) {
364
+ return `${packageName2}${subpath2 ? `/${subpath2}` : ""}`;
365
+ }
366
+ const version2 = packages[packageName2];
367
+ const url2 = toEsmShUrl(
368
+ packageName2,
369
+ version2,
370
+ subpath2,
371
+ Object.keys(deps).length > 0 ? deps : void 0
372
+ );
373
+ return { id: url2, external: true };
374
+ }
375
+ const { packageName, subpath } = parseImportPath(source);
376
+ if (globalsSet.has(packageName)) {
377
+ return source;
378
+ }
379
+ const version = packages[packageName];
380
+ const url = toEsmShUrl(
381
+ packageName,
382
+ version,
383
+ subpath,
384
+ Object.keys(deps).length > 0 ? deps : void 0
385
+ );
386
+ return { id: url, external: true };
387
+ },
388
+ load(id) {
389
+ const { packageName, subpath } = parseImportPath(id);
390
+ const globalName = globals[packageName];
391
+ if (!globalName) return null;
392
+ if (subpath) {
393
+ const url = toEsmShUrl(
394
+ packageName,
395
+ packages[packageName],
396
+ subpath,
397
+ Object.keys(deps).length > 0 ? deps : void 0
398
+ );
399
+ return `export * from '${url}'; export { default } from '${url}';`;
400
+ }
401
+ const commonExports = packageName === "react" ? REACT_EXPORTS : packageName === "react-dom" ? REACT_DOM_EXPORTS : [];
402
+ const lines = [
403
+ `const mod = window.${globalName};`,
404
+ `export default mod;`
405
+ ];
406
+ if (commonExports.length > 0) {
407
+ lines.push(
408
+ `const { ${commonExports.join(", ")} } = mod;`,
409
+ `export { ${commonExports.join(", ")} };`
410
+ );
411
+ }
412
+ return lines.join("\n");
413
+ }
414
+ };
415
+ }
416
+ function getPreloadScripts(imageConfig) {
417
+ const preload = imageConfig.framework?.preload ?? [];
418
+ return preload.map((url) => `<script src="${url}"></script>`);
419
+ }
420
+
421
+ // src/compiler/compile.ts
422
+ var WIDGET_RESOURCE_PREFIX = "ui://widget/";
423
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
424
+ var COMPILE_TMP_DIR = resolve(__dirname2, "..", ".compile-tmp");
425
+ var FALLBACK_IMAGE_CONFIG = {
426
+ platform: "browser",
427
+ esbuild: { target: "es2020", format: "esm", jsx: "automatic" },
428
+ framework: {
429
+ globals: { react: "React", "react-dom": "ReactDOM" },
430
+ preload: [
431
+ "https://esm.sh/react@18",
432
+ "https://esm.sh/react-dom@18/client"
433
+ ],
434
+ deps: { react: "18", "react-dom": "18" }
435
+ }
436
+ };
437
+ var loadedImage = null;
438
+ var imageLoadPromise = null;
439
+ async function getImageConfig() {
440
+ if (loadedImage) return loadedImage.config;
441
+ if (imageLoadPromise) return imageLoadPromise.then((img) => img.config);
442
+ imageLoadPromise = (async () => {
443
+ try {
444
+ loadedImage = await loadImage("@aprovan/patchwork-image-shadcn");
445
+ return loadedImage;
446
+ } catch {
447
+ const compiler = await createCompiler({
448
+ image: "@aprovan/patchwork-image-shadcn",
449
+ proxyUrl: ""
450
+ });
451
+ await compiler.preloadImage("@aprovan/patchwork-image-shadcn");
452
+ const registry = compiler.registry;
453
+ const img = registry?.get("@aprovan/patchwork-image-shadcn");
454
+ if (img) {
455
+ loadedImage = img;
456
+ return img;
457
+ }
458
+ return { config: FALLBACK_IMAGE_CONFIG };
459
+ }
460
+ })();
461
+ return imageLoadPromise.then((img) => img.config);
462
+ }
463
+ function generateHtmlEntry(preloads, cssVars) {
464
+ return `<!DOCTYPE html>
465
+ <html lang="en">
466
+ <head>
467
+ <meta charset="utf-8" />
468
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
469
+ ${preloads.join("\n ")}
470
+ <script src="https://cdn.tailwindcss.com"></script>
471
+ <script>
472
+ window.tailwind = window.tailwind || {};
473
+ window.tailwind.config = {
474
+ darkMode: "class",
475
+ theme: {
476
+ extend: {
477
+ colors: {
478
+ border: "hsl(var(--border))",
479
+ input: "hsl(var(--input))",
480
+ ring: "hsl(var(--ring))",
481
+ background: "hsl(var(--background))",
482
+ foreground: "hsl(var(--foreground))",
483
+ primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
484
+ secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
485
+ destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
486
+ muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
487
+ accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
488
+ popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))" },
489
+ card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
490
+ },
491
+ borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)" },
492
+ },
493
+ },
494
+ };
495
+ </script>
496
+ <style>
497
+ :root {
498
+ ${cssVars}
499
+ }
500
+ </style>
501
+ </head>
502
+ <body>
503
+ <div id="root"></div>
504
+ <script type="module" src="/src/_app.tsx"></script>
505
+ </body>
506
+ </html>`;
507
+ }
508
+ var SHADCN_CSS_VARS = `
509
+ --background: 0 0% 100%;
510
+ --foreground: 222.2 84% 4.9%;
511
+ --card: 0 0% 100%;
512
+ --card-foreground: 222.2 84% 4.9%;
513
+ --popover: 0 0% 100%;
514
+ --popover-foreground: 222.2 84% 4.9%;
515
+ --primary: 222.2 47.4% 11.2%;
516
+ --primary-foreground: 210 40% 98%;
517
+ --secondary: 210 40% 96.1%;
518
+ --secondary-foreground: 222.2 47.4% 11.2%;
519
+ --muted: 210 40% 96.1%;
520
+ --muted-foreground: 215.4 16.3% 46.9%;
521
+ --accent: 210 40% 96.1%;
522
+ --accent-foreground: 222.2 47.4% 11.2%;
523
+ --destructive: 0 84.2% 60.2%;
524
+ --destructive-foreground: 210 40% 98%;
525
+ --border: 214.3 31.8% 91.4%;
526
+ --input: 214.3 31.8% 91.4%;
527
+ --ring: 222.2 84% 4.9%;
528
+ --radius: 0.5rem;
529
+ `.trim();
530
+ function generateMainTsx(entryModule) {
531
+ return `import React from 'react';
532
+ import { createRoot } from 'react-dom/client';
533
+ import Widget from './${entryModule}';
534
+
535
+ const rootEl = document.getElementById('root');
536
+ if (rootEl) {
537
+ const root = createRoot(rootEl);
538
+ root.render(React.createElement(Widget));
539
+ }
540
+ `;
541
+ }
542
+ async function writeProjectFiles(projectDir, source) {
543
+ const srcDir = join(projectDir, "src");
544
+ await mkdir(srcDir, { recursive: true });
545
+ if (typeof source === "string") {
546
+ await writeFile(join(srcDir, "widget.tsx"), source, "utf-8");
547
+ return "widget";
548
+ }
549
+ for (const [filePath, file] of source.files) {
550
+ const fullPath = filePath.startsWith("src/") ? join(projectDir, filePath) : join(srcDir, filePath);
551
+ const dir = resolve(fullPath, "..");
552
+ await mkdir(dir, { recursive: true });
553
+ await writeFile(fullPath, file.content, "utf-8");
554
+ }
555
+ const entry = source.entry;
556
+ const entryName = entry.replace(/\.(tsx|ts|jsx|js)$/, "");
557
+ return entryName.startsWith("src/") ? entryName.slice(4) : entryName;
558
+ }
559
+ function injectShimIntoHtml(html, shimScript) {
560
+ const shimTag = `<script type="module">
561
+ ${shimScript}
562
+ </script>`;
563
+ const bodyCloseIndex = html.lastIndexOf("</body>");
564
+ if (bodyCloseIndex === -1) return html;
565
+ return html.slice(0, bodyCloseIndex) + shimTag + "\n" + html.slice(bodyCloseIndex);
566
+ }
567
+ async function compileWidget(source, manifest, options = {}) {
568
+ const liveUpdates = options.liveUpdates ?? true;
569
+ const baseCacheKey = computeCacheKey(source, manifest);
570
+ let cacheKey = baseCacheKey;
571
+ if (options.services?.length) {
572
+ cacheKey += `:svc:${options.services.sort().join(",")}`;
573
+ }
574
+ if (liveUpdates) {
575
+ cacheKey += `:live`;
576
+ }
577
+ if (has(cacheKey)) {
578
+ const cached = get(cacheKey);
579
+ return {
580
+ html: cached.html,
581
+ hash: cacheKey,
582
+ resourceUri: cached.resourceUri
583
+ };
584
+ }
585
+ const imageConfig = await getImageConfig();
586
+ await mkdir(COMPILE_TMP_DIR, { recursive: true });
587
+ const projectDir = join(COMPILE_TMP_DIR, `build-${randomUUID()}`);
588
+ try {
589
+ await mkdir(projectDir, { recursive: true });
590
+ const entryModule = await writeProjectFiles(projectDir, source);
591
+ await writeFile(
592
+ join(projectDir, "src", "_app.tsx"),
593
+ generateMainTsx(entryModule),
594
+ "utf-8"
595
+ );
596
+ const preloads = getPreloadScripts(imageConfig);
597
+ const htmlContent = generateHtmlEntry(preloads, SHADCN_CSS_VARS);
598
+ await writeFile(join(projectDir, "index.html"), htmlContent, "utf-8");
599
+ const viteConfig = {
600
+ root: projectDir,
601
+ base: "./",
602
+ plugins: [
603
+ patchworkCdnPlugin({ imageConfig }),
604
+ react({
605
+ jsxRuntime: "classic"
606
+ }),
607
+ viteSingleFile({ useRecommendedBuildConfig: true })
608
+ ],
609
+ build: {
610
+ outDir: "dist",
611
+ emptyOutDir: true,
612
+ minify: false,
613
+ rollupOptions: {
614
+ input: resolve(projectDir, "index.html")
615
+ }
616
+ },
617
+ logLevel: "silent"
618
+ };
619
+ await build(viteConfig);
620
+ const outputHtml = await readFile(
621
+ join(projectDir, "dist", "index.html"),
622
+ "utf-8"
623
+ );
624
+ const shimParts = [];
625
+ if (liveUpdates) {
626
+ shimParts.push(generateLiveUpdateShim());
627
+ }
628
+ if (options.services?.length) {
629
+ shimParts.push(generateServiceShim({ namespaces: options.services }));
630
+ }
631
+ let finalHtml = outputHtml;
632
+ for (const script of shimParts) {
633
+ finalHtml = injectShimIntoHtml(finalHtml, script);
634
+ }
635
+ const resourceUri = `${WIDGET_RESOURCE_PREFIX}${cacheKey}/view.html`;
636
+ const cacheEntry = {
637
+ html: finalHtml,
638
+ manifest,
639
+ resourceUri,
640
+ createdAt: Date.now()
641
+ };
642
+ set(cacheKey, cacheEntry);
643
+ return { html: finalHtml, hash: cacheKey, resourceUri };
644
+ } finally {
645
+ await rm(projectDir, { recursive: true, force: true });
646
+ }
647
+ }
648
+
649
+ // src/hello-world.html
650
+ var hello_world_default = '<!DOCTYPE html>\n<html lang="en">\n <head>\n <meta charset="utf-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1" />\n <title>Hello World \u2014 Patchwork MCP App</title>\n <style>\n *,\n *::before,\n *::after {\n box-sizing: border-box;\n }\n\n body {\n margin: 0;\n font-family: system-ui, -apple-system, sans-serif;\n background: #f0f4ff;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n\n .card {\n background: #fff;\n border-radius: 1rem;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);\n padding: 2.5rem 3rem;\n text-align: center;\n max-width: 420px;\n width: 100%;\n }\n\n .wave {\n font-size: 3rem;\n line-height: 1;\n margin-bottom: 1rem;\n }\n\n h1 {\n margin: 0 0 0.75rem;\n font-size: 1.75rem;\n color: #111827;\n }\n\n p {\n margin: 0;\n color: #6b7280;\n font-size: 0.95rem;\n line-height: 1.6;\n }\n\n .badge {\n display: inline-block;\n margin-top: 1.5rem;\n background: #eff6ff;\n color: #3b82f6;\n font-size: 0.75rem;\n font-weight: 600;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n padding: 0.25rem 0.75rem;\n border-radius: 999px;\n }\n </style>\n </head>\n <body>\n <div class="card">\n <div class="wave">\u{1F44B}</div>\n <h1>Hello, world!</h1>\n <p>\n This widget is served by the <strong>Patchwork MCP App Server</strong>.\n It renders inline inside Claude Desktop and Claude web via a\n <code>StreamableHTTPServerTransport</code>.\n </p>\n <span class="badge">MCP App</span>\n </div>\n </body>\n</html>\n';
651
+
652
+ // src/live-update.ts
653
+ var RING_BUFFER_MAX = 100;
654
+ var streamBuffers = /* @__PURE__ */ new Map();
655
+ var globalSeq = 0;
656
+ function getOrCreateBuffer(stream) {
657
+ let buf = streamBuffers.get(stream);
658
+ if (!buf) {
659
+ buf = [];
660
+ streamBuffers.set(stream, buf);
661
+ }
662
+ return buf;
663
+ }
664
+ function getEvents(stream, afterSeq) {
665
+ const buf = streamBuffers.get(stream);
666
+ if (!buf) return [];
667
+ return buf.filter((e) => e.seq > afterSeq);
668
+ }
669
+ function appendEvent(stream, data) {
670
+ const buf = getOrCreateBuffer(stream);
671
+ const event = {
672
+ seq: ++globalSeq,
673
+ data,
674
+ timestamp: Date.now()
675
+ };
676
+ buf.push(event);
677
+ if (buf.length > RING_BUFFER_MAX) {
678
+ buf.shift();
679
+ }
680
+ return event;
681
+ }
682
+ var sessions = /* @__PURE__ */ new Map();
683
+ function subscribeSession(sessionId, stream) {
684
+ const entry = sessions.get(sessionId);
685
+ if (entry) {
686
+ entry.streams.add(stream);
687
+ }
688
+ }
689
+ async function pushStreamUpdate(stream, data) {
690
+ const event = appendEvent(stream, data);
691
+ const pushPromises = [];
692
+ for (const [, entry] of sessions) {
693
+ if (entry.streams.has(stream)) {
694
+ const notifyPromise = entry.server.server.notification({ method: "notifications/tools/list_changed" }).catch((err) => {
695
+ console.warn("[live-update] Failed to notify session:", err);
696
+ });
697
+ pushPromises.push(notifyPromise);
698
+ }
699
+ }
700
+ await Promise.all(pushPromises);
701
+ return event.seq;
702
+ }
703
+ function currentSeq() {
704
+ return globalSeq;
705
+ }
706
+
707
+ // src/services.ts
708
+ import { z } from "zod";
709
+ var TOOL_SEPARATOR = "__";
710
+ function toMcpToolName(namespace, procedure) {
711
+ return `${namespace}${TOOL_SEPARATOR}${procedure}`;
712
+ }
713
+ function jsonSchemaToZodShape(schema) {
714
+ const properties = schema?.["properties"] ?? {};
715
+ const required = new Set(schema?.["required"] ?? []);
716
+ const shape = {};
717
+ for (const [key, prop] of Object.entries(properties)) {
718
+ let field;
719
+ switch (prop.type) {
720
+ case "number":
721
+ case "integer":
722
+ field = z.number();
723
+ break;
724
+ case "boolean":
725
+ field = z.boolean();
726
+ break;
727
+ case "array":
728
+ field = z.array(z.unknown());
729
+ break;
730
+ case "object":
731
+ field = z.record(z.unknown());
732
+ break;
733
+ default:
734
+ field = z.string();
735
+ break;
736
+ }
737
+ if (prop.description) {
738
+ field = field.describe(prop.description);
739
+ }
740
+ if (!required.has(key)) {
741
+ field = field.optional();
742
+ }
743
+ shape[key] = field;
744
+ }
745
+ return shape;
746
+ }
747
+ var ServiceBridge = class {
748
+ backend;
749
+ tools = /* @__PURE__ */ new Map();
750
+ constructor(config) {
751
+ this.backend = config.backend;
752
+ for (const tool of config.tools) {
753
+ this.tools.set(tool.name, tool);
754
+ }
755
+ }
756
+ registerTools(server) {
757
+ for (const [, info] of this.tools) {
758
+ const mcpToolName = toMcpToolName(info.namespace, info.procedure);
759
+ const inputShape = jsonSchemaToZodShape(info.parameters);
760
+ server.registerTool(
761
+ mcpToolName,
762
+ {
763
+ description: info.description ?? `Call ${info.namespace}.${info.procedure}`,
764
+ inputSchema: inputShape
765
+ },
766
+ async (args) => {
767
+ try {
768
+ const result = await this.backend.call(
769
+ info.namespace,
770
+ info.procedure,
771
+ [args ?? {}]
772
+ );
773
+ return {
774
+ content: [
775
+ {
776
+ type: "text",
777
+ text: typeof result === "string" ? result : JSON.stringify(result)
778
+ }
779
+ ]
780
+ };
781
+ } catch (error) {
782
+ const message = error instanceof Error ? error.message : String(error);
783
+ return {
784
+ content: [
785
+ {
786
+ type: "text",
787
+ text: `Service call failed: ${message}`
788
+ }
789
+ ],
790
+ isError: true
791
+ };
792
+ }
793
+ }
794
+ );
795
+ }
796
+ }
797
+ registerSearchServices(server) {
798
+ server.registerTool(
799
+ "search_services",
800
+ {
801
+ description: "Search for available service tools that widgets can call. Returns matching services with their namespaces, procedures, and parameter schemas.",
802
+ inputSchema: {
803
+ query: z.string().optional().describe(
804
+ 'Natural language description of what you want to do (e.g., "get weather forecast")'
805
+ ),
806
+ namespace: z.string().optional().describe(
807
+ 'Filter results to a specific service namespace (e.g., "weather")'
808
+ ),
809
+ tool_name: z.string().optional().describe("Get detailed info about a specific tool by name"),
810
+ limit: z.number().optional().describe("Maximum number of results to return")
811
+ }
812
+ },
813
+ async (args) => {
814
+ const query = args?.["query"];
815
+ const namespace = args?.["namespace"];
816
+ const toolName = args?.["tool_name"];
817
+ const limit = args?.["limit"] ?? 10;
818
+ if (toolName) {
819
+ const dotName = toolName.replace(/__/g, ".");
820
+ const info = this.tools.get(toolName) ?? this.tools.get(dotName);
821
+ if (!info) {
822
+ return {
823
+ content: [
824
+ {
825
+ type: "text",
826
+ text: JSON.stringify({
827
+ success: false,
828
+ error: `Tool '${toolName}' not found`
829
+ })
830
+ }
831
+ ]
832
+ };
833
+ }
834
+ return {
835
+ content: [
836
+ {
837
+ type: "text",
838
+ text: JSON.stringify({ success: true, tool: info })
839
+ }
840
+ ]
841
+ };
842
+ }
843
+ let results = Array.from(this.tools.values());
844
+ if (namespace) {
845
+ results = results.filter(
846
+ (info) => info.namespace === namespace
847
+ );
848
+ }
849
+ if (query) {
850
+ const queryLower = query.toLowerCase();
851
+ const keywords = queryLower.split(/\s+/).filter(Boolean);
852
+ results = results.map((info) => {
853
+ const searchText = `${info.name} ${info.namespace} ${info.procedure} ${info.description ?? ""}`.toLowerCase();
854
+ const matchCount = keywords.filter(
855
+ (kw) => searchText.includes(kw)
856
+ ).length;
857
+ return { info, score: matchCount / keywords.length };
858
+ }).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).map(({ info }) => info);
859
+ }
860
+ results = results.slice(0, limit);
861
+ return {
862
+ content: [
863
+ {
864
+ type: "text",
865
+ text: JSON.stringify({
866
+ success: true,
867
+ count: results.length,
868
+ tools: results,
869
+ namespaces: this.getNamespaces()
870
+ })
871
+ }
872
+ ]
873
+ };
874
+ }
875
+ );
876
+ }
877
+ getNamespaces() {
878
+ const namespaces = /* @__PURE__ */ new Set();
879
+ for (const info of this.tools.values()) {
880
+ namespaces.add(info.namespace);
881
+ }
882
+ return Array.from(namespaces);
883
+ }
884
+ getToolInfos() {
885
+ return Array.from(this.tools.values());
886
+ }
887
+ has(namespace, procedure) {
888
+ return this.tools.has(`${namespace}.${procedure}`);
889
+ }
890
+ };
891
+
892
+ // src/widget-store/store.ts
893
+ import { join as join3, resolve as resolve2 } from "path";
894
+
895
+ // src/widget-store/local-backend.ts
896
+ import { readFile as readFile2, writeFile as writeFile2, unlink, stat, mkdir as mkdir2, readdir, rm as rm2, access } from "fs/promises";
897
+ import { join as join2, dirname as dirname2, normalize } from "path";
898
+ function createDirEntry(name, isDir) {
899
+ return {
900
+ name,
901
+ isFile: () => !isDir,
902
+ isDirectory: () => isDir
903
+ };
904
+ }
905
+ function createFileStats(size2, mtime, isDir) {
906
+ return {
907
+ size: size2,
908
+ mtime,
909
+ isFile: () => !isDir,
910
+ isDirectory: () => isDir
911
+ };
912
+ }
913
+ var LocalFileBackend = class {
914
+ constructor(basePath) {
915
+ this.basePath = basePath;
916
+ }
917
+ basePath;
918
+ resolve(path) {
919
+ return join2(this.basePath, normalize(path));
920
+ }
921
+ async readFile(path) {
922
+ return readFile2(this.resolve(path), "utf-8");
923
+ }
924
+ async writeFile(path, content) {
925
+ const fullPath = this.resolve(path);
926
+ await mkdir2(dirname2(fullPath), { recursive: true });
927
+ await writeFile2(fullPath, content, "utf-8");
928
+ }
929
+ async unlink(path) {
930
+ await unlink(this.resolve(path));
931
+ }
932
+ async stat(path) {
933
+ const fullPath = this.resolve(path);
934
+ const s = await stat(fullPath);
935
+ return createFileStats(s.size, s.mtime, s.isDirectory());
936
+ }
937
+ async mkdir(path, options) {
938
+ await mkdir2(this.resolve(path), options);
939
+ }
940
+ async readdir(path) {
941
+ const fullPath = this.resolve(path);
942
+ const entries = await readdir(fullPath, { withFileTypes: true });
943
+ return entries.map(
944
+ (e) => createDirEntry(e.name, e.isDirectory())
945
+ );
946
+ }
947
+ async rmdir(path, options) {
948
+ if (options?.recursive) {
949
+ await rm2(this.resolve(path), { recursive: true, force: true });
950
+ } else {
951
+ await rm2(this.resolve(path), { force: true });
952
+ }
953
+ }
954
+ async exists(path) {
955
+ try {
956
+ await access(this.resolve(path));
957
+ return true;
958
+ } catch {
959
+ return false;
960
+ }
961
+ }
962
+ };
963
+
964
+ // src/widget-store/store.ts
965
+ var WIDGETS_PREFIX = "widgets";
966
+ var RESOURCE_URI_PREFIX = "ui://widgets/";
967
+ var DEFAULT_STORAGE_DIR = ".widget-store";
968
+ var WidgetStore = class {
969
+ provider;
970
+ storageDir;
971
+ constructor(options = {}) {
972
+ this.storageDir = resolve2(options.storageDir ?? DEFAULT_STORAGE_DIR);
973
+ this.provider = options.backend ?? new LocalFileBackend(this.storageDir);
974
+ }
975
+ fullPath(virtualPath) {
976
+ return join3(WIDGETS_PREFIX, virtualPath);
977
+ }
978
+ async saveWidget(hash, html, manifest, entry) {
979
+ const widgetDir = `${manifest.name}/${hash}`;
980
+ const htmlVirtualPath = this.fullPath(`${widgetDir}/view.html`);
981
+ const manifestVirtualPath = this.fullPath(`${widgetDir}/manifest.json`);
982
+ await this.provider.writeFile(htmlVirtualPath, html);
983
+ await this.provider.writeFile(
984
+ manifestVirtualPath,
985
+ JSON.stringify({
986
+ ...manifest,
987
+ hash,
988
+ entry,
989
+ createdAt: Date.now()
990
+ })
991
+ );
992
+ return {
993
+ path: htmlVirtualPath,
994
+ resourceUri: `${RESOURCE_URI_PREFIX}${widgetDir}/view.html`,
995
+ html,
996
+ manifest,
997
+ entry,
998
+ createdAt: Date.now()
999
+ };
1000
+ }
1001
+ async getWidget(name, hash) {
1002
+ const widgetDir = `${name}/${hash}`;
1003
+ const htmlVirtualPath = this.fullPath(`${widgetDir}/view.html`);
1004
+ const manifestVirtualPath = this.fullPath(`${widgetDir}/manifest.json`);
1005
+ const exists = await this.provider.exists(htmlVirtualPath);
1006
+ if (!exists) return null;
1007
+ const html = await this.provider.readFile(htmlVirtualPath);
1008
+ let manifest;
1009
+ let entry;
1010
+ let createdAt = Date.now();
1011
+ try {
1012
+ const raw = await this.provider.readFile(manifestVirtualPath);
1013
+ const parsed = JSON.parse(raw);
1014
+ manifest = {
1015
+ name: parsed["name"],
1016
+ version: parsed["version"],
1017
+ platform: parsed["platform"] ?? "browser",
1018
+ image: parsed["image"],
1019
+ description: parsed["description"],
1020
+ services: parsed["services"]
1021
+ };
1022
+ entry = parsed["entry"];
1023
+ createdAt = parsed["createdAt"] ?? Date.now();
1024
+ } catch {
1025
+ manifest = {
1026
+ name,
1027
+ version: "0.1.0",
1028
+ platform: "browser",
1029
+ image: "@aprovan/patchwork-image-shadcn"
1030
+ };
1031
+ }
1032
+ return {
1033
+ path: htmlVirtualPath,
1034
+ resourceUri: `${RESOURCE_URI_PREFIX}${widgetDir}/view.html`,
1035
+ html,
1036
+ manifest,
1037
+ entry,
1038
+ createdAt
1039
+ };
1040
+ }
1041
+ async listWidgets() {
1042
+ const results = [];
1043
+ const rootPath = this.fullPath("");
1044
+ try {
1045
+ const names = await this.provider.readdir(rootPath);
1046
+ for (const nameEntry of names) {
1047
+ if (!nameEntry.isDirectory()) continue;
1048
+ const name = nameEntry.name;
1049
+ try {
1050
+ const hashes = await this.provider.readdir(join3(rootPath, name));
1051
+ for (const hashEntry of hashes) {
1052
+ if (!hashEntry.isDirectory()) continue;
1053
+ const hash = hashEntry.name;
1054
+ const manifestVirtualPath = this.fullPath(`${name}/${hash}/manifest.json`);
1055
+ let manifest = {};
1056
+ try {
1057
+ const raw = await this.provider.readFile(manifestVirtualPath);
1058
+ manifest = JSON.parse(raw);
1059
+ } catch {
1060
+ continue;
1061
+ }
1062
+ results.push({
1063
+ path: this.fullPath(`${name}/${hash}/view.html`),
1064
+ resourceUri: `${RESOURCE_URI_PREFIX}${name}/${hash}/view.html`,
1065
+ name: manifest.name ?? name,
1066
+ version: manifest.version ?? "0.1.0",
1067
+ description: manifest.description,
1068
+ services: manifest.services,
1069
+ entry: manifest.entry,
1070
+ createdAt: manifest.createdAt ?? 0
1071
+ });
1072
+ }
1073
+ } catch {
1074
+ continue;
1075
+ }
1076
+ }
1077
+ } catch {
1078
+ return results;
1079
+ }
1080
+ return results.sort((a, b) => b.createdAt - a.createdAt);
1081
+ }
1082
+ async deleteWidget(name, hash) {
1083
+ const widgetDir = this.fullPath(`${name}/${hash}`);
1084
+ const exists = await this.provider.exists(widgetDir);
1085
+ if (!exists) return false;
1086
+ await this.provider.rmdir(widgetDir, { recursive: true });
1087
+ return true;
1088
+ }
1089
+ async hasWidget(name, hash) {
1090
+ return this.provider.exists(this.fullPath(`${name}/${hash}/view.html`));
1091
+ }
1092
+ resourceUriFor(name, hash) {
1093
+ return `${RESOURCE_URI_PREFIX}${name}/${hash}/view.html`;
1094
+ }
1095
+ async loadAll() {
1096
+ const infos = await this.listWidgets();
1097
+ const widgets = [];
1098
+ for (const info of infos) {
1099
+ const uriPath = info.resourceUri.replace(RESOURCE_URI_PREFIX, "").replace(/\/view\.html$/, "");
1100
+ const parts = uriPath.split("/");
1101
+ const name = parts[0];
1102
+ const hash = parts[1];
1103
+ if (!name || !hash) continue;
1104
+ const widget = await this.getWidget(name, hash);
1105
+ if (widget) widgets.push(widget);
1106
+ }
1107
+ return widgets;
1108
+ }
1109
+ };
1110
+ var _instance = null;
1111
+ function getWidgetStore(options) {
1112
+ if (!_instance) {
1113
+ _instance = new WidgetStore(options);
1114
+ }
1115
+ return _instance;
1116
+ }
1117
+ function resetWidgetStore() {
1118
+ _instance = null;
1119
+ }
1120
+
1121
+ // src/index.ts
1122
+ var HELLO_WORLD_RESOURCE_URI = "ui://hello-world/view.html";
1123
+ var MANIFEST_DEFAULTS = {
1124
+ name: "widget",
1125
+ version: "0.1.0",
1126
+ platform: "browser",
1127
+ image: "@aprovan/patchwork-image-shadcn"
1128
+ };
1129
+ function buildManifest(input) {
1130
+ return {
1131
+ name: input?.["name"] ?? MANIFEST_DEFAULTS.name,
1132
+ version: input?.["version"] ?? MANIFEST_DEFAULTS.version,
1133
+ platform: "browser",
1134
+ image: input?.["image"] ?? MANIFEST_DEFAULTS.image,
1135
+ services: input?.["services"]
1136
+ };
1137
+ }
1138
+ function buildVirtualProject(source, files, entry) {
1139
+ if (files && files.length > 0) {
1140
+ const virtualFiles = files.map((f) => ({
1141
+ path: f.path,
1142
+ content: f.content
1143
+ }));
1144
+ return createProjectFromFiles(virtualFiles, entry);
1145
+ }
1146
+ return source ?? "export default function Widget() { return <div>Hello Patchwork</div>; }";
1147
+ }
1148
+ async function registerStoredWidgetResources(server, store) {
1149
+ const widgets = await store.loadAll();
1150
+ for (const widget of widgets) {
1151
+ registerAppResource(
1152
+ server,
1153
+ `Widget ${widget.manifest.name}`,
1154
+ widget.resourceUri,
1155
+ {
1156
+ description: widget.manifest.description ?? `Persisted widget: ${widget.manifest.name}`
1157
+ },
1158
+ async () => ({
1159
+ contents: [
1160
+ {
1161
+ uri: widget.resourceUri,
1162
+ mimeType: RESOURCE_MIME_TYPE,
1163
+ text: widget.html
1164
+ }
1165
+ ]
1166
+ })
1167
+ );
1168
+ }
1169
+ }
1170
+ function registerCachedWidgetResources(server) {
1171
+ for (const [, entry] of allEntries()) {
1172
+ registerAppResource(
1173
+ server,
1174
+ `Widget ${entry.manifest.name}`,
1175
+ entry.resourceUri,
1176
+ {
1177
+ description: entry.manifest.description ?? `Compiled widget: ${entry.manifest.name}`
1178
+ },
1179
+ async () => ({
1180
+ contents: [
1181
+ {
1182
+ uri: entry.resourceUri,
1183
+ mimeType: RESOURCE_MIME_TYPE,
1184
+ text: entry.html
1185
+ }
1186
+ ]
1187
+ })
1188
+ );
1189
+ }
1190
+ }
1191
+ function createMcpAppServer(options = {}) {
1192
+ const server = new McpServer({
1193
+ name: "patchwork-mcp-app-server",
1194
+ version: "0.1.0"
1195
+ });
1196
+ const serviceBridge = options.services ? new ServiceBridge(options.services) : null;
1197
+ const store = getWidgetStore();
1198
+ registerAppTool(
1199
+ server,
1200
+ "hello_world",
1201
+ {
1202
+ description: "Display a hello-world widget inline in the conversation. Returns a static greeting card rendered as an MCP App.",
1203
+ _meta: { ui: { resourceUri: HELLO_WORLD_RESOURCE_URI } }
1204
+ },
1205
+ async () => ({
1206
+ content: [
1207
+ {
1208
+ type: "text",
1209
+ text: "Hello, world! The widget is rendered inline above."
1210
+ }
1211
+ ]
1212
+ })
1213
+ );
1214
+ registerAppResource(
1215
+ server,
1216
+ "Hello World View",
1217
+ HELLO_WORLD_RESOURCE_URI,
1218
+ {
1219
+ description: "Hello-world HTML widget for the Patchwork MCP App Server demo."
1220
+ },
1221
+ async () => ({
1222
+ contents: [
1223
+ {
1224
+ uri: HELLO_WORLD_RESOURCE_URI,
1225
+ mimeType: RESOURCE_MIME_TYPE,
1226
+ text: hello_world_default
1227
+ }
1228
+ ]
1229
+ })
1230
+ );
1231
+ registerAppTool(
1232
+ server,
1233
+ "compile_widget",
1234
+ {
1235
+ description: "Compile a JSX/TSX widget into a self-contained HTML page served as an MCP App resource. Pass source code for a single-file widget, or a files array for a multi-file project. The compiled widget is cached in memory and persisted to the VFS widget store.",
1236
+ inputSchema: {
1237
+ source: z2.string().optional().describe(
1238
+ "JSX/TSX source code for a single-file widget. Must export a default React component."
1239
+ ),
1240
+ files: z2.array(
1241
+ z2.object({
1242
+ path: z2.string().describe("File path relative to project root (e.g. 'main.tsx')"),
1243
+ content: z2.string().describe("File contents")
1244
+ })
1245
+ ).optional().describe(
1246
+ "Array of files for a multi-file widget project. At least one file should be the entry point (main.tsx or index.tsx)."
1247
+ ),
1248
+ entry: z2.string().optional().describe("Entry point file path. Defaults to auto-detection (main.tsx, index.tsx)."),
1249
+ name: z2.string().optional().describe("Widget name for the manifest. Defaults to 'widget'."),
1250
+ image: z2.string().optional().describe(
1251
+ "Patchwork image package to use. Defaults to '@aprovan/patchwork-image-shadcn'."
1252
+ ),
1253
+ services: z2.array(z2.string()).optional().describe(
1254
+ "Service namespaces the widget calls (e.g., ['weather', 'stripe']). A proxy shim is injected so widget code can call namespace.procedure(args) directly."
1255
+ )
1256
+ },
1257
+ _meta: {
1258
+ ui: { resourceUri: "ui://widget/{hash}/view.html" }
1259
+ }
1260
+ },
1261
+ async (args) => {
1262
+ const source = args?.["source"];
1263
+ const files = args?.["files"];
1264
+ const entry = args?.["entry"];
1265
+ const requestedServices = args?.["services"];
1266
+ const manifestInput = {};
1267
+ if (args?.["name"]) manifestInput["name"] = args["name"];
1268
+ if (args?.["image"]) manifestInput["image"] = args["image"];
1269
+ if (requestedServices) manifestInput["services"] = requestedServices;
1270
+ const manifest = buildManifest(manifestInput);
1271
+ const project = buildVirtualProject(source, files, entry);
1272
+ let compileServices;
1273
+ if (requestedServices && requestedServices.length > 0 && serviceBridge) {
1274
+ const availableNamespaces = serviceBridge.getNamespaces();
1275
+ compileServices = requestedServices.filter(
1276
+ (ns) => availableNamespaces.includes(ns)
1277
+ );
1278
+ const unavailable = requestedServices.filter(
1279
+ (ns) => !availableNamespaces.includes(ns)
1280
+ );
1281
+ if (unavailable.length > 0) {
1282
+ console.warn(
1283
+ `[mcp-app-server] Requested services not available: ${unavailable.join(", ")}. Available: ${availableNamespaces.join(", ")}`
1284
+ );
1285
+ }
1286
+ }
1287
+ try {
1288
+ const result = await compileWidget(
1289
+ project,
1290
+ manifest,
1291
+ compileServices ? { services: compileServices } : {}
1292
+ );
1293
+ const entryPath = typeof project === "string" ? void 0 : project.entry;
1294
+ await store.saveWidget(result.hash, result.html, manifest, entryPath);
1295
+ const storedUri = store.resourceUriFor(manifest.name, result.hash);
1296
+ registerAppResource(
1297
+ server,
1298
+ `Widget ${manifest.name}`,
1299
+ storedUri,
1300
+ {
1301
+ description: manifest.description ?? `Persisted widget: ${manifest.name}`
1302
+ },
1303
+ async () => ({
1304
+ contents: [
1305
+ {
1306
+ uri: storedUri,
1307
+ mimeType: RESOURCE_MIME_TYPE,
1308
+ text: result.html
1309
+ }
1310
+ ]
1311
+ })
1312
+ );
1313
+ registerAppResource(
1314
+ server,
1315
+ `Widget ${manifest.name} (cached)`,
1316
+ result.resourceUri,
1317
+ {
1318
+ description: `Compiled widget: ${manifest.name}`
1319
+ },
1320
+ async () => ({
1321
+ contents: [
1322
+ {
1323
+ uri: result.resourceUri,
1324
+ mimeType: RESOURCE_MIME_TYPE,
1325
+ text: result.html
1326
+ }
1327
+ ]
1328
+ })
1329
+ );
1330
+ return {
1331
+ content: [
1332
+ {
1333
+ type: "text",
1334
+ text: `Widget compiled and persisted. Resource URI: ${storedUri}`
1335
+ },
1336
+ {
1337
+ type: "text",
1338
+ text: `Cache key: ${result.hash}`
1339
+ }
1340
+ ],
1341
+ _meta: {
1342
+ ui: { resourceUri: result.resourceUri }
1343
+ }
1344
+ };
1345
+ } catch (error) {
1346
+ const message = error instanceof Error ? error.message : String(error);
1347
+ return {
1348
+ content: [
1349
+ {
1350
+ type: "text",
1351
+ text: `Compilation failed: ${message}`
1352
+ }
1353
+ ],
1354
+ isError: true
1355
+ };
1356
+ }
1357
+ }
1358
+ );
1359
+ if (serviceBridge) {
1360
+ serviceBridge.registerTools(server);
1361
+ serviceBridge.registerSearchServices(server);
1362
+ }
1363
+ registerAppTool(
1364
+ server,
1365
+ "list_widgets",
1366
+ {
1367
+ description: "List all persisted widgets in the VFS widget store. Returns each widget's name, version, description, path, and resource URI.",
1368
+ _meta: { ui: { resourceUri: "ui://widgets/list" } }
1369
+ },
1370
+ async () => {
1371
+ const widgets = await store.listWidgets();
1372
+ if (widgets.length === 0) {
1373
+ return {
1374
+ content: [
1375
+ {
1376
+ type: "text",
1377
+ text: "No widgets stored in the VFS widget store."
1378
+ }
1379
+ ]
1380
+ };
1381
+ }
1382
+ const lines = widgets.map((w) => {
1383
+ const parts = [`- **${w.name}** (v${w.version})`];
1384
+ if (w.description) parts.push(` ${w.description}`);
1385
+ parts.push(` Path: ${w.path}`);
1386
+ parts.push(` URI: ${w.resourceUri}`);
1387
+ if (w.entry) parts.push(` Entry: ${w.entry}`);
1388
+ if (w.services && w.services.length > 0) parts.push(` Services: ${w.services.join(", ")}`);
1389
+ return parts.join("\n");
1390
+ });
1391
+ return {
1392
+ content: [
1393
+ {
1394
+ type: "text",
1395
+ text: `Stored widgets (${widgets.length}):
1396
+
1397
+ ${lines.join("\n\n")}`
1398
+ }
1399
+ ]
1400
+ };
1401
+ }
1402
+ );
1403
+ registerAppTool(
1404
+ server,
1405
+ "render_widget",
1406
+ {
1407
+ description: "Render a persisted widget by its name and hash. Serves the compiled widget as an MCP App resource rendered inline in the conversation.",
1408
+ inputSchema: {
1409
+ name: z2.string().describe("Widget name (as stored in the VFS widget store)."),
1410
+ hash: z2.string().optional().describe("Widget content hash. If omitted, renders the most recent version of the named widget.")
1411
+ },
1412
+ _meta: {
1413
+ ui: { resourceUri: "ui://widgets/{name}/{hash}/view.html" }
1414
+ }
1415
+ },
1416
+ async (args) => {
1417
+ const name = args?.["name"];
1418
+ const hashInput = args?.["hash"];
1419
+ if (!name) {
1420
+ return {
1421
+ content: [
1422
+ {
1423
+ type: "text",
1424
+ text: "Widget name is required."
1425
+ }
1426
+ ],
1427
+ isError: true
1428
+ };
1429
+ }
1430
+ let hash = hashInput;
1431
+ if (!hash) {
1432
+ const widgets = await store.listWidgets();
1433
+ const match = widgets.find((w) => w.name === name);
1434
+ if (!match) {
1435
+ return {
1436
+ content: [
1437
+ {
1438
+ type: "text",
1439
+ text: `No stored widget found with name "${name}".`
1440
+ }
1441
+ ],
1442
+ isError: true
1443
+ };
1444
+ }
1445
+ const resourcePath = match.resourceUri.replace("ui://widgets/", "").replace("/view.html", "");
1446
+ const parts = resourcePath.split("/");
1447
+ hash = parts[1] ?? parts[0] ?? "";
1448
+ }
1449
+ if (!hash) {
1450
+ return {
1451
+ content: [
1452
+ {
1453
+ type: "text",
1454
+ text: `Could not determine hash for widget "${name}".`
1455
+ }
1456
+ ],
1457
+ isError: true
1458
+ };
1459
+ }
1460
+ const widget = await store.getWidget(name, hash);
1461
+ if (!widget) {
1462
+ return {
1463
+ content: [
1464
+ {
1465
+ type: "text",
1466
+ text: `Widget "${name}" with hash "${hash}" not found in the VFS store.`
1467
+ }
1468
+ ],
1469
+ isError: true
1470
+ };
1471
+ }
1472
+ registerAppResource(
1473
+ server,
1474
+ `Widget ${widget.manifest.name}`,
1475
+ widget.resourceUri,
1476
+ {
1477
+ description: widget.manifest.description ?? `Persisted widget: ${widget.manifest.name}`
1478
+ },
1479
+ async () => ({
1480
+ contents: [
1481
+ {
1482
+ uri: widget.resourceUri,
1483
+ mimeType: RESOURCE_MIME_TYPE,
1484
+ text: widget.html
1485
+ }
1486
+ ]
1487
+ })
1488
+ );
1489
+ return {
1490
+ content: [
1491
+ {
1492
+ type: "text",
1493
+ text: `Rendering widget "${name}" (hash: ${hash}).`
1494
+ }
1495
+ ],
1496
+ _meta: {
1497
+ ui: { resourceUri: widget.resourceUri }
1498
+ }
1499
+ };
1500
+ }
1501
+ );
1502
+ registerCachedWidgetResources(server);
1503
+ void registerStoredWidgetResources(server, store);
1504
+ registerLiveUpdateTools(server);
1505
+ return server;
1506
+ }
1507
+ function registerLiveUpdateTools(server) {
1508
+ server.registerTool(
1509
+ "subscribe_stream",
1510
+ {
1511
+ description: "Subscribe this widget session to a named data stream. The server will send `notifications/tools/list_changed` whenever new events arrive; the widget should then call `poll_updates` to fetch them. Returns the current sequence number so the widget knows where to start polling.",
1512
+ inputSchema: {
1513
+ stream: z2.string().describe("Name of the data stream to subscribe to."),
1514
+ session_id: z2.string().optional().describe(
1515
+ "MCP session ID. Widgets should pass the value returned in the Mcp-Session-Id response header during initialization."
1516
+ )
1517
+ }
1518
+ },
1519
+ (args, extra) => {
1520
+ const stream = args["stream"];
1521
+ const sessionId = args["session_id"] ?? extra["sessionId"];
1522
+ if (sessionId) {
1523
+ subscribeSession(sessionId, stream);
1524
+ }
1525
+ const seq = currentSeq();
1526
+ return {
1527
+ content: [
1528
+ {
1529
+ type: "text",
1530
+ text: JSON.stringify({ success: true, stream, seq })
1531
+ }
1532
+ ]
1533
+ };
1534
+ }
1535
+ );
1536
+ server.registerTool(
1537
+ "poll_updates",
1538
+ {
1539
+ description: "Fetch buffered events for a data stream that arrived after the given sequence number. Call this after receiving a `notifications/tools/list_changed` notification (which the server sends when new data is available). Pass the highest `seq` value from the last successful poll to avoid duplicates.",
1540
+ inputSchema: {
1541
+ stream: z2.string().describe("Name of the data stream to poll."),
1542
+ after_seq: z2.number().int().default(0).describe(
1543
+ "Return only events with seq > after_seq. Pass 0 to retrieve all buffered events."
1544
+ )
1545
+ }
1546
+ },
1547
+ (args) => {
1548
+ const stream = args["stream"];
1549
+ const afterSeq = args["after_seq"] ?? 0;
1550
+ const events = getEvents(stream, afterSeq);
1551
+ return {
1552
+ content: [
1553
+ {
1554
+ type: "text",
1555
+ text: JSON.stringify({ success: true, stream, events })
1556
+ }
1557
+ ]
1558
+ };
1559
+ }
1560
+ );
1561
+ server.registerTool(
1562
+ "push_update",
1563
+ {
1564
+ description: "Push a data update onto a named stream, broadcasting it to all subscribed widget sessions. Subscribing widgets will receive a `notifications/tools/list_changed` signal and then call `poll_updates` to retrieve the new data. Use this tool from server-side code or as an LLM tool to drive real-time widget updates.",
1565
+ inputSchema: {
1566
+ stream: z2.string().describe("Name of the data stream to push to."),
1567
+ data: z2.record(z2.unknown()).describe("Arbitrary JSON-serialisable payload to push to subscribers.")
1568
+ }
1569
+ },
1570
+ async (args) => {
1571
+ const stream = args["stream"];
1572
+ const data = args["data"];
1573
+ const seq = await pushStreamUpdate(stream, data);
1574
+ return {
1575
+ content: [
1576
+ {
1577
+ type: "text",
1578
+ text: JSON.stringify({ success: true, stream, seq })
1579
+ }
1580
+ ]
1581
+ };
1582
+ }
1583
+ );
1584
+ }
1585
+ export {
1586
+ WidgetStore,
1587
+ createMcpAppServer,
1588
+ getWidgetStore,
1589
+ pushStreamUpdate,
1590
+ resetWidgetStore
1591
+ };
1592
+ //# sourceMappingURL=index.js.map