keyframe-mcp 0.1.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/package.json +35 -0
  4. package/server.mjs +131 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Keyframe.it
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # keyframe-mcp
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server that lets Claude — or any MCP client —
4
+ **rig, animate and export 2D skeletal animations** by driving
5
+ [Keyframe.it](https://www.keyframe.it.com), a free browser-based animation editor for game
6
+ sprites.
7
+
8
+ It opens the editor in your own Chrome and exposes Keyframe.it's `window.keyframe` scripting
9
+ API (150+ methods) as tools. Rendered frames, clips and assets come back as **real images**, so
10
+ the model sees the art it's working on — and the finished rig is sitting in your browser,
11
+ ready to keep editing.
12
+
13
+ Exports are Spine-compatible bundles (3.8 / 4.2 — `.json` or binary `.skel` + `.atlas` + `.png`)
14
+ that load in pixi-spine and other Spine runtimes. Free, no account, nothing uploaded.
15
+
16
+ ## Install
17
+
18
+ **Claude Code**
19
+
20
+ ```bash
21
+ claude mcp add keyframe -- npx -y keyframe-mcp
22
+ ```
23
+
24
+ **Claude Desktop / Cursor / any MCP client** — add to your MCP config:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "keyframe": {
30
+ "command": "npx",
31
+ "args": ["-y", "keyframe-mcp"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ Requires **Node 18+** and **Google Chrome** installed. It drives your existing Chrome — no
38
+ browser download.
39
+
40
+ ## Usage
41
+
42
+ Ask your assistant something like:
43
+
44
+ > Open Keyframe.it, load the noir example, and make a 2-second idle animation where she
45
+ > breathes and her hair sways. Show me the result.
46
+
47
+ The assistant calls `keyframe_open`, reads the API with `keyframe_methods`, then works through
48
+ `keyframe_call`. For multi-step jobs it should read the built-in recipes first
49
+ (`keyframe_call {method: "recipes"}`) — they encode the right order of operations for slicing
50
+ sheets, rigging, meshing and weights, idle loops, walk cycles and win FX.
51
+
52
+ ## Tools
53
+
54
+ | Tool | What it does |
55
+ | --- | --- |
56
+ | `keyframe_open` `{url?}` | Open the editor (launches Chrome on first use, reuses the tab after). Returns an overview. **Call first.** |
57
+ | `keyframe_methods` | List every scripting method with a one-line description. |
58
+ | `keyframe_call` `{method, args?}` | Call any `window.keyframe` method. Image results (`renderFrame`, `renderClip`, `getAssetPng`) return as actual images. |
59
+ | `keyframe_screenshot` | Screenshot the whole editor UI (rarely needed). |
60
+
61
+ ## Environment
62
+
63
+ | Variable | Default | Meaning |
64
+ | --- | --- | --- |
65
+ | `KEYFRAME_URL` | `https://www.keyframe.it.com/` | Editor URL — set `http://localhost:5173` to drive a local dev build |
66
+ | `KEYFRAME_HEADLESS` | `1` | Set `0` to watch the work happen in a visible Chrome window |
67
+
68
+ Tip: `KEYFRAME_HEADLESS=0` is genuinely fun the first time — you watch bones and keyframes
69
+ appear as the model works.
70
+
71
+ ## Docs
72
+
73
+ The scripting surface is documented in the editor (`window.keyframe.help()`) and at
74
+ [keyframe.it.com/llms.txt](https://www.keyframe.it.com/llms.txt). For the whole product in one
75
+ fetch, see [llms-full.txt](https://www.keyframe.it.com/llms-full.txt).
76
+
77
+ ## License
78
+
79
+ MIT — see [LICENSE](./LICENSE). This covers the connector; the Keyframe.it editor is a separate
80
+ hosted application.
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "keyframe-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Keyframe.it — let Claude (or any MCP client) rig, animate and export 2D skeletal animations by driving the free Keyframe.it editor in your browser.",
5
+ "type": "module",
6
+ "bin": { "keyframe-mcp": "server.mjs" },
7
+ "main": "server.mjs",
8
+ "files": ["server.mjs", "README.md"],
9
+ "engines": { "node": ">=18" },
10
+ "homepage": "https://www.keyframe.it.com/",
11
+ "bugs": { "url": "https://www.keyframe.it.com/" },
12
+ "author": "Keyframe.it",
13
+ "license": "MIT",
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "modelcontextprotocol",
18
+ "mcp-server",
19
+ "claude",
20
+ "animation",
21
+ "2d-animation",
22
+ "skeletal-animation",
23
+ "spine",
24
+ "pixi-spine",
25
+ "game-development",
26
+ "gamedev",
27
+ "sprite",
28
+ "slot-symbols",
29
+ "keyframe"
30
+ ],
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.12.0",
33
+ "playwright-core": "^1.49.0"
34
+ }
35
+ }
package/server.mjs ADDED
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ // keyframe-mcp — MCP server that drives the Keyframe.it editor through its window.keyframe
3
+ // scripting API. Launches the user's installed Chrome (playwright-core `channel: 'chrome'`,
4
+ // no browser download) and proxies API calls as tools. Results carrying an image data URL
5
+ // (renderFrame, renderClip, getAssetPng) come back as real MCP image content, so the model
6
+ // SEES the art, not a base64 wall.
7
+ //
8
+ // Env: KEYFRAME_URL (default https://www.keyframe.it.com/), KEYFRAME_HEADLESS=0 to watch.
9
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
10
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
11
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
12
+ import { chromium } from 'playwright-core'
13
+
14
+ const DEFAULT_URL = process.env.KEYFRAME_URL || 'https://www.keyframe.it.com/'
15
+ let browser = null
16
+ let page = null
17
+
18
+ async function ensurePage(url) {
19
+ if (page && !page.isClosed()) return page
20
+ if (!browser || !browser.isConnected()) {
21
+ browser = await chromium.launch({
22
+ channel: 'chrome',
23
+ headless: process.env.KEYFRAME_HEADLESS !== '0',
24
+ })
25
+ }
26
+ page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
27
+ await page.goto(url || DEFAULT_URL, { waitUntil: 'domcontentloaded' })
28
+ await page.waitForFunction(() => !!globalThis.keyframe, undefined, { timeout: 30000 })
29
+ return page
30
+ }
31
+
32
+ const text = (s) => ({ content: [{ type: 'text', text: typeof s === 'string' ? s : JSON.stringify(s, null, 1) }] })
33
+
34
+ /** JSON result → MCP content; a data-URL image field becomes real image content. */
35
+ function toContent(result) {
36
+ if (result && typeof result === 'object' && typeof result.dataUrl === 'string' && result.dataUrl.startsWith('data:image/')) {
37
+ const m = result.dataUrl.match(/^data:(image\/[a-z+]+);base64,(.*)$/s)
38
+ if (m) {
39
+ const { dataUrl, ...rest } = result
40
+ return {
41
+ content: [
42
+ { type: 'image', mimeType: m[1], data: m[2] },
43
+ { type: 'text', text: JSON.stringify({ ...rest, dataUrl: `(shown above, ${Math.round(m[2].length / 1366)}kB)` }, null, 1) },
44
+ ],
45
+ }
46
+ }
47
+ }
48
+ return text(result)
49
+ }
50
+
51
+ const TOOLS = [
52
+ {
53
+ name: 'keyframe_open',
54
+ description:
55
+ 'Open the Keyframe.it editor (launches Chrome on first use; reuses the tab afterwards). Optional url overrides the default (e.g. a local dev server). Returns the editor overview. Call this first.',
56
+ inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'editor URL (default: keyframe.it.com or $KEYFRAME_URL)' } } },
57
+ },
58
+ {
59
+ name: 'keyframe_methods',
60
+ description: 'List every window.keyframe API method with a one-line description. Call once after keyframe_open, before other calls.',
61
+ inputSchema: { type: 'object', properties: {} },
62
+ },
63
+ {
64
+ name: 'keyframe_call',
65
+ description:
66
+ "Call a window.keyframe API method by name, e.g. {method: 'getState'} or {method: 'poseBone', args: ['armL', {rotation: 20}]}. Results that contain an image (renderFrame, renderClip, getAssetPng) are returned as an actual image. Read keyframe_methods (and the matching recipes() entry for multi-step tasks) first.",
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ method: { type: 'string', description: 'API method name, e.g. getState, applyPose, renderFrame' },
71
+ args: { type: 'array', description: 'positional arguments for the method (JSON values)' },
72
+ },
73
+ required: ['method'],
74
+ },
75
+ },
76
+ {
77
+ name: 'keyframe_screenshot',
78
+ description: 'Screenshot the whole editor UI (rarely needed — prefer keyframe_call renderFrame/getAssetPng for art, getState for data).',
79
+ inputSchema: { type: 'object', properties: {} },
80
+ },
81
+ ]
82
+
83
+ const server = new Server({ name: 'keyframe-mcp', version: '0.1.0' }, { capabilities: { tools: {} } })
84
+
85
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
86
+
87
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
88
+ const { name, arguments: args = {} } = req.params
89
+ try {
90
+ if (name === 'keyframe_open') {
91
+ const p = await ensurePage(args.url)
92
+ const overview = await p.evaluate(() => globalThis.keyframe.describe())
93
+ return text(overview)
94
+ }
95
+ if (name === 'keyframe_methods') {
96
+ const p = await ensurePage()
97
+ return text(await p.evaluate(() => globalThis.keyframe.help()))
98
+ }
99
+ if (name === 'keyframe_call') {
100
+ const p = await ensurePage()
101
+ const result = await p.evaluate(async ({ method, callArgs }) => {
102
+ const fn = globalThis.keyframe?.[method]
103
+ if (typeof fn !== 'function') {
104
+ return { ok: false, error: `no method "${method}" — see keyframe_methods` }
105
+ }
106
+ try {
107
+ const r = await fn.apply(globalThis.keyframe, callArgs)
108
+ return r === undefined ? { ok: true } : r
109
+ } catch (e) {
110
+ return { ok: false, error: String(e?.message ?? e) }
111
+ }
112
+ }, { method: args.method, callArgs: Array.isArray(args.args) ? args.args : [] })
113
+ return toContent(result)
114
+ }
115
+ if (name === 'keyframe_screenshot') {
116
+ const p = await ensurePage()
117
+ const buf = await p.screenshot({ type: 'png' })
118
+ return { content: [{ type: 'image', mimeType: 'image/png', data: buf.toString('base64') }] }
119
+ }
120
+ return { ...text(`unknown tool "${name}"`), isError: true }
121
+ } catch (e) {
122
+ return { ...text(`error: ${String(e?.message ?? e)}`), isError: true }
123
+ }
124
+ })
125
+
126
+ process.on('SIGINT', async () => {
127
+ await browser?.close().catch(() => {})
128
+ process.exit(0)
129
+ })
130
+
131
+ await server.connect(new StdioServerTransport())