keyframe-mcp 0.3.0 → 0.5.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 +47 -1
- package/package.json +2 -2
- package/runner.mjs +32 -0
- package/server.mjs +1 -1
- package/session.mjs +35 -3
- package/tools.mjs +40 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# keyframe-mcp
|
|
2
2
|
|
|
3
|
-
Create, inspect and export 2D skeletal animations in [Keyframe.it](https://www.keyframe.it.com/),
|
|
3
|
+
Create, inspect and export 2D skeletal animations in [Keyframe.it](https://www.keyframe.it.com/), rig, animate, play-test and export pixel-art sprites in [Sprite Puppeteer](https://www.keyframe.it.com/sprite-puppeteer), and rig and animate 3D models in [MeshForge](https://www.keyframe.it.com/meshforge), from any MCP client.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -22,6 +22,8 @@ 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.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.
|
|
26
|
+
|
|
25
27
|
## Workflow
|
|
26
28
|
|
|
27
29
|
1. `keyframe_open` resumes a dedicated Chrome profile and waits for saved projects to restore.
|
|
@@ -96,6 +98,50 @@ Example move for the sample golem (`puppeteer_open {sample:"golem"}` first):
|
|
|
96
98
|
|
|
97
99
|
The page's API is `window.spritePuppeteer`; any browser automation can call it directly. See [llms.txt](https://www.keyframe.it.com/llms.txt).
|
|
98
100
|
|
|
101
|
+
## MeshForge
|
|
102
|
+
|
|
103
|
+
[MeshForge](https://www.keyframe.it.com/meshforge) turns images into 3D models (on the user's own fal.ai key) and rigs, animates and renders them in the browser. It opens as a third page in the same connector profile; a new profile starts with an example orc (rigged, 18 animations). Positions and directions are in **character space**: +x the character's right, +y up from the floor, +z the way it faces, in metres.
|
|
104
|
+
|
|
105
|
+
Animate a rigged model:
|
|
106
|
+
|
|
107
|
+
1. `meshforge_open` (optionally `{model:"Orc"}`), `meshforge_models`, `meshforge_open_model`, or `meshforge_import_model {path:"/abs/path/hero.glb"}`.
|
|
108
|
+
2. Read `meshforge_animation_guide` once: it explains the spec and lists the skeleton with each bone's rest aim.
|
|
109
|
+
3. `meshforge_build_animation` with keys whose bones take `{aim:[x,y,z]}` (point the bone that way), `{rot:{yaw,pitch,roll}}` (degrees from rest) or `{rest:true}`. Feet stay on the floor automatically; `root.move[1]` is jump height.
|
|
110
|
+
4. Judge it with `meshforge_contact_sheet` (front and side) and the measurements in the result (`feetToFloor`, `footSlide`, `loopGap`); refine with `replace:true`.
|
|
111
|
+
5. `meshforge_save` keeps it in the library; `meshforge_export` writes a .glb.
|
|
112
|
+
|
|
113
|
+
Rig an unrigged model (free, no API key):
|
|
114
|
+
|
|
115
|
+
0. Over ~150k triangles? `meshforge_reduce {triangles:40000}` then `meshforge_save {asNew:true}` (free; UVs, textures and weights carry over).
|
|
116
|
+
1. `meshforge_start_rig {template:"humanoid"}` (or humanoid_tail, quadruped, chain) fits a skeleton to the mesh.
|
|
117
|
+
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.
|
|
118
|
+
3. Props: `meshforge_add_prop {name:"axe", parent:"handR", select:{capsule:{a:[…],b:[…],radius:0.08}}}`. Preview a selection first with `meshforge_render_rig {select:…}` (orange); added props show purple.
|
|
119
|
+
4. `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".
|
|
120
|
+
5. `meshforge_preset_animation {kind:"walk"}` (or run, idle) adds a free cycle fitted to the rig; tune with speed, stride, arms, bounce, tail. Then save.
|
|
121
|
+
|
|
122
|
+
Example (the example orc, `meshforge_open {model:"Orc"}` first):
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{"spec":{"name":"chop","duration":1.2,"keys":[{"t":0,"bones":{"upper_armR":{"rest":true},"forearmR":{"rest":true},"spine1":{"rest":true}}},{"t":0.45,"ease":"easeOut","bones":{"upper_armR":{"aim":[0.3,1,-0.4]},"forearmR":{"aim":[0,0.3,-1]},"spine1":{"rot":{"pitch":-12,"yaw":-15}}}},{"t":0.65,"ease":"easeIn","bones":{"upper_armR":{"aim":[0.2,-0.3,1]},"forearmR":{"aim":[0,-0.8,1]},"spine1":{"rot":{"pitch":20,"yaw":10}}}},{"t":1.2,"bones":{"upper_armR":{"rest":true},"forearmR":{"rest":true},"spine1":{"rest":true}}}]}}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
| Tool | Purpose |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `meshforge_open` | Open MeshForge, optionally a library model |
|
|
131
|
+
| `meshforge_methods` / `meshforge_call` | Every scripting method, and a generic call |
|
|
132
|
+
| `meshforge_state` / `meshforge_models` / `meshforge_open_model` | The open model, the library, opening one |
|
|
133
|
+
| `meshforge_import_model` | A .glb from disk into the library |
|
|
134
|
+
| `meshforge_generate` / `meshforge_wait` | Image → 3D on the user's fal.ai key (paid: needs `confirmCost:true`) |
|
|
135
|
+
| `meshforge_skeleton` / `meshforge_animation_guide` | Bones in character space; how to write a spec |
|
|
136
|
+
| `meshforge_build_animation` | Keyframe spec → animation, with measurements |
|
|
137
|
+
| `meshforge_contact_sheet` / `meshforge_render_frame` / `meshforge_inspect` | Frames as images; numbers |
|
|
138
|
+
| `meshforge_start_rig` / `meshforge_render_rig` / `meshforge_set_joints` / `meshforge_bind_rig` | Rigging in the browser |
|
|
139
|
+
| `meshforge_add_prop` | A rigid bone for a held weapon or shield (draft or bound rig) |
|
|
140
|
+
| `meshforge_save` / `meshforge_export` | Library entry; .glb file |
|
|
141
|
+
| `meshforge_screenshot` / `meshforge_open_result` | The page, and handoff to visible Chrome |
|
|
142
|
+
|
|
143
|
+
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`.
|
|
144
|
+
|
|
99
145
|
## Storage and handoff
|
|
100
146
|
|
|
101
147
|
The connector uses **its own Chrome profile**, not existing personal Chrome tabs. Projects belong to that profile and site origin. Export project JSON and import it in your usual browser to transfer work. Existing projects from version 0.1.0's temporary browser profile cannot be recovered unless previously exported.
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keyframe-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"mcpName": "io.github.kendall8388/keyframe",
|
|
5
5
|
"repository": { "type": "git", "url": "https://github.com/kendall8388/Keyframe-it.git", "directory": "mcp" },
|
|
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,
|
|
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.",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
9
|
"keyframe-mcp": "server.mjs"
|
package/runner.mjs
CHANGED
|
@@ -26,6 +26,15 @@ export async function artDataUrl(path) {
|
|
|
26
26
|
if (info.size > ART_MAX) throw new Error('That file is over 40 MB')
|
|
27
27
|
return `data:${type};base64,${(await readFile(path)).toString('base64')}`
|
|
28
28
|
}
|
|
29
|
+
/* MeshForge imports .glb models from the user's disk */
|
|
30
|
+
const GLB_MAX = 200 * 1024 * 1024
|
|
31
|
+
export async function glbDataUrl(path) {
|
|
32
|
+
if (extname(path).toLowerCase() !== '.glb') throw new Error('MeshForge imports .glb files')
|
|
33
|
+
const info = await stat(path)
|
|
34
|
+
if (!info.isFile()) throw new Error(`${path} is not a file`)
|
|
35
|
+
if (info.size > GLB_MAX) throw new Error('That file is over 200 MB')
|
|
36
|
+
return `data:model/gltf-binary;base64,${(await readFile(path)).toString('base64')}`
|
|
37
|
+
}
|
|
29
38
|
export class ToolRunner {
|
|
30
39
|
constructor(session, outputDir = process.env.KEYFRAME_EXPORT_DIR || join(homedir(), '.keyframe-mcp', 'exports')) {
|
|
31
40
|
this.session = session
|
|
@@ -44,6 +53,7 @@ export class ToolRunner {
|
|
|
44
53
|
if (!definition) throw new Error(`Unknown tool: ${name}`)
|
|
45
54
|
if (!definition.validate(args)) throw new Error(`Invalid arguments: ${ajv.errorsText(definition.validate.errors)}`)
|
|
46
55
|
if (definition.target === 'puppeteer') return this.dispatchPuppeteer(name, definition, args)
|
|
56
|
+
if (definition.target === 'meshforge') return this.dispatchMeshforge(name, definition, args)
|
|
47
57
|
const page = await this.session.open(name === 'keyframe_open' ? args.url : undefined)
|
|
48
58
|
if (name === 'keyframe_open_result') return toContent(await this.session.handoff())
|
|
49
59
|
if (name === 'keyframe_screenshot') return { content: [{ type: 'image', mimeType: 'image/png', data: (await page.screenshot({ type: 'png' })).toString('base64') }] }
|
|
@@ -76,6 +86,28 @@ export class ToolRunner {
|
|
|
76
86
|
if (result?.artifact && result.ok !== false) return this.writeArtifact(result)
|
|
77
87
|
return toContent(name === 'puppeteer_open' ? { ...result, profile: this.session.profile, visible: !this.session.headless, handoffTool: 'puppeteer_open_result' } : result)
|
|
78
88
|
}
|
|
89
|
+
/* MeshForge: a third page in the same profile, driven through window.meshforge. Its saves write a
|
|
90
|
+
whole model into the library, so they happen on meshforge_save, on handoff and on close — not
|
|
91
|
+
after every call (results say when there are unsaved changes). */
|
|
92
|
+
async dispatchMeshforge(name, definition, args) {
|
|
93
|
+
const page = await this.session.openMeshforge()
|
|
94
|
+
if (name === 'meshforge_open_result') return toContent(await this.session.meshforgeHandoff())
|
|
95
|
+
if (name === 'meshforge_screenshot') return { content: [{ type: 'image', mimeType: 'image/png', data: (await page.screenshot({ type: 'png' })).toString('base64') }] }
|
|
96
|
+
let method = definition.method, callArgs
|
|
97
|
+
if (name === 'meshforge_call') { method = args.method; callArgs = args.args ?? [] }
|
|
98
|
+
else if (name === 'meshforge_open') { method = args.model ? 'openModel' : 'getState'; callArgs = args.model ? [args.model] : [] }
|
|
99
|
+
else if (name === 'meshforge_import_model') { method = 'importModel'; callArgs = [await glbDataUrl(resolve(args.path)), { name: args.name || basename(args.path) }] }
|
|
100
|
+
else if (name === 'meshforge_generate') { method = 'generateModel'; callArgs = [{ image: await artDataUrl(resolve(args.image)), name: basename(args.image), model: args.model, options: args.options, confirmCost: args.confirmCost }] }
|
|
101
|
+
else callArgs = definition.args(args)
|
|
102
|
+
const result = await this.session.invokeMeshforge(method, callArgs)
|
|
103
|
+
if (result?.artifact && result.ok !== false) return this.writeArtifact(result)
|
|
104
|
+
let out = result
|
|
105
|
+
if (result && typeof result === 'object' && !Array.isArray(result) && result.ok !== false && !result.dataUrl) {
|
|
106
|
+
const info = await this.session.invokeMeshforge('sessionInfo')
|
|
107
|
+
if (info?.dirty) out = { ...result, unsaved: 'Changes are in the viewer only: meshforge_save keeps them in the library.' }
|
|
108
|
+
}
|
|
109
|
+
return toContent(name === 'meshforge_open' ? { ...out, profile: this.session.profile, visible: !this.session.headless, handoffTool: 'meshforge_open_result' } : out)
|
|
110
|
+
}
|
|
79
111
|
async writeArtifact(result) {
|
|
80
112
|
const artifact = result.artifact
|
|
81
113
|
const match = artifact.dataUrl?.match(/^data:([^;,]*)(?:;[^,]*)?;base64,(.*)$/s)
|
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.5.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/session.mjs
CHANGED
|
@@ -14,7 +14,7 @@ function callApi(page, global, method, args) {
|
|
|
14
14
|
catch (e) { return { ok: false, error: String(e?.message ?? e) } }
|
|
15
15
|
}, { global, method, args })
|
|
16
16
|
}
|
|
17
|
-
/* One persistent Chrome profile with up to
|
|
17
|
+
/* One persistent Chrome profile with up to three pages: the Keyframe.it editor, Sprite Puppeteer and MeshForge. */
|
|
18
18
|
export class EditorSession {
|
|
19
19
|
constructor(chromium, env = process.env) {
|
|
20
20
|
this.chromium = chromium
|
|
@@ -24,11 +24,12 @@ export class EditorSession {
|
|
|
24
24
|
this.context = null
|
|
25
25
|
this.page = null
|
|
26
26
|
this.pp = null
|
|
27
|
+
this.mf = null
|
|
27
28
|
}
|
|
28
29
|
async ensureContext() {
|
|
29
30
|
if (!this.context) {
|
|
30
31
|
this.context = await this.chromium.launchPersistentContext(this.profile, { channel: 'chrome', headless: this.headless, viewport: { width: 1440, height: 900 } })
|
|
31
|
-
this.context.on('close', () => { this.context = null; this.page = null; this.pp = null })
|
|
32
|
+
this.context.on('close', () => { this.context = null; this.page = null; this.pp = null; this.mf = null })
|
|
32
33
|
}
|
|
33
34
|
return this.context
|
|
34
35
|
}
|
|
@@ -89,5 +90,36 @@ export class EditorSession {
|
|
|
89
90
|
await this.pp.bringToFront()
|
|
90
91
|
return { ok: true, ...saved, url: this.pp.url(), visible: true, profile: this.profile, note: 'Sprite Puppeteer is open in the connector\'s Chrome window. Its projects live in this dedicated profile; use puppeteer_export {format:"project"} to move a character to your usual browser.' }
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
+
|
|
94
|
+
/* ---------- MeshForge (same site, /meshforge/) ---------- */
|
|
95
|
+
meshforgeUrl() { return new URL('/meshforge/', this.url).href }
|
|
96
|
+
async openMeshforge() {
|
|
97
|
+
await this.ensureContext()
|
|
98
|
+
const target = this.meshforgeUrl(), base = target.replace(/\/$/, '')
|
|
99
|
+
if (!this.mf || this.mf.isClosed()) this.mf = this.context.pages().find(p => p.url().startsWith(base)) ?? await this.context.newPage()
|
|
100
|
+
if (!this.mf.url().startsWith(base)) await this.mf.goto(target, { waitUntil: 'domcontentloaded' })
|
|
101
|
+
await this.mf.waitForFunction(() => !!globalThis.meshforge, undefined, { timeout: 60000 })
|
|
102
|
+
const status = await this.invokeMeshforge('ready')
|
|
103
|
+
if (status?.ok === false) throw new Error(status.error)
|
|
104
|
+
return this.mf
|
|
105
|
+
}
|
|
106
|
+
invokeMeshforge(method, args = []) { return callApi(this.mf, 'meshforge', method, args) }
|
|
107
|
+
/* MeshForge saves a whole model (a GLB) into its library: only when something changed */
|
|
108
|
+
async saveMeshforge() {
|
|
109
|
+
if (!this.mf || this.mf.isClosed()) return
|
|
110
|
+
const info = await this.invokeMeshforge('sessionInfo')
|
|
111
|
+
if (!info?.dirty) return info
|
|
112
|
+
const saved = await this.invokeMeshforge('saveProject')
|
|
113
|
+
if (saved?.ok === false) throw new Error(saved.error)
|
|
114
|
+
return saved
|
|
115
|
+
}
|
|
116
|
+
async meshforgeHandoff() {
|
|
117
|
+
await this.openMeshforge()
|
|
118
|
+
const saved = await this.saveMeshforge()
|
|
119
|
+
const model = saved?.model?.id || (await this.invokeMeshforge('sessionInfo'))?.model
|
|
120
|
+
if (this.headless) { await this.context.close(); this.headless = false; await this.openMeshforge(); if (model) await this.invokeMeshforge('openModel', [model]) }
|
|
121
|
+
await this.mf.bringToFront()
|
|
122
|
+
return { ok: true, ...saved, url: this.mf.url(), visible: true, profile: this.profile, note: 'MeshForge is open in the connector\'s Chrome window. Its library lives in this dedicated profile; use meshforge_export to take a model to your usual browser (+ Import GLB).' }
|
|
123
|
+
}
|
|
124
|
+
async close() { try { await this.save(); await this.savePuppeteer(); await this.saveMeshforge() } finally { await this.context?.close() } }
|
|
93
125
|
}
|
package/tools.mjs
CHANGED
|
@@ -54,7 +54,7 @@ const ppSettings = { ...object({
|
|
|
54
54
|
outline: { enum: ['off', 'repair', 'add'] }, rotsprite: { enum: ['auto', 'on', 'off'] }, facing: { enum: [1, -1] },
|
|
55
55
|
}), minProperties: 1 }
|
|
56
56
|
definitions.push(
|
|
57
|
-
pp('puppeteer_open', 'Open Sprite Puppeteer (pixel-art sprite rigging, animation and play-testing) in the connector profile; optionally load a sample: "orc" (sprite sheets: side, isometric, top-down) or "golem" (rigged). Returns the character state. Call first.', object({ sample: { enum: ['orc', 'golem'] } })),
|
|
57
|
+
pp('puppeteer_open', 'Open Sprite Puppeteer (pixel-art sprite rigging, animation and play-testing) in the connector profile; optionally load a sample: "orc" or "knight" (sprite sheets: side, isometric, top-down) or "golem" (rigged). Returns the character state. Call first.', object({ sample: { enum: ['orc', 'knight', 'golem'] } })),
|
|
58
58
|
pp('puppeteer_methods', 'List every Sprite Puppeteer scripting method with arguments, plus recipes. Reach methods without a named tool through puppeteer_call.', object(), 'help'),
|
|
59
59
|
pp('puppeteer_call', 'Call any window.spritePuppeteer method with positional arguments. Read puppeteer_methods first. Image results appear inline; exports become files.', object({ method: str, args: { type: 'array' } }, ['method'])),
|
|
60
60
|
pp('puppeteer_state', 'Current character: kind (rigged or sprite-sheet), moves, camera set and direction or views, settings, project.', object(), 'getState'),
|
|
@@ -74,4 +74,43 @@ definitions.push(
|
|
|
74
74
|
pp('puppeteer_screenshot', 'Show the Sprite Puppeteer page. Prefer puppeteer_contact_sheet to judge motion.'),
|
|
75
75
|
pp('puppeteer_open_result', 'Save and show Sprite Puppeteer in visible Chrome with the connector profile, so the user can keep working.'),
|
|
76
76
|
)
|
|
77
|
+
/* ---------- MeshForge (www.keyframe.it.com/meshforge): 3D models — rig, animate, props, sprites ---------- */
|
|
78
|
+
const mf = (name, description, inputSchema = object(), method, args = () => []) => ({ ...tool(name, description, inputSchema, method, args), target: 'meshforge' })
|
|
79
|
+
const vec3 = { type: 'array', items: { type: 'number' }, minItems: 3, maxItems: 3 }
|
|
80
|
+
const views = { type: 'array', minItems: 1, maxItems: 7, items: { enum: ['front', 'threeQuarter', 'side', 'right', 'left', 'back', 'top'] } }
|
|
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 mfKey = object({ t: number(0, 60), ease: { enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'step'] }, root: object({ move: vec3, turn: rot }), bones: { type: 'object', additionalProperties: boneTarget } }, ['t'])
|
|
84
|
+
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
|
+
definitions.push(
|
|
86
|
+
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 })),
|
|
87
|
+
mf('meshforge_methods', 'List every MeshForge scripting method (window.meshforge) with arguments, plus recipes. Reach methods without a named tool through meshforge_call.', object(), 'help'),
|
|
88
|
+
mf('meshforge_call', 'Call any window.meshforge method with positional arguments. Read meshforge_methods first. Images appear inline; exports become files.', object({ method: str, args: { type: 'array' } }, ['method'])),
|
|
89
|
+
mf('meshforge_state', 'The open model (rigged? bone count, height), its animations, what is playing, any rig draft, unsaved changes.', object(), 'getState'),
|
|
90
|
+
mf('meshforge_models', 'The MeshForge library in this connector profile: id, name, label (how it was made), status.', object(), 'listModels'),
|
|
91
|
+
mf('meshforge_open_model', 'Open a library model in the viewer by id or name.', object({ model: str }, ['model']), 'openModel', a => [a.model]),
|
|
92
|
+
mf('meshforge_import_model', 'Add a .glb from the local disk to the library and open it.', object({ path: str, name: str }, ['path'])),
|
|
93
|
+
mf('meshforge_generate', 'Image → textured 3D model on the user\'s own fal.ai key (PAID unless MeshForge is in demo mode: ask the user first and pass confirmCost:true). image: a local PNG/JPEG/WebP path. Then meshforge_wait.', object({ image: str, model: str, options: { type: 'object' }, confirmCost: bool }, ['image'])),
|
|
94
|
+
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
|
+
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
|
+
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_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
|
+
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
|
+
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]),
|
|
103
|
+
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
|
+
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('meshforge_add_prop', 'Give a held prop (axe, sword, shield, staff) 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.', object({ name: str, parent: str, select: shape, grow: bool }, ['parent', 'select']), 'addProp', a => [a]),
|
|
106
|
+
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
|
+
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
|
+
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 cycle fitted to the open rig (bipeds, four-legged rigs, tails): kind walk | run | idle, with multipliers speed, stride, arms, bounce, tail (1 = default). Returns measurements.', object({ kind: { enum: ['walk', 'run', 'idle'] }, 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]),
|
|
110
|
+
mf('meshforge_reduce', 'Free polygon reduction in the browser to about `triangles` (UVs, textures, rig weights and animations carry over). Then meshforge_save with asNew:true to keep it as a copy.', object({ triangles: integer(500, 2000000) }, ['triangles']), 'reduceModel', a => [a]),
|
|
111
|
+
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]),
|
|
112
|
+
mf('meshforge_export', 'Write the open model with every animation as a .glb to disk.', object({ format: { enum: ['glb'] } }), 'exportFile', a => [{ format: a.format || 'glb' }]),
|
|
113
|
+
mf('meshforge_screenshot', 'Show the MeshForge page. Prefer meshforge_contact_sheet or meshforge_render_rig to judge work.'),
|
|
114
|
+
mf('meshforge_open_result', 'Save (if changed) and show MeshForge in visible Chrome with the connector profile, so the user can keep working.'),
|
|
115
|
+
)
|
|
77
116
|
export const advertisedTools = definitions.map(({ method, args, target, ...definition }) => definition)
|