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.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/package.json +35 -0
- 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())
|