@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.
Files changed (62) hide show
  1. package/README.md +22 -1
  2. package/dist/checkpoint-store.d.ts +1 -0
  3. package/dist/checkpoint-store.d.ts.map +1 -1
  4. package/dist/checkpoint-store.js +23 -2
  5. package/dist/checkpoint-store.js.map +1 -1
  6. package/dist/httpServer.d.ts +4 -0
  7. package/dist/httpServer.d.ts.map +1 -0
  8. package/dist/httpServer.js +199 -0
  9. package/dist/httpServer.js.map +1 -0
  10. package/dist/index.d.ts +0 -7
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +23 -199
  13. package/dist/index.js.map +1 -1
  14. package/dist/server/animationTools.d.ts +9 -0
  15. package/dist/server/animationTools.d.ts.map +1 -0
  16. package/dist/server/animationTools.js +254 -0
  17. package/dist/server/animationTools.js.map +1 -0
  18. package/dist/server/checkpointTools.d.ts +5 -0
  19. package/dist/server/checkpointTools.d.ts.map +1 -0
  20. package/dist/server/checkpointTools.js +22 -0
  21. package/dist/server/checkpointTools.js.map +1 -0
  22. package/dist/server/elementNormalizer.d.ts +3 -0
  23. package/dist/server/elementNormalizer.d.ts.map +1 -0
  24. package/dist/server/elementNormalizer.js +52 -0
  25. package/dist/server/elementNormalizer.js.map +1 -0
  26. package/dist/server/geometry.d.ts +24 -0
  27. package/dist/server/geometry.d.ts.map +1 -0
  28. package/dist/server/geometry.js +102 -0
  29. package/dist/server/geometry.js.map +1 -0
  30. package/dist/server/queryTools.d.ts +28 -0
  31. package/dist/server/queryTools.d.ts.map +1 -0
  32. package/dist/server/queryTools.js +107 -0
  33. package/dist/server/queryTools.js.map +1 -0
  34. package/dist/server/referenceText.d.ts +3 -0
  35. package/dist/server/referenceText.d.ts.map +1 -0
  36. package/dist/server/referenceText.js +268 -0
  37. package/dist/server/referenceText.js.map +1 -0
  38. package/dist/server/sceneTools.d.ts +4 -0
  39. package/dist/server/sceneTools.d.ts.map +1 -0
  40. package/dist/server/sceneTools.js +86 -0
  41. package/dist/server/sceneTools.js.map +1 -0
  42. package/dist/server/shareTools.d.ts +11 -0
  43. package/dist/server/shareTools.d.ts.map +1 -0
  44. package/dist/server/shareTools.js +81 -0
  45. package/dist/server/shareTools.js.map +1 -0
  46. package/dist/server/stateContext.d.ts +21 -0
  47. package/dist/server/stateContext.d.ts.map +1 -0
  48. package/dist/server/stateContext.js +85 -0
  49. package/dist/server/stateContext.js.map +1 -0
  50. package/dist/server.d.ts +5 -10
  51. package/dist/server.d.ts.map +1 -1
  52. package/dist/server.js +24 -907
  53. package/dist/server.js.map +1 -1
  54. package/dist/shareRoutes.d.ts +4 -0
  55. package/dist/shareRoutes.d.ts.map +1 -0
  56. package/dist/shareRoutes.js +44 -0
  57. package/dist/shareRoutes.js.map +1 -0
  58. package/dist/stdioServer.d.ts +3 -0
  59. package/dist/stdioServer.d.ts.map +1 -0
  60. package/dist/stdioServer.js +5 -0
  61. package/dist/stdioServer.js.map +1 -0
  62. 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
- - **22 tools** for scene creation, animation, camera control, export, and checkpointing
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;YASjB,KAAK;CAgBpB;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"}
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"}
@@ -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(f => fs.promises.unlink(path.join(this.dir, f.name)).catch(() => { })));
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":"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"}
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":"AACA,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AACnG,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD,SAAS,aAAa;IACpB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IAEpC,kEAAkE;IAClE,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;QACxB,OAAO,CAAC,uBAAuB,EAAE,wBAAwB,EAAE,4BAA4B,CAAC,CAAC;IAC3F,CAAC;IAED,MAAM,OAAO,GAAG,GAAG;SAChB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAE/B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,uBAAuB,EAAE,wBAAwB,EAAE,4BAA4B,CAAC,CAAC;IAC3F,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,sEAAsE;IACtE,OAAO,CAAC,MAA0B,EAAE,QAAsD,EAAE,EAAE;QAC5F,qFAAqF;QACrF,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC9B,CAAC;QAED,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7B,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC9B,CAAC;QAED,OAAO,QAAQ,CAAC,IAAI,KAAK,CAAC,qBAAqB,CAAC,EAAE,KAAK,CAAC,CAAC;IAC3D,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,cAA4G,EAC5G,YAAqB;IAErB,MAAM,IAAI,GAAG,YAAY,IAAI,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;IACtE,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,mBAAmB;IACtC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;QACX,MAAM,EAAE,aAAa,EAAE;KACxB,CAAC,CAAC,CAAC;IAEJ,gBAAgB;IAChB,MAAM,OAAO,GAAG,SAAS,CAAC;QACxB,QAAQ,EAAE,EAAE,GAAG,IAAI,EAAE,WAAW;QAChC,GAAG,EAAE,GAAG,EAAE,0BAA0B;QACpC,eAAe,EAAE,IAAI;QACrB,aAAa,EAAE,KAAK;KACrB,CAAC,CAAC;IACH,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAEjB,wCAAwC;IACxC,MAAM,YAAY,GAAG,SAAS,CAAC;QAC7B,QAAQ,EAAE,EAAE,GAAG,IAAI;QACnB,GAAG,EAAE,EAAE,EAAE,uBAAuB;QAChC,eAAe,EAAE,IAAI;QACrB,aAAa,EAAE,KAAK;KACrB,CAAC,CAAC;IAEH,MAAM,cAAc,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAEtC,+DAA+D;IAC/D,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YACnD,IAAI,EAAE,CAAC;QACT,CAAC;aAAM,CAAC;YACN,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QACjC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,0CAA0C;IAC1C,MAAM,UAAU,GAAG,IAAI,GAAG,EAAY,CAAC;IAEvC,0EAA0E;IAC1E,SAAS,YAAY,CAAC,IAAY;QAChC,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;YAChC,IAAI,CAAC;gBACH,MAAM,CAAC,KAAK,CAAC,SAAS,IAAI,MAAM,CAAC,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;IACH,CAAC;IAED,sCAAsC;IACtC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,cAAc,CAAC,UAAU,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;IAErE,6DAA6D;IAC7D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAGrB,CAAC;IAEJ,oEAAoE;IACpE,MAAM,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IACtC,MAAM,mBAAmB,GAAG,WAAW,CAAC,GAAG,EAAE;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,QAAQ,EAAE,CAAC;YACtC,IAAI,GAAG,GAAG,OAAO,CAAC,YAAY,GAAG,cAAc,EAAE,CAAC;gBAChD,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;IACH,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,CAAC;IACd,mBAAmB,CAAC,KAAK,EAAE,CAAC;IAE5B,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;QACpD,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAuB,CAAC;QAEtE,4BAA4B;QAC5B,IAAI,SAAS,IAAI,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAE,CAAC;YACzC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAClC,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;YAC5D,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,KAAK,CAAC,CAAC;gBAC3C,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;oBACrB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;wBACnB,OAAO,EAAE,KAAK;wBACd,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE;wBACzD,EAAE,EAAE,IAAI;qBACT,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;YACD,OAAO;QACT,CAAC;QAED,cAAc;QACd,MAAM,MAAM,GAAG,OAAO,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,IAAI,6BAA6B,CAAC;YAClD,kBAAkB,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE;SAC9C,CAAC,CAAC;QAEH,SAAS,CAAC,OAAO,GAAG,GAAG,EAAE;YACvB,MAAM,GAAG,GAAG,SAAS,CAAC,SAAS,CAAC;YAChC,IAAI,GAAG;gBAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC9B,mEAAmE;YACnE,yEAAyE;QAC3E,CAAC,CAAC;QAEF,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAChC,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;YAElD,uEAAuE;YACvE,MAAM,GAAG,GAAG,SAAS,CAAC,SAAS,CAAC;YAChC,IAAI,GAAG,EAAE,CAAC;gBACR,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACrE,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;YACnC,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;gBACrB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBACnB,OAAO,EAAE,KAAK;oBACd,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE;oBACzD,EAAE,EAAE,IAAI;iBACT,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,qEAAqE;IACrE,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;QAC/C,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;YACjB,cAAc,EAAE,mBAAmB;YACnC,eAAe,EAAE,UAAU;YAC3B,UAAU,EAAE,YAAY;YACxB,6BAA6B,EAAE,GAAG;SACnC,CAAC,CAAC;QACH,GAAG,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;QAC5C,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACpB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,yBAAyB;IACzB,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,IAAa,EAAE,GAAa,EAAE,EAAE;QACjD,GAAG,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,kEAAkE;IAClE,4EAA4E;IAC5E,mBAAmB,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAEvC,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;QACvC,OAAO,CAAC,GAAG,CAAC,uDAAuD,IAAI,MAAM,CAAC,CAAC;QAC/E,OAAO,CAAC,GAAG,CAAC,wCAAwC,IAAI,OAAO,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,IAAI,YAAY,GAAG,KAAK,CAAC;IACzB,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,IAAI,YAAY;YAAE,OAAO;QACzB,YAAY,GAAG,IAAI,CAAC;QACpB,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;QAE9C,oCAAoC;QACpC,UAAU,CAAC,KAAK,EAAE,CAAC;QAEnB,oCAAoC;QACpC,aAAa,CAAC,mBAAmB,CAAC,CAAC;QAEnC,2BAA2B;QAC3B,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;YAChC,IAAI,CAAC;gBAAC,MAAM,CAAC,GAAG,EAAE,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;QACnD,CAAC;QACD,UAAU,CAAC,KAAK,EAAE,CAAC;QAEnB,wBAAwB;QACxB,QAAQ,CAAC,KAAK,EAAE,CAAC;QAEjB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;QAClC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAClC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,10 +1,3 @@
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
2
  export {};
10
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;GAMG"}
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, getSharedState } from './server.js';
18
- async function startStdioServer(factory) {
19
- await factory().connect(new StdioServerTransport());
20
- }
21
- async function startHTTPServer(factoryWithSSE) {
22
- const port = parseInt(process.env.PORT ?? '3001', 10);
23
- const app = express();
24
- app.use(helmet({ contentSecurityPolicy: false })); // Security headers
25
- app.use(cors({
26
- origin: process.env.CORS_ORIGIN ?? '*',
27
- }));
28
- // Rate limiting
29
- const limiter = rateLimit({
30
- windowMs: 60 * 1000, // 1 minute
31
- max: 200, // 200 requests per minute
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
- // Factory that wires SSE broadcasting
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
- const factory = () => createServer(store);
208
- await startStdioServer(factory);
32
+ await startStdioServer(() => createServer(store));
209
33
  }
210
34
  else {
211
- await startHTTPServer((_sseClients, broadcastSSE) => {
212
- return createServer(store, (state) => {
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) => {