@excalimate/mcp-server 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/dist/checkpoint-store.d.ts +1 -0
- package/dist/checkpoint-store.d.ts.map +1 -1
- package/dist/checkpoint-store.js +23 -2
- package/dist/checkpoint-store.js.map +1 -1
- package/dist/httpServer.d.ts +4 -0
- package/dist/httpServer.d.ts.map +1 -0
- package/dist/httpServer.js +199 -0
- package/dist/httpServer.js.map +1 -0
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -199
- package/dist/index.js.map +1 -1
- package/dist/server/animationTools.d.ts +9 -0
- package/dist/server/animationTools.d.ts.map +1 -0
- package/dist/server/animationTools.js +254 -0
- package/dist/server/animationTools.js.map +1 -0
- package/dist/server/checkpointTools.d.ts +5 -0
- package/dist/server/checkpointTools.d.ts.map +1 -0
- package/dist/server/checkpointTools.js +22 -0
- package/dist/server/checkpointTools.js.map +1 -0
- package/dist/server/elementNormalizer.d.ts +3 -0
- package/dist/server/elementNormalizer.d.ts.map +1 -0
- package/dist/server/elementNormalizer.js +52 -0
- package/dist/server/elementNormalizer.js.map +1 -0
- package/dist/server/geometry.d.ts +24 -0
- package/dist/server/geometry.d.ts.map +1 -0
- package/dist/server/geometry.js +102 -0
- package/dist/server/geometry.js.map +1 -0
- package/dist/server/queryTools.d.ts +28 -0
- package/dist/server/queryTools.d.ts.map +1 -0
- package/dist/server/queryTools.js +107 -0
- package/dist/server/queryTools.js.map +1 -0
- package/dist/server/referenceText.d.ts +3 -0
- package/dist/server/referenceText.d.ts.map +1 -0
- package/dist/server/referenceText.js +268 -0
- package/dist/server/referenceText.js.map +1 -0
- package/dist/server/sceneTools.d.ts +4 -0
- package/dist/server/sceneTools.d.ts.map +1 -0
- package/dist/server/sceneTools.js +86 -0
- package/dist/server/sceneTools.js.map +1 -0
- package/dist/server/shareTools.d.ts +11 -0
- package/dist/server/shareTools.d.ts.map +1 -0
- package/dist/server/shareTools.js +81 -0
- package/dist/server/shareTools.js.map +1 -0
- package/dist/server/stateContext.d.ts +21 -0
- package/dist/server/stateContext.d.ts.map +1 -0
- package/dist/server/stateContext.js +85 -0
- package/dist/server/stateContext.js.map +1 -0
- package/dist/server.d.ts +5 -10
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +24 -907
- package/dist/server.js.map +1 -1
- package/dist/shareRoutes.d.ts +4 -0
- package/dist/shareRoutes.d.ts.map +1 -0
- package/dist/shareRoutes.js +44 -0
- package/dist/shareRoutes.js.map +1 -0
- package/dist/stdioServer.d.ts +3 -0
- package/dist/stdioServer.d.ts.map +1 -0
- package/dist/stdioServer.js +5 -0
- package/dist/stdioServer.js.map +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -14,8 +14,14 @@ The fastest way to use Excalimate with AI is to combine the deployed web app wit
|
|
|
14
14
|
npx @excalimate/mcp-server
|
|
15
15
|
# → Listening on http://localhost:3001/mcp
|
|
16
16
|
# → Live preview SSE at http://localhost:3001/live
|
|
17
|
+
|
|
18
|
+
# Custom port:
|
|
19
|
+
npx @excalimate/mcp-server --port 4000
|
|
17
20
|
```
|
|
18
21
|
|
|
22
|
+
Port priority: CLI arg (`--port` / `-p`) > `PORT` env var > default `3001`.
|
|
23
|
+
Also supports `--port=4000` syntax.
|
|
24
|
+
|
|
19
25
|
Or install globally:
|
|
20
26
|
|
|
21
27
|
```bash
|
|
@@ -27,6 +33,8 @@ excalimate-mcp
|
|
|
27
33
|
|
|
28
34
|
Go to [excalimate.com](https://excalimate.com) in your browser and click **📡 Live** in the toolbar. The app connects to `localhost:3001` automatically.
|
|
29
35
|
|
|
36
|
+
You can configure the MCP server URL in **File → Preferences**. The app persists this setting in localStorage, shows connection progress (progress bar + notifications), provides smart error dialogs, and warns about HTTPS→HTTP mixed-content connections.
|
|
37
|
+
|
|
30
38
|
### Step 3: Connect your AI
|
|
31
39
|
|
|
32
40
|
Point your AI tool to `http://localhost:3001/mcp` as an MCP server. Then ask it to create a diagram and animate it — you'll see elements appear and animate in your browser in real-time.
|
|
@@ -35,12 +43,13 @@ Point your AI tool to `http://localhost:3001/mcp` as an MCP server. Then ask it
|
|
|
35
43
|
|
|
36
44
|
## Features
|
|
37
45
|
|
|
38
|
-
- **
|
|
46
|
+
- **23 tools** for scene creation, animation, camera control, export, checkpointing, and sharing
|
|
39
47
|
- **Dual transport**: stdio (Claude Desktop) + Streamable HTTP (cloud deployment)
|
|
40
48
|
- **Live preview**: Real-time updates in [excalimate.com](https://excalimate.com) via SSE
|
|
41
49
|
- **Sequence reveal**: Staggered element reveal animations in one tool call
|
|
42
50
|
- **Camera animation**: Pan/zoom keyframes for cinematic effects
|
|
43
51
|
- **Checkpoint persistence**: Save/load complete scene + animation state
|
|
52
|
+
- **Runtime versioning**: Server version is read from `package.json`
|
|
44
53
|
|
|
45
54
|
## Installation & Usage
|
|
46
55
|
|
|
@@ -49,6 +58,9 @@ Point your AI tool to `http://localhost:3001/mcp` as an MCP server. Then ask it
|
|
|
49
58
|
```bash
|
|
50
59
|
npx @excalimate/mcp-server
|
|
51
60
|
# → http://localhost:3001/mcp
|
|
61
|
+
|
|
62
|
+
# Custom port:
|
|
63
|
+
npx @excalimate/mcp-server -p 4000
|
|
52
64
|
```
|
|
53
65
|
|
|
54
66
|
Open [excalimate.com](https://excalimate.com) and click **📡 Live**, then configure your AI tool below.
|
|
@@ -179,6 +191,7 @@ cd mcp-server
|
|
|
179
191
|
npm install
|
|
180
192
|
npm run build
|
|
181
193
|
node dist/index.js # HTTP mode
|
|
194
|
+
node dist/index.js --port 4000
|
|
182
195
|
node dist/index.js --stdio # stdio mode
|
|
183
196
|
```
|
|
184
197
|
|
|
@@ -223,6 +236,13 @@ node dist/index.js --stdio # stdio mode
|
|
|
223
236
|
| `items_visible_in_camera` | Report visibility of items at a given time |
|
|
224
237
|
| `animations_of_item` | Describe all animations of an element |
|
|
225
238
|
|
|
239
|
+
### Sharing Tools
|
|
240
|
+
| Tool | Description |
|
|
241
|
+
|------|-------------|
|
|
242
|
+
| `share_project` | Create E2E encrypted share URL for the current project |
|
|
243
|
+
|
|
244
|
+
`share_project` accepts `{ baseUrl?: string }` and defaults to `https://excalimate.com`, returning URLs like `https://excalimate.com/#share=ID,KEY`.
|
|
245
|
+
|
|
226
246
|
### Checkpoint Tools
|
|
227
247
|
| Tool | Description |
|
|
228
248
|
|------|-------------|
|
|
@@ -244,4 +264,5 @@ node dist/index.js --stdio # stdio mode
|
|
|
244
264
|
}
|
|
245
265
|
4. set_clip_range {0, 5000} → Set 5-second export window
|
|
246
266
|
5. save_checkpoint {id: "demo"} → Save for web app preview
|
|
267
|
+
or share_project {baseUrl: "https://excalimate.com"} → Generate E2E encrypted share URL
|
|
247
268
|
```
|
|
@@ -13,6 +13,7 @@ export declare class FileCheckpointStore implements CheckpointStore {
|
|
|
13
13
|
save(id: string, data: ServerState): Promise<void>;
|
|
14
14
|
load(id: string): Promise<ServerState | null>;
|
|
15
15
|
list(): Promise<string[]>;
|
|
16
|
+
private _pruning;
|
|
16
17
|
private prune;
|
|
17
18
|
}
|
|
18
19
|
export declare class MemoryCheckpointStore implements CheckpointStore {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkpoint-store.d.ts","sourceRoot":"","sources":["../src/checkpoint-store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAW9C,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC9C,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC3B;AAED,qBAAa,mBAAoB,YAAW,eAAe;IACzD,OAAO,CAAC,GAAG,CAAS;;IAOd,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAclD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAc7C,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"checkpoint-store.d.ts","sourceRoot":"","sources":["../src/checkpoint-store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAW9C,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC9C,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC3B;AAED,qBAAa,mBAAoB,YAAW,eAAe;IACzD,OAAO,CAAC,GAAG,CAAS;;IAOd,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAclD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAc7C,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAS/B,OAAO,CAAC,QAAQ,CAAS;YAEX,KAAK;CAiCpB;AAED,qBAAa,qBAAsB,YAAW,eAAe;IAC3D,OAAO,CAAC,KAAK,CAA6B;IAEpC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAWlD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAO7C,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;CAGhC"}
|
package/dist/checkpoint-store.js
CHANGED
|
@@ -53,7 +53,12 @@ export class FileCheckpointStore {
|
|
|
53
53
|
return [];
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
_pruning = false;
|
|
56
57
|
async prune() {
|
|
58
|
+
// Serialize prune operations to avoid concurrent filesystem races
|
|
59
|
+
if (this._pruning)
|
|
60
|
+
return;
|
|
61
|
+
this._pruning = true;
|
|
57
62
|
try {
|
|
58
63
|
const entries = await fs.promises.readdir(this.dir);
|
|
59
64
|
const jsonFiles = entries.filter(f => f.endsWith('.json'));
|
|
@@ -65,9 +70,25 @@ export class FileCheckpointStore {
|
|
|
65
70
|
})));
|
|
66
71
|
stats.sort((a, b) => a.mtime - b.mtime);
|
|
67
72
|
const toRemove = stats.slice(0, stats.length - MAX_CHECKPOINTS);
|
|
68
|
-
await Promise.all(toRemove.map(
|
|
73
|
+
await Promise.all(toRemove.map(async (f) => {
|
|
74
|
+
try {
|
|
75
|
+
await fs.promises.unlink(path.join(this.dir, f.name));
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const code = err.code;
|
|
79
|
+
// ENOENT is expected (concurrent delete) — log anything else
|
|
80
|
+
if (code !== 'ENOENT') {
|
|
81
|
+
console.warn(`[checkpoint] Failed to prune ${f.name}:`, err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.warn('[checkpoint] Prune scan failed:', err);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
this._pruning = false;
|
|
69
91
|
}
|
|
70
|
-
catch { /* best-effort */ }
|
|
71
92
|
}
|
|
72
93
|
}
|
|
73
94
|
export class MemoryCheckpointStore {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkpoint-store.js","sourceRoot":"","sources":["../src/checkpoint-store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AAGzB,MAAM,oBAAoB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,QAAQ;AACvD,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B,SAAS,UAAU,CAAC,EAAU;IAC5B,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;IAC3F,CAAC;AACH,CAAC;AAQD,MAAM,OAAO,mBAAmB;IACtB,GAAG,CAAS;IAEpB;QACE,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,4BAA4B,CAAC,CAAC;QAChE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAU,EAAE,IAAiB;QACtC,UAAU,CAAC,EAAE,CAAC,CAAC;QACf,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,UAAU,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;YAC7C,MAAM,IAAI,KAAK,CAAC,sBAAsB,oBAAoB,aAAa,CAAC,CAAC;QAC3E,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;QACnD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1E,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7C,CAAC;QACD,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAClD,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAU;QACnB,UAAU,CAAC,EAAE,CAAC,CAAC;QACf,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;QACnD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1E,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7C,CAAC;QACD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAC1D,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACzB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;QACnF,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,KAAK;QACjB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpD,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;YAC3D,IAAI,SAAS,CAAC,MAAM,IAAI,eAAe;gBAAE,OAAO;YAChD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAC7B,SAAS,CAAC,GAAG,CAAC,KAAK,EAAC,CAAC,EAAC,EAAE,CAAC,CAAC;gBACxB,IAAI,EAAE,CAAC;gBACP,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO;aAChE,CAAC,CAAC,CACJ,CAAC;YACF,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;YACxC,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,eAAe,CAAC,CAAC;YAChE,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"checkpoint-store.js","sourceRoot":"","sources":["../src/checkpoint-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { Response } from 'express';
|
|
3
|
+
export declare function startHTTPServer(factoryWithSSE: (sseClients: Set<Response>, broadcastSSE: (data: string) => void, port: number) => McpServer, portOverride?: number): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=httpServer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpServer.d.ts","sourceRoot":"","sources":["../src/httpServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAOzE,OAAO,KAAK,EAAW,QAAQ,EAAE,MAAM,SAAS,CAAC;AAwCjD,wBAAsB,eAAe,CACnC,cAAc,EAAE,CAAC,UAAU,EAAE,GAAG,CAAC,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAAE,IAAI,EAAE,MAAM,KAAK,SAAS,EAC5G,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CAoLf"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import helmet from 'helmet';
|
|
6
|
+
import rateLimit from 'express-rate-limit';
|
|
7
|
+
import { getSharedState } from './server.js';
|
|
8
|
+
import { registerShareRoutes } from './shareRoutes.js';
|
|
9
|
+
function getCorsOrigin() {
|
|
10
|
+
const raw = process.env.CORS_ORIGIN;
|
|
11
|
+
// No CORS origin configured: allow local dev + production domains
|
|
12
|
+
if (!raw || !raw.trim()) {
|
|
13
|
+
return ['http://localhost:5173', 'https://excalimate.com', 'https://www.excalimate.com'];
|
|
14
|
+
}
|
|
15
|
+
const origins = raw
|
|
16
|
+
.split(',')
|
|
17
|
+
.map((o) => o.trim())
|
|
18
|
+
.filter((o) => o.length > 0);
|
|
19
|
+
if (origins.length === 0) {
|
|
20
|
+
return ['http://localhost:5173', 'https://excalimate.com', 'https://www.excalimate.com'];
|
|
21
|
+
}
|
|
22
|
+
if (origins.length === 1) {
|
|
23
|
+
return origins[0];
|
|
24
|
+
}
|
|
25
|
+
// Multiple allowed origins: validate incoming origin against the list
|
|
26
|
+
return (origin, callback) => {
|
|
27
|
+
// Allow requests without an Origin header (e.g., same-origin or non-browser clients)
|
|
28
|
+
if (!origin) {
|
|
29
|
+
return callback(null, true);
|
|
30
|
+
}
|
|
31
|
+
if (origins.includes(origin)) {
|
|
32
|
+
return callback(null, true);
|
|
33
|
+
}
|
|
34
|
+
return callback(new Error('Not allowed by CORS'), false);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export async function startHTTPServer(factoryWithSSE, portOverride) {
|
|
38
|
+
const port = portOverride ?? parseInt(process.env.PORT ?? '3001', 10);
|
|
39
|
+
const app = express();
|
|
40
|
+
app.use(helmet()); // Security headers
|
|
41
|
+
app.use(cors({
|
|
42
|
+
origin: getCorsOrigin(),
|
|
43
|
+
}));
|
|
44
|
+
// Rate limiting
|
|
45
|
+
const limiter = rateLimit({
|
|
46
|
+
windowMs: 60 * 1000, // 1 minute
|
|
47
|
+
max: 200, // 200 requests per minute
|
|
48
|
+
standardHeaders: true,
|
|
49
|
+
legacyHeaders: false,
|
|
50
|
+
});
|
|
51
|
+
app.use(limiter);
|
|
52
|
+
// Stricter rate limit for share uploads
|
|
53
|
+
const shareLimiter = rateLimit({
|
|
54
|
+
windowMs: 60 * 1000,
|
|
55
|
+
max: 10, // 10 shares per minute
|
|
56
|
+
standardHeaders: true,
|
|
57
|
+
legacyHeaders: false,
|
|
58
|
+
});
|
|
59
|
+
const jsonMiddleware = express.json();
|
|
60
|
+
// Parse JSON only for non-share routes (share uses raw binary)
|
|
61
|
+
app.use((req, res, next) => {
|
|
62
|
+
if (req.path === '/share' && req.method === 'POST') {
|
|
63
|
+
next();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
jsonMiddleware(req, res, next);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// SSE clients for live state broadcasting
|
|
70
|
+
const sseClients = new Set();
|
|
71
|
+
/** Safely broadcast to all SSE clients, removing dead ones on failure. */
|
|
72
|
+
function broadcastSSE(data) {
|
|
73
|
+
for (const client of sseClients) {
|
|
74
|
+
try {
|
|
75
|
+
client.write(`data: ${data}\n\n`);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
sseClients.delete(client);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Factory that wires SSE broadcasting
|
|
83
|
+
const factory = () => factoryWithSSE(sseClients, broadcastSSE, port);
|
|
84
|
+
// Session map: keep server + transport alive across requests
|
|
85
|
+
const sessions = new Map();
|
|
86
|
+
// Periodically clean up stale sessions (no activity for 30 minutes)
|
|
87
|
+
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
88
|
+
const sessionCleanupTimer = setInterval(() => {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
for (const [sid, session] of sessions) {
|
|
91
|
+
if (now - session.lastActivity > SESSION_TTL_MS) {
|
|
92
|
+
sessions.delete(sid);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, 60 * 1000);
|
|
96
|
+
sessionCleanupTimer.unref();
|
|
97
|
+
app.all('/mcp', async (req, res) => {
|
|
98
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
99
|
+
// Route to existing session
|
|
100
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
101
|
+
const session = sessions.get(sessionId);
|
|
102
|
+
session.lastActivity = Date.now();
|
|
103
|
+
try {
|
|
104
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error('MCP session error:', error);
|
|
108
|
+
if (!res.headersSent) {
|
|
109
|
+
res.status(500).json({
|
|
110
|
+
jsonrpc: '2.0',
|
|
111
|
+
error: { code: -32603, message: 'Internal server error' },
|
|
112
|
+
id: null,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// New session
|
|
119
|
+
const server = factory();
|
|
120
|
+
const transport = new StreamableHTTPServerTransport({
|
|
121
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
122
|
+
});
|
|
123
|
+
transport.onclose = () => {
|
|
124
|
+
const sid = transport.sessionId;
|
|
125
|
+
if (sid)
|
|
126
|
+
sessions.delete(sid);
|
|
127
|
+
// Don't call server.close() here — it would call transport.close()
|
|
128
|
+
// again, causing infinite recursion. The SDK handles cleanup internally.
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
await server.connect(transport);
|
|
132
|
+
await transport.handleRequest(req, res, req.body);
|
|
133
|
+
// Store session after first successful request (session ID is now set)
|
|
134
|
+
const sid = transport.sessionId;
|
|
135
|
+
if (sid) {
|
|
136
|
+
sessions.set(sid, { server, transport, lastActivity: Date.now() });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.error('MCP error:', error);
|
|
141
|
+
if (!res.headersSent) {
|
|
142
|
+
res.status(500).json({
|
|
143
|
+
jsonrpc: '2.0',
|
|
144
|
+
error: { code: -32603, message: 'Internal server error' },
|
|
145
|
+
id: null,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
// SSE endpoint — web app connects here to receive live state updates
|
|
151
|
+
app.get('/live', (req, res) => {
|
|
152
|
+
res.writeHead(200, {
|
|
153
|
+
'Content-Type': 'text/event-stream',
|
|
154
|
+
'Cache-Control': 'no-cache',
|
|
155
|
+
Connection: 'keep-alive',
|
|
156
|
+
'Access-Control-Allow-Origin': '*',
|
|
157
|
+
});
|
|
158
|
+
res.write('data: {"type":"connected"}\n\n');
|
|
159
|
+
sseClients.add(res);
|
|
160
|
+
req.on('close', () => sseClients.delete(res));
|
|
161
|
+
});
|
|
162
|
+
// Current state endpoint
|
|
163
|
+
app.get('/state', (_req, res) => {
|
|
164
|
+
res.json(getSharedState());
|
|
165
|
+
});
|
|
166
|
+
// ── E2E Encrypted Sharing ──────────────────────────────────────
|
|
167
|
+
// The server only stores encrypted blobs. It never sees the encryption key.
|
|
168
|
+
registerShareRoutes(app, shareLimiter);
|
|
169
|
+
const httpServer = app.listen(port, () => {
|
|
170
|
+
console.log(`Excalimate MCP server listening on http://localhost:${port}/mcp`);
|
|
171
|
+
console.log(`Live preview SSE at http://localhost:${port}/live`);
|
|
172
|
+
});
|
|
173
|
+
let shuttingDown = false;
|
|
174
|
+
const shutdown = async () => {
|
|
175
|
+
if (shuttingDown)
|
|
176
|
+
return;
|
|
177
|
+
shuttingDown = true;
|
|
178
|
+
console.log('\nGraceful shutdown started...');
|
|
179
|
+
// 1. Stop accepting new connections
|
|
180
|
+
httpServer.close();
|
|
181
|
+
// 2. Stop the session cleanup timer
|
|
182
|
+
clearInterval(sessionCleanupTimer);
|
|
183
|
+
// 3. Close all SSE clients
|
|
184
|
+
for (const client of sseClients) {
|
|
185
|
+
try {
|
|
186
|
+
client.end();
|
|
187
|
+
}
|
|
188
|
+
catch { /* best-effort */ }
|
|
189
|
+
}
|
|
190
|
+
sseClients.clear();
|
|
191
|
+
// 4. Clear MCP sessions
|
|
192
|
+
sessions.clear();
|
|
193
|
+
console.log('Shutdown complete.');
|
|
194
|
+
process.exit(0);
|
|
195
|
+
};
|
|
196
|
+
process.on('SIGINT', shutdown);
|
|
197
|
+
process.on('SIGTERM', shutdown);
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=httpServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpServer.js","sourceRoot":"","sources":["../src/httpServer.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
CHANGED
|
@@ -1,201 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Excalimate MCP Server — Entry Point
|
|
4
|
-
*
|
|
5
|
-
* Supports two transports:
|
|
6
|
-
* --stdio : For Claude Desktop / Copilot CLI
|
|
7
|
-
* (default) : Streamable HTTP on port 3001
|
|
8
|
-
*/
|
|
9
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
11
|
-
import cors from 'cors';
|
|
12
|
-
import crypto from 'node:crypto';
|
|
13
|
-
import express from 'express';
|
|
14
|
-
import helmet from 'helmet';
|
|
15
|
-
import rateLimit from 'express-rate-limit';
|
|
16
2
|
import { FileCheckpointStore } from './checkpoint-store.js';
|
|
17
|
-
import { createServer
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
standardHeaders: true,
|
|
33
|
-
legacyHeaders: false,
|
|
34
|
-
});
|
|
35
|
-
app.use(limiter);
|
|
36
|
-
// Stricter rate limit for share uploads
|
|
37
|
-
const shareLimiter = rateLimit({
|
|
38
|
-
windowMs: 60 * 1000,
|
|
39
|
-
max: 10, // 10 shares per minute
|
|
40
|
-
standardHeaders: true,
|
|
41
|
-
legacyHeaders: false,
|
|
42
|
-
});
|
|
43
|
-
const jsonMiddleware = express.json();
|
|
44
|
-
// Parse JSON only for non-share routes (share uses raw binary)
|
|
45
|
-
app.use((req, res, next) => {
|
|
46
|
-
if (req.path === '/share' && req.method === 'POST') {
|
|
47
|
-
next();
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
jsonMiddleware(req, res, next);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
// SSE clients for live state broadcasting
|
|
54
|
-
const sseClients = new Set();
|
|
55
|
-
/** Safely broadcast to all SSE clients, removing dead ones on failure. */
|
|
56
|
-
function broadcastSSE(data) {
|
|
57
|
-
for (const client of sseClients) {
|
|
58
|
-
try {
|
|
59
|
-
client.write(`data: ${data}\n\n`);
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
sseClients.delete(client);
|
|
63
|
-
}
|
|
3
|
+
import { createServer } from './server.js';
|
|
4
|
+
import { startStdioServer } from './stdioServer.js';
|
|
5
|
+
import { startHTTPServer } from './httpServer.js';
|
|
6
|
+
/** Parse --port=NNNN or --port NNNN from argv */
|
|
7
|
+
function parsePort() {
|
|
8
|
+
for (let i = 0; i < process.argv.length; i++) {
|
|
9
|
+
const arg = process.argv[i];
|
|
10
|
+
if (arg.startsWith('--port=')) {
|
|
11
|
+
return parseInt(arg.slice('--port='.length), 10);
|
|
12
|
+
}
|
|
13
|
+
if (arg === '--port' && process.argv[i + 1]) {
|
|
14
|
+
return parseInt(process.argv[i + 1], 10);
|
|
15
|
+
}
|
|
16
|
+
if (arg === '-p' && process.argv[i + 1]) {
|
|
17
|
+
return parseInt(process.argv[i + 1], 10);
|
|
64
18
|
}
|
|
65
19
|
}
|
|
66
|
-
|
|
67
|
-
const factory = () => factoryWithSSE(sseClients, broadcastSSE);
|
|
68
|
-
// Session map: keep server + transport alive across requests
|
|
69
|
-
const sessions = new Map();
|
|
70
|
-
// Periodically clean up stale sessions (no activity for 30 minutes)
|
|
71
|
-
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
72
|
-
const sessionCleanupTimer = setInterval(() => {
|
|
73
|
-
const now = Date.now();
|
|
74
|
-
for (const [sid, session] of sessions) {
|
|
75
|
-
if (now - session.lastActivity > SESSION_TTL_MS) {
|
|
76
|
-
sessions.delete(sid);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}, 60 * 1000);
|
|
80
|
-
sessionCleanupTimer.unref();
|
|
81
|
-
app.all('/mcp', async (req, res) => {
|
|
82
|
-
const sessionId = req.headers['mcp-session-id'];
|
|
83
|
-
// Route to existing session
|
|
84
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
85
|
-
const session = sessions.get(sessionId);
|
|
86
|
-
session.lastActivity = Date.now();
|
|
87
|
-
try {
|
|
88
|
-
await session.transport.handleRequest(req, res, req.body);
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
console.error('MCP session error:', error);
|
|
92
|
-
if (!res.headersSent) {
|
|
93
|
-
res.status(500).json({
|
|
94
|
-
jsonrpc: '2.0',
|
|
95
|
-
error: { code: -32603, message: 'Internal server error' },
|
|
96
|
-
id: null,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
// New session
|
|
103
|
-
const server = factory();
|
|
104
|
-
const transport = new StreamableHTTPServerTransport({
|
|
105
|
-
sessionIdGenerator: () => crypto.randomUUID(),
|
|
106
|
-
});
|
|
107
|
-
transport.onclose = () => {
|
|
108
|
-
const sid = transport.sessionId;
|
|
109
|
-
if (sid)
|
|
110
|
-
sessions.delete(sid);
|
|
111
|
-
// Don't call server.close() here — it would call transport.close()
|
|
112
|
-
// again, causing infinite recursion. The SDK handles cleanup internally.
|
|
113
|
-
};
|
|
114
|
-
try {
|
|
115
|
-
await server.connect(transport);
|
|
116
|
-
await transport.handleRequest(req, res, req.body);
|
|
117
|
-
// Store session after first successful request (session ID is now set)
|
|
118
|
-
const sid = transport.sessionId;
|
|
119
|
-
if (sid) {
|
|
120
|
-
sessions.set(sid, { server, transport, lastActivity: Date.now() });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
console.error('MCP error:', error);
|
|
125
|
-
if (!res.headersSent) {
|
|
126
|
-
res.status(500).json({
|
|
127
|
-
jsonrpc: '2.0',
|
|
128
|
-
error: { code: -32603, message: 'Internal server error' },
|
|
129
|
-
id: null,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
// SSE endpoint — web app connects here to receive live state updates
|
|
135
|
-
app.get('/live', (req, res) => {
|
|
136
|
-
res.writeHead(200, {
|
|
137
|
-
'Content-Type': 'text/event-stream',
|
|
138
|
-
'Cache-Control': 'no-cache',
|
|
139
|
-
Connection: 'keep-alive',
|
|
140
|
-
'Access-Control-Allow-Origin': '*',
|
|
141
|
-
});
|
|
142
|
-
res.write('data: {"type":"connected"}\n\n');
|
|
143
|
-
sseClients.add(res);
|
|
144
|
-
req.on('close', () => sseClients.delete(res));
|
|
145
|
-
});
|
|
146
|
-
// Current state endpoint
|
|
147
|
-
app.get('/state', (_req, res) => {
|
|
148
|
-
res.json(getSharedState());
|
|
149
|
-
});
|
|
150
|
-
// ── E2E Encrypted Sharing ──────────────────────────────────────
|
|
151
|
-
// The server only stores encrypted blobs. It never sees the encryption key.
|
|
152
|
-
const shareStore = new Map();
|
|
153
|
-
const MAX_SHARE_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
154
|
-
const MAX_SHARES = 500;
|
|
155
|
-
// Upload encrypted blob
|
|
156
|
-
app.post('/share', shareLimiter, express.raw({ type: 'application/octet-stream', limit: '10mb' }), (req, res) => {
|
|
157
|
-
const id = crypto.randomUUID().replace(/-/g, '').slice(0, 16);
|
|
158
|
-
const body = req.body;
|
|
159
|
-
if (!body || body.length === 0) {
|
|
160
|
-
res.status(400).json({ error: 'Empty body. Send as application/octet-stream.' });
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (body.length > MAX_SHARE_SIZE) {
|
|
164
|
-
res.status(413).json({ error: 'Payload too large' });
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
// Evict oldest if over limit
|
|
168
|
-
if (shareStore.size >= MAX_SHARES) {
|
|
169
|
-
const oldest = shareStore.keys().next().value;
|
|
170
|
-
if (oldest !== undefined)
|
|
171
|
-
shareStore.delete(oldest);
|
|
172
|
-
}
|
|
173
|
-
shareStore.set(id, body);
|
|
174
|
-
res.json({ id, url: `/share/${id}` });
|
|
175
|
-
});
|
|
176
|
-
// Download encrypted blob
|
|
177
|
-
app.get('/share/:id', (req, res) => {
|
|
178
|
-
const data = shareStore.get(req.params.id);
|
|
179
|
-
if (!data) {
|
|
180
|
-
res.status(404).json({ error: 'Not found' });
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
res.set('Content-Type', 'application/octet-stream');
|
|
184
|
-
res.send(data);
|
|
185
|
-
});
|
|
186
|
-
app.listen(port, () => {
|
|
187
|
-
console.log(`Excalimate MCP server listening on http://localhost:${port}/mcp`);
|
|
188
|
-
console.log(`Live preview SSE at http://localhost:${port}/live`);
|
|
189
|
-
});
|
|
190
|
-
const shutdown = () => {
|
|
191
|
-
console.log('\nShutting down...');
|
|
192
|
-
process.exit(0);
|
|
193
|
-
};
|
|
194
|
-
process.on('SIGINT', shutdown);
|
|
195
|
-
process.on('SIGTERM', shutdown);
|
|
20
|
+
return undefined;
|
|
196
21
|
}
|
|
197
22
|
async function main() {
|
|
198
|
-
// Prevent cascading unhandled rejections from crashing the process
|
|
199
23
|
process.on('unhandledRejection', (reason) => {
|
|
200
24
|
console.error('Unhandled rejection:', reason);
|
|
201
25
|
});
|
|
@@ -203,22 +27,22 @@ async function main() {
|
|
|
203
27
|
console.error('Uncaught exception:', err);
|
|
204
28
|
});
|
|
205
29
|
const store = new FileCheckpointStore();
|
|
30
|
+
const cliPort = parsePort();
|
|
206
31
|
if (process.argv.includes('--stdio')) {
|
|
207
|
-
|
|
208
|
-
await startStdioServer(factory);
|
|
32
|
+
await startStdioServer(() => createServer(store));
|
|
209
33
|
}
|
|
210
34
|
else {
|
|
211
|
-
await startHTTPServer((_sseClients, broadcastSSE) => {
|
|
212
|
-
return createServer(store, (
|
|
35
|
+
await startHTTPServer((_sseClients, broadcastSSE, port) => {
|
|
36
|
+
return createServer(store, (delta) => {
|
|
213
37
|
try {
|
|
214
|
-
const data = JSON.stringify({ type: 'state', state });
|
|
38
|
+
const data = JSON.stringify({ type: 'state', state: delta });
|
|
215
39
|
broadcastSSE(data);
|
|
216
40
|
}
|
|
217
41
|
catch (err) {
|
|
218
42
|
console.error('Failed to broadcast state:', err);
|
|
219
43
|
}
|
|
220
|
-
});
|
|
221
|
-
});
|
|
44
|
+
}, port);
|
|
45
|
+
}, cliPort);
|
|
222
46
|
}
|
|
223
47
|
}
|
|
224
48
|
main().catch((e) => {
|