@lottiefiles/creator-mcp 0.1.1

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 (3) hide show
  1. package/README.md +279 -0
  2. package/dist/index.mjs +492 -0
  3. package/package.json +54 -0
package/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # @lottiefiles/creator-mcp
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@lottiefiles/creator-mcp.svg?style=flat-square)](https://www.npmjs.com/package/@lottiefiles/creator-mcp)
4
+
5
+ Let AI control [LottieFiles Creator](https://creator.lottiefiles.com) — build and edit
6
+ Lottie animations through natural language.
7
+
8
+ <!-- TODO: Add demo GIF showing AI assistant controlling Creator -->
9
+
10
+ [Getting Started](#getting-started) | [Example Prompts](#example-prompts)
11
+
12
+ ---
13
+
14
+ ## What It Does
15
+
16
+ This MCP server connects AI assistants like Claude, Cursor, and Windsurf to the
17
+ [LottieFiles Creator](https://creator.lottiefiles.com) animation editor. Your AI gets full
18
+ access to the Creator API — it can read, modify, and create animations on your behalf.
19
+
20
+ - **Read & modify animation properties** — colors, transforms, opacity, timing
21
+ - **Create shapes, layers, and keyframes** programmatically
22
+ - **Inspect animation structure** — list layers, examine keyframes, export data
23
+ - **Full Creator API access** — anything Creator can do, your AI can do
24
+
25
+ ## Getting Started
26
+
27
+ ### Prerequisites
28
+
29
+ - **Node.js** 18 or later
30
+ - **LottieFiles Creator** open in your browser at
31
+ [creator.lottiefiles.com](https://creator.lottiefiles.com)
32
+
33
+ ### Install
34
+
35
+ [![Install in VS Code](https://img.shields.io/badge/Install_in-VS_Code-0098FF?style=flat-square&logo=visualstudiocode&logoColor=ffffff)](https://vscode.dev/redirect/mcp/install?name=%40lottiefiles%2Fcreator-mcp&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40lottiefiles%2Fcreator-mcp%22%5D%7D)
36
+ [![Install in VS Code Insiders](https://img.shields.io/badge/Install_in-VS_Code_Insiders-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=ffffff)](https://insiders.vscode.dev/redirect/mcp/install?name=%40lottiefiles%2Fcreator-mcp&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40lottiefiles%2Fcreator-mcp%22%5D%7D&quality=insiders)
37
+ [![Install in Cursor](https://img.shields.io/badge/Install_in-Cursor-000000?style=flat-square&logo=cursor&logoColor=ffffff)](https://cursor.com/install-mcp?name=lottiefiles-creator&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsb3R0aWVmaWxlcy9jcmVhdG9yLW1jcCJdfQ==)
38
+
39
+ Add the following config to your MCP client:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "lottiefiles-creator": {
45
+ "command": "npx",
46
+ "args": ["-y", "@lottiefiles/creator-mcp@latest"]
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ### MCP Client configuration
53
+
54
+ <details>
55
+ <summary><strong>Amp</strong></summary>
56
+
57
+ ```bash
58
+ amp mcp add lottiefiles-creator -- npx -y @lottiefiles/creator-mcp@latest
59
+ ```
60
+
61
+ </details>
62
+
63
+ <details>
64
+ <summary><strong>Antigravity</strong></summary>
65
+
66
+ Follow Antigravity's MCP setup docs and use the config above.
67
+
68
+ </details>
69
+
70
+ <details>
71
+ <summary><strong>Claude Code</strong></summary>
72
+
73
+ ```bash
74
+ claude mcp add lottiefiles-creator -- npx -y @lottiefiles/creator-mcp@latest
75
+ ```
76
+
77
+ </details>
78
+
79
+ <details>
80
+ <summary><strong>Claude Desktop</strong></summary>
81
+
82
+ Use the config above in your `claude_desktop_config.json`.
83
+
84
+ </details>
85
+
86
+ <details>
87
+ <summary><strong>Cline</strong></summary>
88
+
89
+ Follow Cline's MCP setup docs and use the config above.
90
+
91
+ </details>
92
+
93
+ <details>
94
+ <summary><strong>Codex</strong></summary>
95
+
96
+ Follow the [configure MCP guide](https://github.com/openai/codex/blob/main/docs/config.md)
97
+ using:
98
+
99
+ ```toml
100
+ [mcp_servers.lottiefiles-creator]
101
+ command = "npx"
102
+ args = ["-y", "@lottiefiles/creator-mcp@latest"]
103
+ ```
104
+
105
+ Or add via CLI:
106
+
107
+ ```bash
108
+ codex mcp add lottiefiles-creator -- npx -y @lottiefiles/creator-mcp@latest
109
+ ```
110
+
111
+ </details>
112
+
113
+ <details>
114
+ <summary><strong>Copilot CLI</strong></summary>
115
+
116
+ 1. Start `copilot`
117
+ 2. Run `/mcp add`
118
+ 3. Configure:
119
+
120
+ - Server name: `lottiefiles-creator`
121
+ - Server type: `Local`
122
+ - Command: `npx -y @lottiefiles/creator-mcp@latest`
123
+
124
+ </details>
125
+
126
+ <details>
127
+ <summary><strong>Copilot / VS Code</strong></summary>
128
+
129
+ Follow VS Code MCP setup docs and use the config above.
130
+
131
+ Or via CLI:
132
+
133
+ ```bash
134
+ code --add-mcp '{"name":"lottiefiles-creator","command":"npx","args":["-y","@lottiefiles/creator-mcp@latest"]}'
135
+ ```
136
+
137
+ </details>
138
+
139
+ <details>
140
+ <summary><strong>Copilot / GitHub CLI</strong></summary>
141
+
142
+ ```bash
143
+ gh copilot mcp add lottiefiles-creator -- npx -y @lottiefiles/creator-mcp@latest
144
+ ```
145
+
146
+ </details>
147
+
148
+ <details>
149
+ <summary><strong>Cursor</strong></summary>
150
+
151
+ Click the install button above, or use the config above.
152
+
153
+ </details>
154
+
155
+ <details>
156
+ <summary><strong>Factory CLI</strong></summary>
157
+
158
+ ```bash
159
+ droid mcp add lottiefiles-creator "npx -y @lottiefiles/creator-mcp@latest"
160
+ ```
161
+
162
+ </details>
163
+
164
+ <details>
165
+ <summary><strong>Gemini CLI</strong></summary>
166
+
167
+ ```bash
168
+ gemini mcp add lottiefiles-creator npx -y @lottiefiles/creator-mcp@latest
169
+ gemini mcp add -s user lottiefiles-creator npx -y @lottiefiles/creator-mcp@latest
170
+ ```
171
+
172
+ </details>
173
+
174
+ <details>
175
+ <summary><strong>Gemini Code Assist</strong></summary>
176
+
177
+ Follow Gemini Code Assist MCP setup docs and use the config above.
178
+
179
+ </details>
180
+
181
+ <details>
182
+ <summary><strong>JetBrains AI Assistant & Junie</strong></summary>
183
+
184
+ Follow JetBrains MCP setup docs and use the config above.
185
+
186
+ </details>
187
+
188
+ <details>
189
+ <summary><strong>Kiro</strong></summary>
190
+
191
+ Follow Kiro MCP setup docs and use the config above.
192
+
193
+ </details>
194
+
195
+ <details>
196
+ <summary><strong>Qoder</strong></summary>
197
+
198
+ Follow Qoder MCP setup docs and use the config above.
199
+
200
+ </details>
201
+
202
+ <details>
203
+ <summary><strong>Visual Studio</strong></summary>
204
+
205
+ Add a local MCP server with:
206
+
207
+ - Name: `lottiefiles-creator`
208
+ - Command: `npx`
209
+ - Arguments: `-y @lottiefiles/creator-mcp@latest`
210
+
211
+ </details>
212
+
213
+ <details>
214
+ <summary><strong>Warp</strong></summary>
215
+
216
+ Follow Warp MCP setup docs and use the config above.
217
+
218
+ </details>
219
+
220
+ <details>
221
+ <summary><strong>Windsurf</strong></summary>
222
+
223
+ Follow Windsurf MCP setup docs and use the config above.
224
+
225
+ </details>
226
+
227
+ <details>
228
+ <summary><strong>VS Code</strong></summary>
229
+
230
+ If using `.vscode/mcp.json`, use:
231
+
232
+ ```json
233
+ {
234
+ "servers": {
235
+ "lottiefiles-creator": {
236
+ "command": "npx",
237
+ "args": ["-y", "@lottiefiles/creator-mcp@latest"]
238
+ }
239
+ }
240
+ }
241
+ ```
242
+
243
+ </details>
244
+
245
+ ### Your first prompt
246
+
247
+ 1. Open [LottieFiles Creator](https://creator.lottiefiles.com).
248
+ 2. Enable MCP in Creator.
249
+ 3. Keep that tab open while using your AI assistant.
250
+
251
+ Ask your assistant:
252
+
253
+ ```text
254
+ Run a script that logs the active scene size in lottiefiles creator
255
+ ```
256
+
257
+ If setup is correct, you should see a tool response with scene dimensions.
258
+
259
+ ## Example Prompts
260
+
261
+ Once connected, try asking your AI assistant:
262
+
263
+ | Prompt | What it does |
264
+ | ---------------------------------------------- | ------------------------------------------------ |
265
+ | "What layers are in this animation?" | Lists all layers with their types and properties |
266
+ | "Change the background color to blue" | Modifies the background layer color |
267
+ | "List all keyframes on the first layer" | Inspects keyframe data for a specific layer |
268
+ | "Make the animation loop smoothly" | Adjusts timing and easing for seamless looping |
269
+ | "Center all selected layers in the scene" | Aligns selected layers to scene center |
270
+ | "Change all fills to a neon cyan palette" | Batch-updates colors across shapes |
271
+ | "Create a pulsing loading spinner" | Builds a looped shape animation |
272
+ | "Offset each layer by 3 frames" | Staggers layer timing for motion rhythm |
273
+ | "Remove hidden layers from this scene" | Cleans up invisible and unused layers |
274
+ | "Round all rectangle corners to 16" | Applies consistent corner radius styling |
275
+ | "List every text layer and its content" | Extracts text content for copy review |
276
+ | "Duplicate this scene and make a dark variant" | Creates an alternate themed scene |
277
+ | "Create a 2-second intro with logo fade-in" | Generates timeline-based intro animation |
278
+
279
+ <!-- TODO: Add screenshot showing example prompt and Creator result side-by-side -->
package/dist/index.mjs ADDED
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env node
2
+ import { WebSocketServer } from "ws";
3
+ import { randomUUID } from "crypto";
4
+ import { z } from "zod";
5
+ import { createRequire } from "module";
6
+ import { Console } from "node:console";
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { onExit } from "signal-exit";
10
+ import eos from "end-of-stream";
11
+
12
+ //#region ../creator-mcp-shared/src/constants.ts
13
+ /** Default WebSocket port used by the MCP bridge. */
14
+ const DEFAULT_WS_PORT = 3847;
15
+ /** WebSocket host (localhost only for security). */
16
+ const WS_HOST = "127.0.0.1";
17
+ /** Full WebSocket URL for the MCP bridge. */
18
+ const WS_URL = `ws://${WS_HOST}:${DEFAULT_WS_PORT}`;
19
+ /** Custom close code: another tab is already connected. */
20
+ const WS_CLOSE_ANOTHER_TAB = 4001;
21
+ /** Canonical tool name for the run_script MCP tool. */
22
+ const RUN_SCRIPT_TOOL_NAME = "run_script";
23
+ /** Canonical tool description for the run_script MCP tool. */
24
+ const RUN_SCRIPT_TOOL_DESCRIPTION = `Execute a JavaScript script in the LottieFiles Creator animation editor. The script runs in a sandboxed runtime with access to the \`creator\` global object (the Creator API) for reading and manipulating animations, layers, shapes, keyframes, colors, and more. Use console.log() to output results — all console output is captured and returned.
25
+
26
+ IMPORTANT: Before calling this tool, you MUST call these tools (once per session):
27
+ 1. get_api_doc — The API type definitions (paginated, read ALL pages). The Creator API is unique and cannot be guessed.
28
+ 2. get_rules — Conventions like layer ordering that affect visual correctness.
29
+
30
+ Skipping these tools will lead to TypeError and incorrect scripts. Call them first, then write your script.`;
31
+ /** Canonical tool name for the get_api_doc MCP tool. */
32
+ const GET_API_DOC_TOOL_NAME = "get_api_doc";
33
+ /** Lines per page for paginated get_api_doc responses. */
34
+ const API_DOC_PAGE_SIZE = 1e3;
35
+ /** Canonical tool description for the get_api_doc MCP tool. */
36
+ const GET_API_DOC_TOOL_DESCRIPTION = `Get the TypeScript type definitions for the Creator API. The API is large, so results are paginated (~1000 lines per page). Call with page=1 first, then continue calling with each subsequent page number until all pages are read.
37
+
38
+ YOU MUST read ALL pages before writing any scripts with run_script. The Creator API is unique and cannot be guessed from common patterns. Skipping pages will cause TypeErrors and incorrect scripts.`;
39
+ /** Canonical tool name for the get_rules MCP tool. */
40
+ const GET_RULES_TOOL_NAME = "get_rules";
41
+ /** Canonical tool description for the get_rules MCP tool. */
42
+ const GET_RULES_TOOL_DESCRIPTION = "Get the rules and conventions for writing Creator scripts. YOU MUST call this tool before writing scripts to avoid common pitfalls like incorrect layer ordering.";
43
+ /** Canonical MCP setup guide URL. */
44
+ const MCP_GUIDE_URL = "https://lottiefiles.notion.site/lottie-creator-mcp";
45
+ /** Message returned when a script produces no console output. */
46
+ const NO_RETURN_VALUE_MESSAGE = "(Script executed successfully, no console output)";
47
+
48
+ //#endregion
49
+ //#region ../creator-mcp-shared/src/schemas.ts
50
+ /** Zod schema for validating incoming ScriptRequest messages over WebSocket. */
51
+ const scriptRequestSchema = z.object({
52
+ id: z.string(),
53
+ type: z.literal("run_script"),
54
+ script: z.string()
55
+ });
56
+ /** Zod schema for validating ScriptResponse messages over WebSocket. */
57
+ const scriptResponseSchema = z.object({
58
+ type: z.literal("script_response"),
59
+ id: z.string(),
60
+ logs: z.array(z.string()),
61
+ error: z.string().optional()
62
+ });
63
+
64
+ //#endregion
65
+ //#region ../creator-mcp-shared/src/resources.ts
66
+ /** Path to the Creator API type definitions, relative to the Creator origin. */
67
+ const TYPES_RESOURCE = { path: "/mcp-resources/types.d.ts" };
68
+ /** Path to the Creator scripting rules, relative to the Creator origin. */
69
+ const RULES_RESOURCE = { path: "/mcp-resources/rules.md" };
70
+
71
+ //#endregion
72
+ //#region ../creator-mcp-shared/src/format-tool-result.ts
73
+ /** Format script execution logs and optional error into an MCP tool result. */
74
+ function formatToolResult(logs, error) {
75
+ if (error) return {
76
+ content: [{
77
+ type: "text",
78
+ text: logs.length > 0 ? `${logs.join("\n")}\n\nError: ${error}` : `Error: ${error}`
79
+ }],
80
+ isError: true
81
+ };
82
+ return { content: [{
83
+ type: "text",
84
+ text: logs.length > 0 ? logs.join("\n") : NO_RETURN_VALUE_MESSAGE
85
+ }] };
86
+ }
87
+ /** Paginate a large text document and return a single page with navigation hints. */
88
+ function paginateText(text, page, toolName, pageSize = API_DOC_PAGE_SIZE) {
89
+ const lines = text.split("\n");
90
+ const totalPages = Math.ceil(lines.length / pageSize);
91
+ if (page < 1 || page > totalPages) return { content: [{
92
+ type: "text",
93
+ text: `Invalid page ${page}. The document has ${totalPages} pages.`
94
+ }] };
95
+ const start = (page - 1) * pageSize;
96
+ const pageLines = lines.slice(start, start + pageSize);
97
+ const header = `[Page ${page} of ${totalPages}]`;
98
+ const footer = page < totalPages ? `\n\n[End of page ${page}/${totalPages} — IMPORTANT: You MUST call ${toolName} with page=${page + 1} to continue reading. Do NOT write scripts until you have read ALL pages.]` : `\n\n[End of page ${page}/${totalPages} — You have read the complete API documentation.]`;
99
+ return { content: [{
100
+ type: "text",
101
+ text: `${header}\n\n${pageLines.join("\n")}${footer}`
102
+ }] };
103
+ }
104
+
105
+ //#endregion
106
+ //#region src/constants.ts
107
+ const pkg = createRequire(import.meta.url)("../package.json");
108
+ const PACKAGE_NAME = pkg.name;
109
+ const PACKAGE_VERSION = pkg.version;
110
+ const env = z.object({
111
+ CREATOR_URL: z.string().url().default("https://creator.lottiefiles.com"),
112
+ WS_PORT: z.coerce.number().int().min(1).max(65535).default(3847),
113
+ SCRIPT_TIMEOUT_MS: z.coerce.number().int().min(1e3).max(3e5).default(6e4)
114
+ }).parse(process.env);
115
+ const CREATOR_URL = env.CREATOR_URL;
116
+ const WS_PORT = env.WS_PORT;
117
+ const PING_INTERVAL_MS = 5e3;
118
+ const SCRIPT_TIMEOUT_MS = env.SCRIPT_TIMEOUT_MS;
119
+ const WEBSOCKET_SERVER_UNAVAILABLE_MESSAGE = "Creator MCP bridge is unavailable in this session. Another Creator MCP server may already be running in a different app or terminal. Stop other Creator MCP servers, then restart this one.";
120
+ const NO_CREATOR_CONNECTED_MESSAGE = `No Creator tab is connected. Open LottieFiles Creator at ${CREATOR_URL} in your browser and enable MCP. If you need setup help, see ${MCP_GUIDE_URL}`;
121
+
122
+ //#endregion
123
+ //#region src/logger.ts
124
+ const prefix = `[${PACKAGE_NAME.replace("@", "").replace("/", "-")}]`;
125
+ /**
126
+ * MCP-safe logger that follows the Console API.
127
+ *
128
+ * All output goes to stderr so it never conflicts with the MCP stdio
129
+ * transport on stdout. Messages are automatically prefixed with the
130
+ * package name.
131
+ */
132
+ var Logger = class extends Console {
133
+ constructor() {
134
+ super({
135
+ stdout: process.stderr,
136
+ stderr: process.stderr
137
+ });
138
+ }
139
+ log(...args) {
140
+ super.log(prefix, ...args);
141
+ }
142
+ info(...args) {
143
+ super.info(prefix, ...args);
144
+ }
145
+ warn(...args) {
146
+ super.warn(prefix, ...args);
147
+ }
148
+ error(...args) {
149
+ super.error(prefix, ...args);
150
+ }
151
+ debug(...args) {
152
+ super.debug(prefix, ...args);
153
+ }
154
+ };
155
+ const logger = new Logger();
156
+
157
+ //#endregion
158
+ //#region src/websocket-server.ts
159
+ const allowedOrigin = new URL(CREATOR_URL).origin;
160
+ var ConnectionManager = class {
161
+ _wss = null;
162
+ _activeConnection = null;
163
+ _pendingRequests = /* @__PURE__ */ new Map();
164
+ _pingInterval = null;
165
+ _isWebSocketReady = false;
166
+ async start(port = DEFAULT_WS_PORT) {
167
+ return new Promise((resolve, reject) => {
168
+ this._wss = new WebSocketServer({
169
+ port,
170
+ host: WS_HOST,
171
+ maxPayload: 10 * 1024 * 1024
172
+ });
173
+ this._wss.on("listening", () => {
174
+ logger.info(`WebSocket server listening on ${WS_HOST}:${port}`);
175
+ this._isWebSocketReady = true;
176
+ this._startPingInterval();
177
+ resolve();
178
+ });
179
+ this._wss.on("error", (error) => {
180
+ this._isWebSocketReady = false;
181
+ if (error.code === "EADDRINUSE") logger.error(`Port ${port} is already in use`);
182
+ reject(error);
183
+ });
184
+ this._wss.on("close", () => {
185
+ this._isWebSocketReady = false;
186
+ });
187
+ this._wss.on("connection", (ws, req) => {
188
+ if (req.headers.origin !== allowedOrigin) {
189
+ logger.warn(`Rejected connection from origin: ${req.headers.origin}`);
190
+ ws.close(1008, "Origin not allowed");
191
+ return;
192
+ }
193
+ if (this._activeConnection) {
194
+ logger.warn("Rejected connection: another tab is already connected");
195
+ ws.close(WS_CLOSE_ANOTHER_TAB, "Another tab is already connected");
196
+ return;
197
+ }
198
+ const id = randomUUID();
199
+ this._activeConnection = {
200
+ id,
201
+ ws,
202
+ isAlive: true
203
+ };
204
+ logger.info(`Tab connected (id: ${id})`);
205
+ ws.on("pong", () => {
206
+ if (this._activeConnection?.id === id) this._activeConnection.isAlive = true;
207
+ });
208
+ ws.on("message", (data) => {
209
+ try {
210
+ const parsed = JSON.parse(data.toString());
211
+ const result = scriptResponseSchema.safeParse(parsed);
212
+ if (!result.success) {
213
+ logger.warn("Received message with unexpected shape:", JSON.stringify(parsed));
214
+ return;
215
+ }
216
+ this._handleMessage(result.data);
217
+ } catch (error) {
218
+ logger.error("Failed to parse message:", error);
219
+ }
220
+ });
221
+ ws.on("close", () => {
222
+ this._handleDisconnect();
223
+ });
224
+ ws.on("error", () => {
225
+ this._handleDisconnect();
226
+ });
227
+ });
228
+ });
229
+ }
230
+ _handleMessage(message) {
231
+ if (message.type !== "script_response") return;
232
+ const pending = this._pendingRequests.get(message.id);
233
+ if (!pending) {
234
+ logger.warn(`Received response for unknown request id: ${message.id}`);
235
+ return;
236
+ }
237
+ clearTimeout(pending.timer);
238
+ this._pendingRequests.delete(message.id);
239
+ pending.resolve(message);
240
+ }
241
+ _handleDisconnect() {
242
+ if (!this._activeConnection) return;
243
+ const { id } = this._activeConnection;
244
+ this._activeConnection = null;
245
+ for (const [reqId, pending] of this._pendingRequests) {
246
+ clearTimeout(pending.timer);
247
+ pending.reject(/* @__PURE__ */ new Error("Creator tab disconnected during script execution"));
248
+ this._pendingRequests.delete(reqId);
249
+ }
250
+ logger.info(`Tab disconnected (id: ${id})`);
251
+ }
252
+ _startPingInterval() {
253
+ this._pingInterval = setInterval(() => {
254
+ const conn = this._activeConnection;
255
+ if (!conn) return;
256
+ if (!conn.isAlive) {
257
+ logger.warn(`Tab unresponsive, terminating (id: ${conn.id})`);
258
+ conn.ws.terminate();
259
+ this._handleDisconnect();
260
+ return;
261
+ }
262
+ conn.isAlive = false;
263
+ conn.ws.ping();
264
+ }, PING_INTERVAL_MS);
265
+ }
266
+ get hasActiveConnection() {
267
+ return this._activeConnection !== null;
268
+ }
269
+ get isWebSocketReady() {
270
+ return this._isWebSocketReady;
271
+ }
272
+ async executeScript(script) {
273
+ if (!this._activeConnection) throw new Error(NO_CREATOR_CONNECTED_MESSAGE);
274
+ const conn = this._activeConnection;
275
+ const id = randomUUID();
276
+ const request = {
277
+ id,
278
+ type: "run_script",
279
+ script
280
+ };
281
+ return new Promise((resolve, reject) => {
282
+ const timer = setTimeout(() => {
283
+ this._pendingRequests.delete(id);
284
+ reject(/* @__PURE__ */ new Error(`Script execution timed out after ${SCRIPT_TIMEOUT_MS / 1e3}s`));
285
+ }, SCRIPT_TIMEOUT_MS);
286
+ this._pendingRequests.set(id, {
287
+ resolve,
288
+ reject,
289
+ timer
290
+ });
291
+ try {
292
+ conn.ws.send(JSON.stringify(request));
293
+ } catch (error) {
294
+ clearTimeout(timer);
295
+ this._pendingRequests.delete(id);
296
+ reject(/* @__PURE__ */ new Error(`Failed to send script to Creator: ${error instanceof Error ? error.message : String(error)}`));
297
+ }
298
+ });
299
+ }
300
+ stop() {
301
+ if (this._pingInterval) {
302
+ clearInterval(this._pingInterval);
303
+ this._pingInterval = null;
304
+ }
305
+ for (const [, pending] of this._pendingRequests) {
306
+ clearTimeout(pending.timer);
307
+ pending.reject(/* @__PURE__ */ new Error("Server shutting down"));
308
+ }
309
+ this._pendingRequests.clear();
310
+ if (this._activeConnection) {
311
+ this._activeConnection.ws.close();
312
+ this._activeConnection = null;
313
+ }
314
+ this._wss?.close();
315
+ this._wss = null;
316
+ this._isWebSocketReady = false;
317
+ }
318
+ };
319
+
320
+ //#endregion
321
+ //#region src/tools/run-script.ts
322
+ async function handleRunScript(connectionManager, script) {
323
+ if (!connectionManager.isWebSocketReady) return {
324
+ content: [{
325
+ type: "text",
326
+ text: WEBSOCKET_SERVER_UNAVAILABLE_MESSAGE
327
+ }],
328
+ isError: true
329
+ };
330
+ if (!connectionManager.hasActiveConnection) return {
331
+ content: [{
332
+ type: "text",
333
+ text: NO_CREATOR_CONNECTED_MESSAGE
334
+ }],
335
+ isError: true
336
+ };
337
+ try {
338
+ const response = await connectionManager.executeScript(script);
339
+ return formatToolResult(response.logs, response.error);
340
+ } catch (error) {
341
+ return {
342
+ content: [{
343
+ type: "text",
344
+ text: error instanceof Error ? error.message : String(error)
345
+ }],
346
+ isError: true
347
+ };
348
+ }
349
+ }
350
+
351
+ //#endregion
352
+ //#region src/mcp-server.ts
353
+ async function fetchCreatorText(resourcePath) {
354
+ const url = `${CREATOR_URL}${resourcePath}`;
355
+ const response = await fetch(url);
356
+ if (!response.ok) throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
357
+ return response.text();
358
+ }
359
+ function textResult(text) {
360
+ return { content: [{
361
+ type: "text",
362
+ text
363
+ }] };
364
+ }
365
+ async function startMcpServer(connectionManager) {
366
+ const server = new McpServer({
367
+ name: PACKAGE_NAME,
368
+ version: PACKAGE_VERSION
369
+ });
370
+ server.registerTool(RUN_SCRIPT_TOOL_NAME, {
371
+ title: "Run Script in Creator",
372
+ description: RUN_SCRIPT_TOOL_DESCRIPTION,
373
+ inputSchema: { script: z.string().describe("JavaScript code to execute. Use console.log() to output results.") },
374
+ annotations: {
375
+ readOnlyHint: false,
376
+ destructiveHint: false,
377
+ idempotentHint: false,
378
+ openWorldHint: false
379
+ }
380
+ }, async ({ script }) => {
381
+ return handleRunScript(connectionManager, script);
382
+ });
383
+ server.registerTool(GET_API_DOC_TOOL_NAME, {
384
+ title: "Get Creator API Documentation",
385
+ description: GET_API_DOC_TOOL_DESCRIPTION,
386
+ inputSchema: { page: z.number().int().min(1).default(1).describe("Page number (1-indexed). Start with 1 and keep calling until all pages are read.") },
387
+ annotations: {
388
+ readOnlyHint: true,
389
+ destructiveHint: false,
390
+ idempotentHint: true,
391
+ openWorldHint: false
392
+ }
393
+ }, async ({ page }) => {
394
+ try {
395
+ return paginateText(await fetchCreatorText(TYPES_RESOURCE.path), page, GET_API_DOC_TOOL_NAME);
396
+ } catch (error) {
397
+ return {
398
+ content: [{
399
+ type: "text",
400
+ text: error instanceof Error ? error.message : String(error)
401
+ }],
402
+ isError: true
403
+ };
404
+ }
405
+ });
406
+ server.registerTool(GET_RULES_TOOL_NAME, {
407
+ title: "Get Creator Script Rules",
408
+ description: GET_RULES_TOOL_DESCRIPTION,
409
+ annotations: {
410
+ readOnlyHint: true,
411
+ destructiveHint: false,
412
+ idempotentHint: true,
413
+ openWorldHint: false
414
+ }
415
+ }, async () => {
416
+ try {
417
+ return textResult(await fetchCreatorText(RULES_RESOURCE.path));
418
+ } catch (error) {
419
+ return {
420
+ content: [{
421
+ type: "text",
422
+ text: error instanceof Error ? error.message : String(error)
423
+ }],
424
+ isError: true
425
+ };
426
+ }
427
+ });
428
+ const transport = new StdioServerTransport();
429
+ await server.connect(transport);
430
+ logger.info("MCP server started on stdio");
431
+ }
432
+
433
+ //#endregion
434
+ //#region src/index.ts
435
+ function setupShutdownHandlers(connectionManager) {
436
+ let isCleanedUp = false;
437
+ const cleanup = (reason) => {
438
+ if (isCleanedUp) return;
439
+ isCleanedUp = true;
440
+ logger.info(`Shutting down Creator MCP (${reason})`);
441
+ try {
442
+ connectionManager.stop();
443
+ } catch (error) {
444
+ logger.error("Failed during shutdown:", error);
445
+ }
446
+ };
447
+ onExit((code, signal) => {
448
+ cleanup(signal ?? `exit code ${code ?? 0}`);
449
+ });
450
+ eos(process.stdin, (error) => {
451
+ if (error) {
452
+ logger.error("stdin stream closed with error:", error);
453
+ cleanup("stdin error");
454
+ process.exit(1);
455
+ }
456
+ cleanup("stdin closed");
457
+ process.exit(0);
458
+ });
459
+ process.once("uncaughtException", (error) => {
460
+ logger.error("Uncaught exception:", error);
461
+ cleanup("uncaughtException");
462
+ process.exit(1);
463
+ });
464
+ process.once("unhandledRejection", (reason) => {
465
+ logger.error("Unhandled promise rejection:", reason);
466
+ cleanup("unhandledRejection");
467
+ process.exit(1);
468
+ });
469
+ }
470
+ async function main() {
471
+ const connectionManager = new ConnectionManager();
472
+ setupShutdownHandlers(connectionManager);
473
+ try {
474
+ await connectionManager.start(WS_PORT);
475
+ } catch (error) {
476
+ if (error.code === "EADDRINUSE") {
477
+ logger.error(`Port ${WS_PORT} is already in use. Another instance may be running.`);
478
+ logger.error("Starting in limited mode (MCP server only, no WebSocket).");
479
+ } else {
480
+ logger.error("Failed to start WebSocket server:", error);
481
+ process.exit(1);
482
+ }
483
+ }
484
+ await startMcpServer(connectionManager);
485
+ }
486
+ main().catch((error) => {
487
+ logger.error("Fatal error:", error);
488
+ process.exit(1);
489
+ });
490
+
491
+ //#endregion
492
+ export { };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@lottiefiles/creator-mcp",
3
+ "version": "0.1.1",
4
+ "description": "MCP server for LottieFiles Creator",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "registry": "https://registry.npmjs.org",
9
+ "access": "public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/LottieFiles/creator.git"
14
+ },
15
+ "bin": {
16
+ "creator-mcp": "dist/index.mjs"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "1.27.1",
23
+ "end-of-stream": "1.4.5",
24
+ "signal-exit": "4.1.0",
25
+ "ws": "8.19.0",
26
+ "zod": "4.1.5"
27
+ },
28
+ "devDependencies": {
29
+ "@eslint/js": "9.23.0",
30
+ "@types/end-of-stream": "1.4.4",
31
+ "@types/node": "22.9.0",
32
+ "@types/ws": "8.18.1",
33
+ "@vitest/eslint-plugin": "1.1.39",
34
+ "eslint": "9.16.0",
35
+ "tsdown": "0.20.3",
36
+ "tsx": "4.21.0",
37
+ "typescript": "5.8.3",
38
+ "typescript-eslint": "8.29.0",
39
+ "vitest": "4.0.18",
40
+ "@lottiefiles/creator-mcp-shared": "0.1.0"
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "dev": "tsx watch src/index.ts",
45
+ "start": "node dist/index.mjs",
46
+ "lint": "pnpm lint:eslint",
47
+ "lint:check": "pnpm lint:eslint:check",
48
+ "lint:eslint": "eslint --fix .",
49
+ "lint:eslint:check": "eslint .",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "type-check": "tsc --noEmit"
53
+ }
54
+ }