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