@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.
- package/.turbo/turbo-build.log +20 -0
- package/E2E_TESTING.md +198 -0
- package/LICENSE +373 -0
- package/README.md +134 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.js +1592 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +1841 -0
- package/dist/server.js.map +1 -0
- package/package.json +52 -0
- package/src/__tests__/cache.test.ts +119 -0
- package/src/__tests__/cdn-plugin.test.ts +86 -0
- package/src/__tests__/compile.test.ts +93 -0
- package/src/__tests__/e2e-pipeline.test.ts +417 -0
- package/src/__tests__/live-update.test.ts +158 -0
- package/src/__tests__/local-backend.test.ts +100 -0
- package/src/__tests__/memory-backend.ts +144 -0
- package/src/__tests__/registry-backend.test.ts +256 -0
- package/src/__tests__/services.test.ts +153 -0
- package/src/__tests__/shim.test.ts +132 -0
- package/src/__tests__/widget-store.test.ts +183 -0
- package/src/compiler/cache.ts +68 -0
- package/src/compiler/cdn-plugin.ts +197 -0
- package/src/compiler/compile.ts +306 -0
- package/src/compiler/index.ts +3 -0
- package/src/hello-world.html +79 -0
- package/src/html.d.ts +4 -0
- package/src/index.ts +641 -0
- package/src/live-update.ts +149 -0
- package/src/reference-widgets/live-dashboard.ts +162 -0
- package/src/registry-backend.ts +304 -0
- package/src/server.ts +178 -0
- package/src/services.ts +251 -0
- package/src/shim.ts +247 -0
- package/src/widget-store/index.ts +10 -0
- package/src/widget-store/local-backend.ts +77 -0
- package/src/widget-store/store.ts +198 -0
- package/src/widget-store/types.ts +50 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +14 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, writeFile, rm, readFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
createCompiler,
|
|
7
|
+
loadImage,
|
|
8
|
+
type Manifest,
|
|
9
|
+
type VirtualProject,
|
|
10
|
+
type ImageConfig,
|
|
11
|
+
type LoadedImage,
|
|
12
|
+
} from "@aprovan/patchwork-compiler";
|
|
13
|
+
import react from "@vitejs/plugin-react";
|
|
14
|
+
import { build, type InlineConfig } from "vite";
|
|
15
|
+
import { viteSingleFile } from "vite-plugin-singlefile";
|
|
16
|
+
import { generateServiceShim, generateLiveUpdateShim } from "../shim.js";
|
|
17
|
+
import {
|
|
18
|
+
computeCacheKey,
|
|
19
|
+
set as cacheSet,
|
|
20
|
+
has as cacheHas,
|
|
21
|
+
get as cacheGet,
|
|
22
|
+
type CachedWidget,
|
|
23
|
+
} from "./cache.js";
|
|
24
|
+
import { patchworkCdnPlugin, getPreloadScripts } from "./cdn-plugin.js";
|
|
25
|
+
|
|
26
|
+
const WIDGET_RESOURCE_PREFIX = "ui://widget/";
|
|
27
|
+
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const COMPILE_TMP_DIR = resolve(__dirname, "..", ".compile-tmp");
|
|
30
|
+
|
|
31
|
+
const FALLBACK_IMAGE_CONFIG: ImageConfig = {
|
|
32
|
+
platform: "browser",
|
|
33
|
+
esbuild: { target: "es2020", format: "esm", jsx: "automatic" },
|
|
34
|
+
framework: {
|
|
35
|
+
globals: { react: "React", "react-dom": "ReactDOM" },
|
|
36
|
+
preload: [
|
|
37
|
+
"https://esm.sh/react@18",
|
|
38
|
+
"https://esm.sh/react-dom@18/client",
|
|
39
|
+
],
|
|
40
|
+
deps: { react: "18", "react-dom": "18" },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let loadedImage: LoadedImage | null = null;
|
|
45
|
+
let imageLoadPromise: Promise<LoadedImage> | null = null;
|
|
46
|
+
|
|
47
|
+
async function getImageConfig(): Promise<ImageConfig> {
|
|
48
|
+
if (loadedImage) return loadedImage.config;
|
|
49
|
+
if (imageLoadPromise) return imageLoadPromise.then((img) => img.config);
|
|
50
|
+
|
|
51
|
+
imageLoadPromise = (async () => {
|
|
52
|
+
try {
|
|
53
|
+
loadedImage = await loadImage("@aprovan/patchwork-image-shadcn");
|
|
54
|
+
return loadedImage;
|
|
55
|
+
} catch {
|
|
56
|
+
const compiler = await createCompiler({
|
|
57
|
+
image: "@aprovan/patchwork-image-shadcn",
|
|
58
|
+
proxyUrl: "",
|
|
59
|
+
});
|
|
60
|
+
await compiler.preloadImage("@aprovan/patchwork-image-shadcn");
|
|
61
|
+
const registry = (compiler as unknown as Record<string, unknown>)
|
|
62
|
+
.registry as Map<string, LoadedImage> | undefined;
|
|
63
|
+
const img = registry?.get("@aprovan/patchwork-image-shadcn");
|
|
64
|
+
if (img) {
|
|
65
|
+
loadedImage = img;
|
|
66
|
+
return img;
|
|
67
|
+
}
|
|
68
|
+
return { config: FALLBACK_IMAGE_CONFIG } as LoadedImage;
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
|
|
72
|
+
return imageLoadPromise.then((img) => img.config);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function generateHtmlEntry(preloads: string[], cssVars: string): string {
|
|
76
|
+
return `<!DOCTYPE html>
|
|
77
|
+
<html lang="en">
|
|
78
|
+
<head>
|
|
79
|
+
<meta charset="utf-8" />
|
|
80
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
81
|
+
${preloads.join("\n ")}
|
|
82
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
83
|
+
<script>
|
|
84
|
+
window.tailwind = window.tailwind || {};
|
|
85
|
+
window.tailwind.config = {
|
|
86
|
+
darkMode: "class",
|
|
87
|
+
theme: {
|
|
88
|
+
extend: {
|
|
89
|
+
colors: {
|
|
90
|
+
border: "hsl(var(--border))",
|
|
91
|
+
input: "hsl(var(--input))",
|
|
92
|
+
ring: "hsl(var(--ring))",
|
|
93
|
+
background: "hsl(var(--background))",
|
|
94
|
+
foreground: "hsl(var(--foreground))",
|
|
95
|
+
primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
|
|
96
|
+
secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
|
|
97
|
+
destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
|
|
98
|
+
muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
|
|
99
|
+
accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
|
|
100
|
+
popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))" },
|
|
101
|
+
card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
|
|
102
|
+
},
|
|
103
|
+
borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)" },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
</script>
|
|
108
|
+
<style>
|
|
109
|
+
:root {
|
|
110
|
+
${cssVars}
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
113
|
+
</head>
|
|
114
|
+
<body>
|
|
115
|
+
<div id="root"></div>
|
|
116
|
+
<script type="module" src="/src/_app.tsx"></script>
|
|
117
|
+
</body>
|
|
118
|
+
</html>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const SHADCN_CSS_VARS = `
|
|
122
|
+
--background: 0 0% 100%;
|
|
123
|
+
--foreground: 222.2 84% 4.9%;
|
|
124
|
+
--card: 0 0% 100%;
|
|
125
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
126
|
+
--popover: 0 0% 100%;
|
|
127
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
128
|
+
--primary: 222.2 47.4% 11.2%;
|
|
129
|
+
--primary-foreground: 210 40% 98%;
|
|
130
|
+
--secondary: 210 40% 96.1%;
|
|
131
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
132
|
+
--muted: 210 40% 96.1%;
|
|
133
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
134
|
+
--accent: 210 40% 96.1%;
|
|
135
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
136
|
+
--destructive: 0 84.2% 60.2%;
|
|
137
|
+
--destructive-foreground: 210 40% 98%;
|
|
138
|
+
--border: 214.3 31.8% 91.4%;
|
|
139
|
+
--input: 214.3 31.8% 91.4%;
|
|
140
|
+
--ring: 222.2 84% 4.9%;
|
|
141
|
+
--radius: 0.5rem;
|
|
142
|
+
`.trim();
|
|
143
|
+
|
|
144
|
+
function generateMainTsx(entryModule: string): string {
|
|
145
|
+
return `import React from 'react';
|
|
146
|
+
import { createRoot } from 'react-dom/client';
|
|
147
|
+
import Widget from './${entryModule}';
|
|
148
|
+
|
|
149
|
+
const rootEl = document.getElementById('root');
|
|
150
|
+
if (rootEl) {
|
|
151
|
+
const root = createRoot(rootEl);
|
|
152
|
+
root.render(React.createElement(Widget));
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function writeProjectFiles(
|
|
158
|
+
projectDir: string,
|
|
159
|
+
source: string | VirtualProject,
|
|
160
|
+
): Promise<string> {
|
|
161
|
+
const srcDir = join(projectDir, "src");
|
|
162
|
+
await mkdir(srcDir, { recursive: true });
|
|
163
|
+
|
|
164
|
+
if (typeof source === "string") {
|
|
165
|
+
await writeFile(join(srcDir, "widget.tsx"), source, "utf-8");
|
|
166
|
+
return "widget";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const [filePath, file] of source.files) {
|
|
170
|
+
const fullPath = filePath.startsWith("src/")
|
|
171
|
+
? join(projectDir, filePath)
|
|
172
|
+
: join(srcDir, filePath);
|
|
173
|
+
const dir = resolve(fullPath, "..");
|
|
174
|
+
await mkdir(dir, { recursive: true });
|
|
175
|
+
await writeFile(fullPath, file.content, "utf-8");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const entry = source.entry;
|
|
179
|
+
const entryName = entry.replace(/\.(tsx|ts|jsx|js)$/, "");
|
|
180
|
+
return entryName.startsWith("src/")
|
|
181
|
+
? entryName.slice(4)
|
|
182
|
+
: entryName;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function injectShimIntoHtml(html: string, shimScript: string): string {
|
|
186
|
+
const shimTag = `<script type="module">\n${shimScript}\n</script>`;
|
|
187
|
+
const bodyCloseIndex = html.lastIndexOf("</body>");
|
|
188
|
+
if (bodyCloseIndex === -1) return html;
|
|
189
|
+
return html.slice(0, bodyCloseIndex) + shimTag + "\n" + html.slice(bodyCloseIndex);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface CompileWidgetResult {
|
|
193
|
+
html: string;
|
|
194
|
+
hash: string;
|
|
195
|
+
resourceUri: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface CompileWidgetOptions {
|
|
199
|
+
services?: string[];
|
|
200
|
+
/** Inject the live-update shim (window.patchwork) into the compiled widget. Defaults to true. */
|
|
201
|
+
liveUpdates?: boolean;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function compileWidget(
|
|
205
|
+
source: string | VirtualProject,
|
|
206
|
+
manifest: Manifest,
|
|
207
|
+
options: CompileWidgetOptions = {},
|
|
208
|
+
): Promise<CompileWidgetResult> {
|
|
209
|
+
const liveUpdates = options.liveUpdates ?? true;
|
|
210
|
+
|
|
211
|
+
const baseCacheKey = computeCacheKey(source, manifest);
|
|
212
|
+
let cacheKey = baseCacheKey;
|
|
213
|
+
if (options.services?.length) {
|
|
214
|
+
cacheKey += `:svc:${options.services.sort().join(",")}`;
|
|
215
|
+
}
|
|
216
|
+
if (liveUpdates) {
|
|
217
|
+
cacheKey += `:live`;
|
|
218
|
+
}
|
|
219
|
+
if (cacheHas(cacheKey)) {
|
|
220
|
+
const cached = cacheGet(cacheKey)!;
|
|
221
|
+
return {
|
|
222
|
+
html: cached.html,
|
|
223
|
+
hash: cacheKey,
|
|
224
|
+
resourceUri: cached.resourceUri,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const imageConfig = await getImageConfig();
|
|
229
|
+
|
|
230
|
+
await mkdir(COMPILE_TMP_DIR, { recursive: true });
|
|
231
|
+
const projectDir = join(COMPILE_TMP_DIR, `build-${randomUUID()}`);
|
|
232
|
+
try {
|
|
233
|
+
await mkdir(projectDir, { recursive: true });
|
|
234
|
+
|
|
235
|
+
const entryModule = await writeProjectFiles(projectDir, source);
|
|
236
|
+
await writeFile(
|
|
237
|
+
join(projectDir, "src", "_app.tsx"),
|
|
238
|
+
generateMainTsx(entryModule),
|
|
239
|
+
"utf-8",
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const preloads = getPreloadScripts(imageConfig);
|
|
243
|
+
const htmlContent = generateHtmlEntry(preloads, SHADCN_CSS_VARS);
|
|
244
|
+
await writeFile(join(projectDir, "index.html"), htmlContent, "utf-8");
|
|
245
|
+
|
|
246
|
+
const viteConfig: InlineConfig = {
|
|
247
|
+
root: projectDir,
|
|
248
|
+
base: "./",
|
|
249
|
+
plugins: [
|
|
250
|
+
patchworkCdnPlugin({ imageConfig }),
|
|
251
|
+
react({
|
|
252
|
+
jsxRuntime: "classic",
|
|
253
|
+
}),
|
|
254
|
+
viteSingleFile({ useRecommendedBuildConfig: true }),
|
|
255
|
+
],
|
|
256
|
+
build: {
|
|
257
|
+
outDir: "dist",
|
|
258
|
+
emptyOutDir: true,
|
|
259
|
+
minify: false,
|
|
260
|
+
rollupOptions: {
|
|
261
|
+
input: resolve(projectDir, "index.html"),
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
logLevel: "silent",
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
await build(viteConfig);
|
|
268
|
+
|
|
269
|
+
const outputHtml = await readFile(
|
|
270
|
+
join(projectDir, "dist", "index.html"),
|
|
271
|
+
"utf-8",
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Build up the shim scripts. The live-update shim is injected first so
|
|
275
|
+
// __patchwork_app is available when the service shim runs. If both are
|
|
276
|
+
// present the live-update shim guards against double-initialisation.
|
|
277
|
+
const shimParts: string[] = [];
|
|
278
|
+
if (liveUpdates) {
|
|
279
|
+
shimParts.push(generateLiveUpdateShim());
|
|
280
|
+
}
|
|
281
|
+
if (options.services?.length) {
|
|
282
|
+
shimParts.push(generateServiceShim({ namespaces: options.services }));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let finalHtml = outputHtml;
|
|
286
|
+
for (const script of shimParts) {
|
|
287
|
+
finalHtml = injectShimIntoHtml(finalHtml, script);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const resourceUri = `${WIDGET_RESOURCE_PREFIX}${cacheKey}/view.html`;
|
|
291
|
+
|
|
292
|
+
const cacheEntry: CachedWidget = {
|
|
293
|
+
html: finalHtml,
|
|
294
|
+
manifest,
|
|
295
|
+
resourceUri,
|
|
296
|
+
createdAt: Date.now(),
|
|
297
|
+
};
|
|
298
|
+
cacheSet(cacheKey, cacheEntry);
|
|
299
|
+
|
|
300
|
+
return { html: finalHtml, hash: cacheKey, resourceUri };
|
|
301
|
+
} finally {
|
|
302
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export { cacheGet, cacheHas, computeCacheKey };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { compileWidget, cacheGet, cacheHas, type CompileWidgetResult, type CompileWidgetOptions } from "./compile.js";
|
|
2
|
+
export { getPreloadScripts, patchworkCdnPlugin, type CdnPluginOptions } from "./cdn-plugin.js";
|
|
3
|
+
export { computeCacheKey, get, set, has, clear, size, allEntries, type CachedWidget } from "./cache.js";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Hello World — Patchwork MCP App</title>
|
|
7
|
+
<style>
|
|
8
|
+
*,
|
|
9
|
+
*::before,
|
|
10
|
+
*::after {
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
margin: 0;
|
|
16
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
17
|
+
background: #f0f4ff;
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
min-height: 100vh;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.card {
|
|
25
|
+
background: #fff;
|
|
26
|
+
border-radius: 1rem;
|
|
27
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
|
28
|
+
padding: 2.5rem 3rem;
|
|
29
|
+
text-align: center;
|
|
30
|
+
max-width: 420px;
|
|
31
|
+
width: 100%;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.wave {
|
|
35
|
+
font-size: 3rem;
|
|
36
|
+
line-height: 1;
|
|
37
|
+
margin-bottom: 1rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
h1 {
|
|
41
|
+
margin: 0 0 0.75rem;
|
|
42
|
+
font-size: 1.75rem;
|
|
43
|
+
color: #111827;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
p {
|
|
47
|
+
margin: 0;
|
|
48
|
+
color: #6b7280;
|
|
49
|
+
font-size: 0.95rem;
|
|
50
|
+
line-height: 1.6;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.badge {
|
|
54
|
+
display: inline-block;
|
|
55
|
+
margin-top: 1.5rem;
|
|
56
|
+
background: #eff6ff;
|
|
57
|
+
color: #3b82f6;
|
|
58
|
+
font-size: 0.75rem;
|
|
59
|
+
font-weight: 600;
|
|
60
|
+
letter-spacing: 0.05em;
|
|
61
|
+
text-transform: uppercase;
|
|
62
|
+
padding: 0.25rem 0.75rem;
|
|
63
|
+
border-radius: 999px;
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
66
|
+
</head>
|
|
67
|
+
<body>
|
|
68
|
+
<div class="card">
|
|
69
|
+
<div class="wave">👋</div>
|
|
70
|
+
<h1>Hello, world!</h1>
|
|
71
|
+
<p>
|
|
72
|
+
This widget is served by the <strong>Patchwork MCP App Server</strong>.
|
|
73
|
+
It renders inline inside Claude Desktop and Claude web via a
|
|
74
|
+
<code>StreamableHTTPServerTransport</code>.
|
|
75
|
+
</p>
|
|
76
|
+
<span class="badge">MCP App</span>
|
|
77
|
+
</div>
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|
package/src/html.d.ts
ADDED