keyframe-mcp 0.6.0 → 0.8.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/README.md +15 -6
- package/package.json +1 -1
- package/server.mjs +1 -1
- package/tools.mjs +15 -9
package/README.md
CHANGED
|
@@ -22,6 +22,10 @@ Version 0.2.1 adds ownership metadata for the official MCP Registry, where the s
|
|
|
22
22
|
|
|
23
23
|
Version 0.3.0 adds 18 `puppeteer_*` tools for Sprite Puppeteer: sample and your own characters, camera sets and directions, contact sheets, keyframed moves, and sheet, Godot, Unity, GameMaker, PNG-frame, Spine, GIF and project exports.
|
|
24
24
|
|
|
25
|
+
Version 0.8.0 adds `meshforge_add_part`, `meshforge_set_clip_parts` and `meshforge_split_objects` (33 `meshforge_*` tools in all). `meshforge_add_part` puts another library model on the open one, such as a tool in a hand, a pack on the back or a hat, riding that joint in every animation and saved with the model; `meshforge_set_clip_parts` picks which parts each animation shows, so one model can hold an axe in Chop, a pickaxe in Mine and a load in Carry. Humanoid rigs from `meshforge_start_rig` now get finger and thumb bones and fit T-poses; animation keys take `hands` (open, relaxed, grip, fist) and bones `{curl}`. `meshforge_split_objects` separates a generation of several props in one mesh into its own library models, filed under the original. `meshforge_remesh` gains `quads`, a free quad remesh (Instant Meshes, run in the browser) whose quads follow the shape, and `meshforge_export` gains `obj-quads`.
|
|
26
|
+
|
|
27
|
+
Version 0.7.0 adds `meshforge_free_arm` (30 `meshforge_*` tools in all): on a rig draft it cuts an arm the model has fused to its body free along the crease and closes both sides, so the arm can be raised without dragging the body. `meshforge_add_prop` can move a prop that stands apart into the hand first (`hold: "hang"` for a lantern or a bag, or `holdAt`), and the skeleton is now fitted to the body alone, ignoring pieces standing beside it.
|
|
28
|
+
|
|
25
29
|
Version 0.6.0 adds `meshforge_remesh` (29 `meshforge_*` tools in all): a free remesh that rebuilds the model as an even mesh with new UVs and its texture baked across. `meshforge_reduce` can also scale textures down (`maxTexture`), and the motion presets gain actions: punch, claw swipe, kick, hit reaction, jump, death and taunt. Through `meshforge_call` there are rig markers (`setRigMarkers`), a stretch check (`stretchCheck`) and a local weight smooth (`smoothWeightsAt`).
|
|
26
30
|
|
|
27
31
|
Version 0.5.0 adds four more `meshforge_*` tools (28 in all): a weight fix, a flex test, free walk/run/idle cycles and free polygon reduction; binding now uses bone heat, like Blender's automatic weights, and there is a humanoid-with-a-tail skeleton. Version 0.4.0 added the first 24: rig a 3D model in the browser (template skeleton, joints read off a measuring grid, automatic skin weights), give held props their own rigid bone, animate from keyframe specs written in character space, judge motion from contact sheets and measurements, save to the library and export GLB.
|
|
@@ -114,12 +118,13 @@ Animate a rigged model:
|
|
|
114
118
|
|
|
115
119
|
Rig an unrigged model (free, no API key):
|
|
116
120
|
|
|
117
|
-
0. Over ~150k triangles? `meshforge_reduce {triangles:40000}` then `meshforge_save {asNew:true}` (free; UVs, textures and weights carry over). A messy generated surface? `meshforge_remesh {
|
|
121
|
+
0. Over ~150k triangles? `meshforge_reduce {triangles:40000}` then `meshforge_save {asNew:true}` (free; UVs, textures and weights carry over). A messy generated surface? `meshforge_remesh {quads:12000}` rebuilds it as quads that follow the shape (or `{triangles:30000}` as even triangles) with a baked texture.
|
|
118
122
|
1. `meshforge_start_rig {template:"humanoid"}` (or humanoid_tail, quadruped, chain) fits a skeleton to the mesh.
|
|
119
123
|
2. `meshforge_render_rig` shows the joints over a measuring grid from the front and side. Read the right coordinates off the grid and fix joints with `meshforge_set_joints {joints:{forearmR:[0.55,0.74,0.05]}}` (mirrored by default). Joints belong inside the body.
|
|
120
|
-
3.
|
|
121
|
-
4. `
|
|
122
|
-
5. `
|
|
124
|
+
3. An arm fused to the body (a sleeve grown into a robe)? `meshforge_free_arm {side:"R"}` cuts it free along the crease so it can be raised cleanly.
|
|
125
|
+
4. Props: `meshforge_add_prop {name:"axe", parent:"handR", select:{capsule:{a:[…],b:[…],radius:0.08}}}`. A prop standing apart, like a lantern on the ground: `{name:"lantern", parent:"handL", select:{piece:[x,y,z]}, hold:"hang"}` moves it into the hand first. Preview a selection with `meshforge_render_rig {select:…}` (orange); added props show purple.
|
|
126
|
+
5. `meshforge_bind_rig` (bone heat, like Blender's automatic weights). If its weight check reports strays, `meshforge_fix_weights`. Then `meshforge_flex_test` and a contact sheet of "Flex test".
|
|
127
|
+
6. `meshforge_preset_animation {kind:"walk"}` (or run, idle, punch, swipe, kick, hit, jump, death, taunt) adds free motion fitted to the rig; tune with speed, stride, arms, bounce, tail. Then save.
|
|
123
128
|
|
|
124
129
|
Example (the example orc, `meshforge_open {model:"Orc"}` first):
|
|
125
130
|
|
|
@@ -138,8 +143,12 @@ Example (the example orc, `meshforge_open {model:"Orc"}` first):
|
|
|
138
143
|
| `meshforge_build_animation` | Keyframe spec → animation, with measurements |
|
|
139
144
|
| `meshforge_contact_sheet` / `meshforge_render_frame` / `meshforge_inspect` | Frames as images; numbers |
|
|
140
145
|
| `meshforge_start_rig` / `meshforge_render_rig` / `meshforge_set_joints` / `meshforge_bind_rig` | Rigging in the browser |
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
146
|
+
| `meshforge_free_arm` | Cut an arm fused to the body free before binding |
|
|
147
|
+
| `meshforge_add_part` | Another library model on this one: a tool in a hand, a pack, a hat |
|
|
148
|
+
| `meshforge_set_clip_parts` | Which parts each animation shows (one model, many tools) |
|
|
149
|
+
| `meshforge_split_objects` | Separate the objects in one generation into their own library models |
|
|
150
|
+
| `meshforge_add_prop` | A rigid bone for a held weapon, shield or lantern (draft or bound rig); can move it into the hand |
|
|
151
|
+
| `meshforge_save` / `meshforge_export` | Library entry; .glb file (or .obj with quads) |
|
|
143
152
|
| `meshforge_screenshot` / `meshforge_open_result` | The page, and handoff to visible Chrome |
|
|
144
153
|
|
|
145
154
|
Unlike the other pages, MeshForge doesn't save after every call (a save writes a whole model): results say `unsaved` until `meshforge_save`, and the connector saves on handoff and shutdown. The page's API is `window.meshforge`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keyframe-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"mcpName": "io.github.kendall8388/keyframe",
|
|
5
5
|
"repository": { "type": "git", "url": "https://github.com/kendall8388/Keyframe-it.git", "directory": "mcp" },
|
|
6
6
|
"description": "MCP server for Keyframe.it — let Claude (or any MCP client) rig, animate and export 2D skeletal animations in the free Keyframe.it editor, rig, animate, play-test and export pixel-art sprites in Sprite Puppeteer, and rig and animate 3D models in MeshForge, by driving them in your browser.",
|
package/server.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { advertisedTools } from './tools.mjs'
|
|
|
10
10
|
|
|
11
11
|
export function createServer(session = new EditorSession(chromium)) {
|
|
12
12
|
const runner = new ToolRunner(session)
|
|
13
|
-
const server = new Server({ name: 'keyframe-mcp', version: '0.
|
|
13
|
+
const server = new Server({ name: 'keyframe-mcp', version: '0.8.0' }, { capabilities: { tools: {}, resources: {} } })
|
|
14
14
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: advertisedTools }))
|
|
15
15
|
server.setRequestHandler(CallToolRequestSchema, req => runner.call(req.params.name, req.params.arguments ?? {}))
|
|
16
16
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: runner.resources() }))
|
package/tools.mjs
CHANGED
|
@@ -79,8 +79,10 @@ const mf = (name, description, inputSchema = object(), method, args = () => [])
|
|
|
79
79
|
const vec3 = { type: 'array', items: { type: 'number' }, minItems: 3, maxItems: 3 }
|
|
80
80
|
const views = { type: 'array', minItems: 1, maxItems: 7, items: { enum: ['front', 'threeQuarter', 'side', 'right', 'left', 'back', 'top'] } }
|
|
81
81
|
const rot = object({ yaw: { type: 'number' }, pitch: { type: 'number' }, roll: { type: 'number' } })
|
|
82
|
-
const boneTarget = { anyOf: [object({ aim: vec3, twist: { type: 'number' } }, ['aim']), object({ rot }, ['rot']), object({ rest: { const: true } }, ['rest'])] }
|
|
83
|
-
const
|
|
82
|
+
const boneTarget = { anyOf: [object({ aim: vec3, twist: { type: 'number' } }, ['aim']), object({ rot }, ['rot']), object({ curl: { type: 'number' } }, ['curl']), object({ rest: { const: true } }, ['rest'])] }
|
|
83
|
+
const handPose = { anyOf: [{ enum: ['open', 'relaxed', 'grip', 'fist'] }, { type: 'number' }] }
|
|
84
|
+
const partList = { type: 'array', items: str, maxItems: 50 }
|
|
85
|
+
const mfKey = object({ t: number(0, 60), ease: { enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'step'] }, root: object({ move: vec3, turn: rot }), hands: object({ R: handPose, L: handPose }), bones: { type: 'object', additionalProperties: boneTarget } }, ['t'])
|
|
84
86
|
const shape = { type: 'object', description: 'Character space: {box:{min,max}} | {sphere:{center,radius}} | {capsule:{a,b,radius}} | {piece:[x,y,z]} | {any:[…]}, optional minus' }
|
|
85
87
|
definitions.push(
|
|
86
88
|
mf('meshforge_open', 'Open MeshForge (3D models: rig in the browser, animate from keyframe specs, prop bones, sprite sheets) in the connector profile, optionally opening a library model by id or name. A new profile starts with the example orc (rigged, 18 clips). Call first.', object({ model: str })),
|
|
@@ -94,23 +96,27 @@ definitions.push(
|
|
|
94
96
|
mf('meshforge_wait', 'Wait for a generation job to finish (up to timeout seconds), then meshforge_open_model.', object({ id: str, timeout: integer(5, 900) }, ['id']), 'waitForModel', a => [a.id, { timeout: a.timeout }]),
|
|
95
97
|
mf('meshforge_skeleton', 'The open model\'s bones: name, parent, role, joint position and rest aim, in character space (+x right, +y up from the floor, +z forward; metres).', object(), 'getSkeleton'),
|
|
96
98
|
mf('meshforge_animation_guide', 'How to write an animation spec (aim/rot/root/ease, grounding, anatomy, timing) plus the skeleton. Read before meshforge_build_animation.', object(), 'animationGuide'),
|
|
97
|
-
mf('meshforge_build_animation', 'Keyframe spec → animation on the open rig, played at once. Bones take {aim:[x,y,z], twist} (point the bone that way, character space), {rot:{yaw,pitch,roll}} (degrees from rest) or {rest:true}. root.move [x,y,z] metres (feet are kept on the floor; y = jump height). Returns warnings and measurements. replace:true overwrites a clip of the same name.', object({
|
|
98
|
-
spec: object({ name: str, duration: number(0.05, 60), loop: bool, fps: integer(5, 60), ground: { enum: ['auto', 'none'] }, keys: { type: 'array', minItems: 1, maxItems: 500, items: mfKey } }, ['name', 'duration', 'keys']), replace: bool }, ['spec']), 'buildAnimation', a => [a.spec, { replace: a.replace }]),
|
|
99
|
+
mf('meshforge_build_animation', 'Keyframe spec → animation on the open rig, played at once. Bones take {aim:[x,y,z], twist} (point the bone that way, character space), {rot:{yaw,pitch,roll}} (degrees from rest), {curl: degrees} (a finger bending toward the palm) or {rest:true}. A key\'s hands {R, L}: open | relaxed | grip | fist | degrees curls every finger of that hand (rigs with finger bones). root.move [x,y,z] metres (feet are kept on the floor; y = jump height). spec.parts: the parts (meshforge_call listParts) this clip shows, the others hidden in it; leave out to show all. Returns warnings and measurements. replace:true overwrites a clip of the same name.', object({
|
|
100
|
+
spec: object({ name: str, duration: number(0.05, 60), loop: bool, fps: integer(5, 60), ground: { enum: ['auto', 'none'] }, parts: partList, keys: { type: 'array', minItems: 1, maxItems: 500, items: mfKey } }, ['name', 'duration', 'keys']), replace: bool }, ['spec']), 'buildAnimation', a => [a.spec, { replace: a.replace }]),
|
|
99
101
|
mf('meshforge_contact_sheet', 'Frames of an animation from the front and side (or other views) in one labelled image: the way to judge motion.', object({ animation: str, samples: integer(2, 16), views, size: integer(96, 384) }), 'renderContactSheet', a => [a]),
|
|
100
102
|
mf('meshforge_render_frame', 'One picture of the open model, optionally at a time in an animation, from a view (default threeQuarter).', object({ animation: str, time: number(0, 60), view: { enum: ['front', 'threeQuarter', 'side', 'right', 'left', 'back', 'top'] }, size: integer(128, 1024) }), 'renderFrame', a => [a]),
|
|
101
103
|
mf('meshforge_inspect', 'Numbers for an animation: feet vs floor, foot sliding, loop gap, which bones move, hips travel.', object({ animation: str, samples: integer(2, 120) }, ['animation']), 'inspectAnimation', a => [a.animation, { samples: a.samples }]),
|
|
102
|
-
mf('meshforge_start_rig', 'Rig the open model in the browser (free): a template skeleton (humanoid, humanoid_tail for lizard/shark folk, quadruped or chain) fitted to the mesh. Over ~150k triangles, meshforge_reduce first. facing: which way the model looks in its own space. Then meshforge_render_rig.', object({ template: { enum: ['humanoid', 'humanoid_tail', 'quadruped', 'chain'] }, facing: { enum: ['+z', '-z', '+x', '-x'] } }), 'startRig', a => [a]),
|
|
104
|
+
mf('meshforge_start_rig', 'Rig the open model in the browser (free): a template skeleton (humanoid, humanoid_tail for lizard/shark folk, quadruped or chain) fitted to the mesh (T- or A-pose). Humanoids get finger and thumb bones where the hands show them (fingers:false to skip). Over ~150k triangles, meshforge_reduce first. facing: which way the model looks in its own space. Then meshforge_render_rig.', object({ template: { enum: ['humanoid', 'humanoid_tail', 'quadruped', 'chain'] }, facing: { enum: ['+z', '-z', '+x', '-x'] }, fingers: bool }), 'startRig', a => [a]),
|
|
103
105
|
mf('meshforge_render_rig', 'The model from the front and side with the draft joints drawn on it over a measuring grid (read coordinates off it). Props show purple; pass select to preview a selection in orange.', object({ views, size: integer(256, 1024), select: shape }), 'renderRig', a => [a]),
|
|
104
106
|
mf('meshforge_set_joints', 'Move draft joints: {jointName: [x, y, z]} in character space. mirror (default true) moves the left/right twin too. Joints belong INSIDE the body.', object({ joints: { type: 'object', minProperties: 1, additionalProperties: vec3 }, mirror: bool }, ['joints']), 'setRigJoints', a => [a.joints, { mirror: a.mirror }]),
|
|
105
|
-
mf('
|
|
107
|
+
mf('meshforge_free_arm', 'On a rig draft, before binding: cut an arm the model has fused to its body (a sleeve grown into a robe, an arm resting on the side) free along the crease, from the armpit down, and close both sides, so the arm can be raised without dragging the body\'s skin. side "L" | "R" (the character\'s own); from: where the cut starts, as a fraction down the upper arm (default 0.35). Leaves the model alone and says why if no clean cut exists. Undo with meshforge_call {method:"restoreArm", args:[{side}]}.', object({ side: { enum: ['L', 'R'] }, from: number(0, 0.9) }, ['side']), 'freeArm', a => [a]),
|
|
108
|
+
mf('meshforge_add_part', 'Put another model from the library on the open one: a tool in a hand (hand_r, hand_l), a pack on the back (back), a hat (head), a pouch at the belt (hip); it rides that joint in every animation. On an unrigged model the only slot is free (stands beside it). It starts at a size that suits the spot (small | auto | large). Adjust or remove with meshforge_call {method:"adjustPart", args:[name, {slot, move:[x,y,z], turn:[x,y,z] degrees, scale}]} / "removePart" / "listParts"; meshforge_save keeps it with the model.', object({ model: str, slot: { enum: ['hand_r', 'hand_l', 'back', 'head', 'hip', 'free'] }, size: { enum: ['small', 'auto', 'large'] } }, ['model']), 'addPart', a => [a]),
|
|
109
|
+
mf('meshforge_set_clip_parts', 'Choose which parts an animation shows, so one model carries every tool and load: an axe only in Chop, a pickaxe only in Mine, logs only in Carry. parts: part names from meshforge_call {method:"listParts"} (each lists shownIn); [] hides them all in that clip; null shows them all again. Then meshforge_save.', object({ animation: str, parts: { anyOf: [partList, { type: 'null' }] } }, ['animation', 'parts']), 'setClipParts', a => [a.animation, a.parts]),
|
|
110
|
+
mf('meshforge_split_objects', 'Find the separate objects in the open model, such as a generation of several props (an axe, a pickaxe, a sack) that came back as one mesh. Without save it lists them left to right as seen from the front: index, triangles, center and size. save:true saves each one (or only the indices in objects) as its own library model filed under the original; names gives their names in the same order. gap: touching | close (default) | loose sets how near loose pieces must be to count as one object. Pieces that touch or overlap are one object.', object({ save: bool, gap: { enum: ['touching', 'close', 'loose'] }, objects: { type: 'array', items: integer(0, 999), minItems: 1, maxItems: 100 }, names: { type: 'array', items: str, maxItems: 100 } }), 'splitObjects', a => [a]),
|
|
111
|
+
mf('meshforge_add_prop', 'Give a prop (axe, sword, shield, staff, lantern) its own rigid bone under parent (usually a hand). Works on a rig draft or on a model already rigged (MeshForge or Meshy). The selection grows along the prop automatically; check with meshforge_render_rig (draft) or meshforge_contact_sheet. On a draft, hold:"hang" first moves a prop that stands apart (a lantern on the ground) into the hand, its top in the fist and the rest hanging below; holdAt:[x,y,z] names the point of the prop to put in the fist instead.', object({ name: str, parent: str, select: shape, grow: bool, hold: { enum: ['hang'] }, holdAt: vec3 }, ['parent', 'select']), 'addProp', a => [a]),
|
|
106
112
|
mf('meshforge_bind_rig', 'Build the bones and automatic skin weights from the draft: method "heat" (bone heat, as Blender\'s automatic weights; default) or "nearest" (quicker). Existing animations are retargeted where the skeletons can be matched. Returns a weight check; then meshforge_flex_test.', object({ method: { enum: ['heat', 'nearest'] } }), 'bindRig', a => [a]),
|
|
107
113
|
mf('meshforge_fix_weights', 'Find skin that follows the wrong limb (a hand moving part of a foot) and hand it to the bone it sits by. Returns what was fixed and a fresh check.', object(), 'fixWeights'),
|
|
108
114
|
mf('meshforge_flex_test', 'Add a temporary clip (not saved) bending each limb, the spine, head and tail in turn; then meshforge_contact_sheet {animation:"Flex test", samples:12} to spot bad weights.', object(), 'flexTest'),
|
|
109
|
-
mf('meshforge_preset_animation', 'Free ready-made motion fitted to the open rig: cycles walk | run | idle (bipeds, four-legged rigs, tails) and actions punch | swipe | kick | hit | jump | death | taunt (two-legged rigs). Multipliers speed, stride, arms (for actions: strength), bounce, tail (1 = default). Returns measurements.', object({ kind: { enum: ['walk', 'run', 'idle', 'punch', 'swipe', 'kick', 'hit', 'jump', 'death', 'taunt'] }, speed: number(0.25, 4), stride: number(0, 3), arms: number(0, 3), bounce: number(0, 3), tail: number(0, 3), name: str }), 'buildPresetAnimation', a => [a]),
|
|
115
|
+
mf('meshforge_preset_animation', 'Free ready-made motion fitted to the open rig: cycles walk | run | idle (bipeds, four-legged rigs, tails) and actions punch | swipe | kick | hit | jump | death | taunt (two-legged rigs). Multipliers speed, stride, arms (for actions: strength), bounce, tail (1 = default). Arms held out at rest (T-pose) hang relaxed; finger rigs get matching hands. parts: the parts this clip shows. Returns measurements.', object({ kind: { enum: ['walk', 'run', 'idle', 'punch', 'swipe', 'kick', 'hit', 'jump', 'death', 'taunt'] }, speed: number(0.25, 4), stride: number(0, 3), arms: number(0, 3), bounce: number(0, 3), tail: number(0, 3), name: str, parts: partList }), 'buildPresetAnimation', a => [a]),
|
|
110
116
|
mf('meshforge_reduce', 'Free polygon reduction in the browser to about `triangles` (UVs, textures, rig weights and animations carry over); maxTexture (e.g. 2048) also scales textures down. Then meshforge_save with asNew:true to keep it as a copy.', object({ triangles: integer(500, 2000000), maxTexture: integer(256, 8192) }, ['triangles']), 'reduceModel', a => [a]),
|
|
111
|
-
mf('meshforge_remesh', 'Free remesh: rebuild the open model as one clean, even mesh
|
|
117
|
+
mf('meshforge_remesh', 'Free remesh: rebuild the open model as one clean, even mesh with fresh UVs and its texture baked across (texSize 1024 | 2048 | 4096). quads (e.g. 12000) lays quads that follow the shape (Instant Meshes: edge loops around limbs and fingers; smaller quads where the surface bends tightly, larger on broad areas, or even:true for one size everywhere; better for rigging); otherwise about `triangles` (default 30000) even triangles. Drops any rig: rig it again after. Then meshforge_save with asNew:true; meshforge_export format obj-quads writes the quads.', object({ quads: integer(500, 60000), even: bool, triangles: integer(1000, 100000), texSize: { enum: [1024, 2048, 4096] } }), 'remeshModel', a => [a]),
|
|
112
118
|
mf('meshforge_save', 'Save the open model (rig and animations) to the MeshForge library. A model saved from MeshForge before is updated; a generated or imported one gets a new entry.', object({ name: str, asNew: bool }), 'saveProject', a => [a]),
|
|
113
|
-
mf('meshforge_export', 'Write the open model with every animation as a
|
|
119
|
+
mf('meshforge_export', 'Write the open model to disk: glb (default) with every animation, or obj-quads, the surface as an OBJ with quads (exact quads after a quad remesh) for Blender and other editors.', object({ format: { enum: ['glb', 'obj-quads'] } }), 'exportFile', a => [{ format: a.format || 'glb' }]),
|
|
114
120
|
mf('meshforge_screenshot', 'Show the MeshForge page. Prefer meshforge_contact_sheet or meshforge_render_rig to judge work.'),
|
|
115
121
|
mf('meshforge_open_result', 'Save (if changed) and show MeshForge in visible Chrome with the connector profile, so the user can keep working.'),
|
|
116
122
|
)
|