@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
@@ -0,0 +1,77 @@
1
+ import { readFile, writeFile, unlink, stat, mkdir, readdir, rm, access } from "node:fs/promises";
2
+ import { join, dirname, normalize } from "node:path";
3
+ import type { DirEntry, FileStats, FSProvider } from "./types.js";
4
+
5
+ function createDirEntry(name: string, isDir: boolean): DirEntry {
6
+ return {
7
+ name,
8
+ isFile: () => !isDir,
9
+ isDirectory: () => isDir,
10
+ };
11
+ }
12
+
13
+ function createFileStats(size: number, mtime: Date, isDir: boolean): FileStats {
14
+ return {
15
+ size,
16
+ mtime,
17
+ isFile: () => !isDir,
18
+ isDirectory: () => isDir,
19
+ };
20
+ }
21
+
22
+ export class LocalFileBackend implements FSProvider {
23
+ constructor(private basePath: string) {}
24
+
25
+ private resolve(path: string): string {
26
+ return join(this.basePath, normalize(path));
27
+ }
28
+
29
+ async readFile(path: string): Promise<string> {
30
+ return readFile(this.resolve(path), "utf-8");
31
+ }
32
+
33
+ async writeFile(path: string, content: string): Promise<void> {
34
+ const fullPath = this.resolve(path);
35
+ await mkdir(dirname(fullPath), { recursive: true });
36
+ await writeFile(fullPath, content, "utf-8");
37
+ }
38
+
39
+ async unlink(path: string): Promise<void> {
40
+ await unlink(this.resolve(path));
41
+ }
42
+
43
+ async stat(path: string): Promise<FileStats> {
44
+ const fullPath = this.resolve(path);
45
+ const s = await stat(fullPath);
46
+ return createFileStats(s.size, s.mtime, s.isDirectory());
47
+ }
48
+
49
+ async mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {
50
+ await mkdir(this.resolve(path), options);
51
+ }
52
+
53
+ async readdir(path: string): Promise<DirEntry[]> {
54
+ const fullPath = this.resolve(path);
55
+ const entries = await readdir(fullPath, { withFileTypes: true });
56
+ return entries.map((e) =>
57
+ createDirEntry(e.name, e.isDirectory()),
58
+ );
59
+ }
60
+
61
+ async rmdir(path: string, options?: { recursive?: boolean }): Promise<void> {
62
+ if (options?.recursive) {
63
+ await rm(this.resolve(path), { recursive: true, force: true });
64
+ } else {
65
+ await rm(this.resolve(path), { force: true });
66
+ }
67
+ }
68
+
69
+ async exists(path: string): Promise<boolean> {
70
+ try {
71
+ await access(this.resolve(path));
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,198 @@
1
+ import { join, resolve } from "node:path";
2
+ import { LocalFileBackend } from "./local-backend.js";
3
+ import type { FSProvider, StoredWidget, StoredWidgetInfo, WidgetStoreOptions } from "./types.js";
4
+ import type { Manifest } from "@aprovan/patchwork-compiler";
5
+
6
+ const WIDGETS_PREFIX = "widgets";
7
+ const RESOURCE_URI_PREFIX = "ui://widgets/";
8
+ const DEFAULT_STORAGE_DIR = ".widget-store";
9
+
10
+ export class WidgetStore {
11
+ private provider: FSProvider;
12
+ private storageDir: string;
13
+
14
+ constructor(options: WidgetStoreOptions = {}) {
15
+ this.storageDir = resolve(options.storageDir ?? DEFAULT_STORAGE_DIR);
16
+ this.provider = options.backend ?? new LocalFileBackend(this.storageDir);
17
+ }
18
+
19
+ private fullPath(virtualPath: string): string {
20
+ return join(WIDGETS_PREFIX, virtualPath);
21
+ }
22
+
23
+ async saveWidget(
24
+ hash: string,
25
+ html: string,
26
+ manifest: Manifest,
27
+ entry?: string,
28
+ ): Promise<StoredWidget> {
29
+ const widgetDir = `${manifest.name}/${hash}`;
30
+ const htmlVirtualPath = this.fullPath(`${widgetDir}/view.html`);
31
+ const manifestVirtualPath = this.fullPath(`${widgetDir}/manifest.json`);
32
+
33
+ await this.provider.writeFile(htmlVirtualPath, html);
34
+ await this.provider.writeFile(
35
+ manifestVirtualPath,
36
+ JSON.stringify({
37
+ ...manifest,
38
+ hash,
39
+ entry,
40
+ createdAt: Date.now(),
41
+ }),
42
+ );
43
+
44
+ return {
45
+ path: htmlVirtualPath,
46
+ resourceUri: `${RESOURCE_URI_PREFIX}${widgetDir}/view.html`,
47
+ html,
48
+ manifest,
49
+ entry,
50
+ createdAt: Date.now(),
51
+ };
52
+ }
53
+
54
+ async getWidget(name: string, hash: string): Promise<StoredWidget | null> {
55
+ const widgetDir = `${name}/${hash}`;
56
+ const htmlVirtualPath = this.fullPath(`${widgetDir}/view.html`);
57
+ const manifestVirtualPath = this.fullPath(`${widgetDir}/manifest.json`);
58
+
59
+ const exists = await this.provider.exists(htmlVirtualPath);
60
+ if (!exists) return null;
61
+
62
+ const html = await this.provider.readFile(htmlVirtualPath);
63
+ let manifest: Manifest;
64
+ let entry: string | undefined;
65
+ let createdAt = Date.now();
66
+
67
+ try {
68
+ const raw = await this.provider.readFile(manifestVirtualPath);
69
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
70
+ manifest = {
71
+ name: parsed["name"] as string,
72
+ version: parsed["version"] as string,
73
+ platform: (parsed["platform"] as "browser" | "cli") ?? "browser",
74
+ image: parsed["image"] as string,
75
+ description: parsed["description"] as string | undefined,
76
+ services: parsed["services"] as string[] | undefined,
77
+ };
78
+ entry = parsed["entry"] as string | undefined;
79
+ createdAt = (parsed["createdAt"] as number) ?? Date.now();
80
+ } catch {
81
+ manifest = {
82
+ name,
83
+ version: "0.1.0",
84
+ platform: "browser",
85
+ image: "@aprovan/patchwork-image-shadcn",
86
+ };
87
+ }
88
+
89
+ return {
90
+ path: htmlVirtualPath,
91
+ resourceUri: `${RESOURCE_URI_PREFIX}${widgetDir}/view.html`,
92
+ html,
93
+ manifest,
94
+ entry,
95
+ createdAt,
96
+ };
97
+ }
98
+
99
+ async listWidgets(): Promise<StoredWidgetInfo[]> {
100
+ const results: StoredWidgetInfo[] = [];
101
+ const rootPath = this.fullPath("");
102
+
103
+ try {
104
+ const names = await this.provider.readdir(rootPath);
105
+ for (const nameEntry of names) {
106
+ if (!nameEntry.isDirectory()) continue;
107
+ const name = nameEntry.name;
108
+
109
+ try {
110
+ const hashes = await this.provider.readdir(join(rootPath, name));
111
+ for (const hashEntry of hashes) {
112
+ if (!hashEntry.isDirectory()) continue;
113
+ const hash = hashEntry.name;
114
+ const manifestVirtualPath = this.fullPath(`${name}/${hash}/manifest.json`);
115
+
116
+ let manifest: Partial<Manifest> & { entry?: string; createdAt?: number; hash?: string } = {};
117
+ try {
118
+ const raw = await this.provider.readFile(manifestVirtualPath);
119
+ manifest = JSON.parse(raw) as Partial<Manifest> & {
120
+ entry?: string;
121
+ createdAt?: number;
122
+ hash?: string;
123
+ };
124
+ } catch {
125
+ continue;
126
+ }
127
+
128
+ results.push({
129
+ path: this.fullPath(`${name}/${hash}/view.html`),
130
+ resourceUri: `${RESOURCE_URI_PREFIX}${name}/${hash}/view.html`,
131
+ name: manifest.name ?? name,
132
+ version: manifest.version ?? "0.1.0",
133
+ description: manifest.description,
134
+ services: manifest.services,
135
+ entry: manifest.entry,
136
+ createdAt: manifest.createdAt ?? 0,
137
+ });
138
+ }
139
+ } catch {
140
+ continue;
141
+ }
142
+ }
143
+ } catch {
144
+ return results;
145
+ }
146
+
147
+ return results.sort((a, b) => b.createdAt - a.createdAt);
148
+ }
149
+
150
+ async deleteWidget(name: string, hash: string): Promise<boolean> {
151
+ const widgetDir = this.fullPath(`${name}/${hash}`);
152
+ const exists = await this.provider.exists(widgetDir);
153
+ if (!exists) return false;
154
+
155
+ await this.provider.rmdir(widgetDir, { recursive: true });
156
+ return true;
157
+ }
158
+
159
+ async hasWidget(name: string, hash: string): Promise<boolean> {
160
+ return this.provider.exists(this.fullPath(`${name}/${hash}/view.html`));
161
+ }
162
+
163
+ resourceUriFor(name: string, hash: string): string {
164
+ return `${RESOURCE_URI_PREFIX}${name}/${hash}/view.html`;
165
+ }
166
+
167
+ async loadAll(): Promise<StoredWidget[]> {
168
+ const infos = await this.listWidgets();
169
+ const widgets: StoredWidget[] = [];
170
+
171
+ for (const info of infos) {
172
+ const uriPath = info.resourceUri.replace(RESOURCE_URI_PREFIX, "").replace(/\/view\.html$/, "");
173
+ const parts = uriPath.split("/");
174
+ const name = parts[0];
175
+ const hash = parts[1];
176
+
177
+ if (!name || !hash) continue;
178
+
179
+ const widget = await this.getWidget(name, hash);
180
+ if (widget) widgets.push(widget);
181
+ }
182
+
183
+ return widgets;
184
+ }
185
+ }
186
+
187
+ let _instance: WidgetStore | null = null;
188
+
189
+ export function getWidgetStore(options?: WidgetStoreOptions): WidgetStore {
190
+ if (!_instance) {
191
+ _instance = new WidgetStore(options);
192
+ }
193
+ return _instance;
194
+ }
195
+
196
+ export function resetWidgetStore(): void {
197
+ _instance = null;
198
+ }
@@ -0,0 +1,50 @@
1
+ import type { Manifest } from "@aprovan/patchwork-compiler";
2
+
3
+ export interface FileStats {
4
+ size: number;
5
+ mtime: Date;
6
+ isFile(): boolean;
7
+ isDirectory(): boolean;
8
+ }
9
+
10
+ export interface DirEntry {
11
+ name: string;
12
+ isFile(): boolean;
13
+ isDirectory(): boolean;
14
+ }
15
+
16
+ export interface FSProvider {
17
+ readFile(path: string, encoding?: "utf8" | "base64"): Promise<string>;
18
+ writeFile(path: string, content: string): Promise<void>;
19
+ unlink(path: string): Promise<void>;
20
+ stat(path: string): Promise<FileStats>;
21
+ mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
22
+ readdir(path: string): Promise<DirEntry[]>;
23
+ rmdir(path: string, options?: { recursive?: boolean }): Promise<void>;
24
+ exists(path: string): Promise<boolean>;
25
+ }
26
+
27
+ export interface StoredWidget {
28
+ path: string;
29
+ resourceUri: string;
30
+ html: string;
31
+ manifest: Manifest;
32
+ entry?: string;
33
+ createdAt: number;
34
+ }
35
+
36
+ export interface StoredWidgetInfo {
37
+ path: string;
38
+ resourceUri: string;
39
+ name: string;
40
+ version: string;
41
+ description?: string;
42
+ services?: string[];
43
+ entry?: string;
44
+ createdAt: number;
45
+ }
46
+
47
+ export interface WidgetStoreOptions {
48
+ storageDir?: string;
49
+ backend?: FSProvider;
50
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "moduleResolution": "bundler",
7
+ "module": "ESNext"
8
+ },
9
+ "include": ["src/**/*"]
10
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts', 'src/server.ts'],
5
+ format: ['esm'],
6
+ target: 'node20',
7
+ clean: true,
8
+ dts: true,
9
+ splitting: false,
10
+ sourcemap: true,
11
+ shims: true,
12
+ skipNodeModulesBundle: true,
13
+ loader: { '.html': 'text' },
14
+ });