@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.
- package/README.md +279 -0
- package/dist/index.mjs +492 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# @lottiefiles/creator-mcp
|
|
2
|
+
|
|
3
|
+
[](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
|
+
[](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
|
+
[](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
|
+
[](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
|
+
}
|