@gripforgeai/mcp 0.1.0 → 0.1.2
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 +4 -1
- package/dist/server.js +67 -6
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Or in `.mcp.json`:
|
|
|
23
23
|
}
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Get a free API key
|
|
26
|
+
Get a free API key at https://gripforge.ai/login — the free plan includes **3 API/MCP attaches per month** to try it out (plus 15 in the web Studio). Paid plans from €29/mo for production use.
|
|
27
27
|
|
|
28
28
|
## Install (Grok)
|
|
29
29
|
|
|
@@ -52,6 +52,9 @@ Also works with Cursor, Windsurf and any MCP-compatible client — same
|
|
|
52
52
|
|
|
53
53
|
- `gripforge_attach` — character + prop in, bone-local bind + Three.js / Unity /
|
|
54
54
|
Godot snippets out. Styles: melee, gun, shield, staff (scythe/polearm).
|
|
55
|
+
With `export_glb: true` (+ `out_dir`) it also writes `attached.glb`: the
|
|
56
|
+
character with the fist closed and the prop attached, textures preserved —
|
|
57
|
+
use this for mitten-hand rigs, whose closed fist cannot travel in a JSON bind.
|
|
55
58
|
- `gripforge_formats` — supported formats & options.
|
|
56
59
|
|
|
57
60
|
## Env
|
package/dist/server.js
CHANGED
|
@@ -15,19 +15,38 @@ import { z } from 'zod';
|
|
|
15
15
|
const API_URL = process.env.GRIPFORGE_API_URL ?? 'https://gripforge.ai';
|
|
16
16
|
const API_KEY = process.env.GRIPFORGE_API_KEY;
|
|
17
17
|
const SUPPORTED = ['.glb', '.gltf', '.fbx', '.obj'];
|
|
18
|
-
const server = new McpServer({ name: 'gripforge', version: '0.1.
|
|
18
|
+
const server = new McpServer({ name: 'gripforge', version: '0.1.2' });
|
|
19
19
|
const err = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
20
20
|
server.tool('gripforge_attach', 'Attach a prop (sword, shield, gun, staff/scythe…) onto a rigged character. ' +
|
|
21
21
|
'Finds the hand bone across naming schemes, scales to body height, seats the grip, ' +
|
|
22
|
-
'and returns a bind + ready-to-paste Three.js / Unity / Godot snippets.'
|
|
22
|
+
'and returns a bind + ready-to-paste Three.js / Unity / Godot snippets. ' +
|
|
23
|
+
'Exact placement: add an Empty/locator named "grip" on the handle inside the weapon file — ' +
|
|
24
|
+
'GripForge puts that point in the palm and skips all grip heuristics. ' +
|
|
25
|
+
'Hand closing: rigs WITH finger bones get bind.gripPose rotations (applied by the snippets); ' +
|
|
26
|
+
'rigs WITHOUT finger bones (mitten hands) need export_glb: true + out_dir to receive ' +
|
|
27
|
+
'attached.glb with the fist baked in — the JSON bind alone cannot close a mitten hand.', {
|
|
23
28
|
character_path: z.string().describe('Absolute path to the rigged character (.glb .gltf .fbx .obj)'),
|
|
24
29
|
prop_path: z.string().describe('Absolute path to the prop mesh'),
|
|
25
30
|
style: z.enum(['melee', 'gun', 'shield', 'staff']).optional().describe('Grip style (default: guessed from the filename)'),
|
|
26
31
|
hand: z.enum(['right', 'left']).optional().describe('Hand side (default right)'),
|
|
27
32
|
height_ratio: z.number().min(0.05).max(1.5).optional().describe('Prop size as a fraction of body height'),
|
|
28
33
|
fist: z.number().min(0).max(1).optional().describe('Fist closing amount, 0 open → 1 closed (default 1)'),
|
|
34
|
+
grip_offset: z
|
|
35
|
+
.array(z.number())
|
|
36
|
+
.length(3)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Grip fine-tune [palm, lateral, along-handle] in palm units — overrides the weapon-class defaults'),
|
|
39
|
+
ai_refine: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('Vision-model verification of the grip: renders the fist+weapon, checks the hold, auto-corrects (adds ~10-30s)'),
|
|
43
|
+
export_glb: z
|
|
44
|
+
.boolean()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Bake character + closed fist + prop into one attached.glb (requires out_dir; .glb/.gltf inputs only). ' +
|
|
47
|
+
'Use this for mitten-hand rigs: the closed fist cannot travel in the bind JSON.'),
|
|
29
48
|
out_dir: z.string().optional().describe('Write bind.json + engine snippets into this folder'),
|
|
30
|
-
}, async ({ character_path, prop_path, style, hand, height_ratio, fist, out_dir }) => {
|
|
49
|
+
}, async ({ character_path, prop_path, style, hand, height_ratio, fist, grip_offset, ai_refine, export_glb, out_dir }) => {
|
|
31
50
|
if (!API_KEY) {
|
|
32
51
|
return err('GRIPFORGE_API_KEY missing. Create a free account at ' +
|
|
33
52
|
API_URL +
|
|
@@ -41,6 +60,13 @@ server.tool('gripforge_attach', 'Attach a prop (sword, shield, gun, staff/scythe
|
|
|
41
60
|
return err(`Unsupported format: ${p} — use ${SUPPORTED.join(' ')}`);
|
|
42
61
|
}
|
|
43
62
|
}
|
|
63
|
+
if (charPath === propPath) {
|
|
64
|
+
return err('character_path and prop_path point to the same file. Export the weapon as its own GLB ' +
|
|
65
|
+
'(Blender: select the weapon only, File > Export > glTF, check "Selected Objects") and pass that as prop_path.');
|
|
66
|
+
}
|
|
67
|
+
if (export_glb && !out_dir) {
|
|
68
|
+
return err('export_glb needs out_dir — that is where attached.glb will be written.');
|
|
69
|
+
}
|
|
44
70
|
const form = new FormData();
|
|
45
71
|
form.append('character', new Blob([await readFile(charPath)]), basename(charPath));
|
|
46
72
|
form.append('prop', new Blob([await readFile(propPath)]), basename(propPath));
|
|
@@ -52,12 +78,22 @@ server.tool('gripforge_attach', 'Attach a prop (sword, shield, gun, staff/scythe
|
|
|
52
78
|
form.append('ratio', String(height_ratio));
|
|
53
79
|
if (fist != null)
|
|
54
80
|
form.append('fist', String(fist));
|
|
81
|
+
if (grip_offset)
|
|
82
|
+
form.append('grip_offset', grip_offset.join(','));
|
|
83
|
+
if (ai_refine)
|
|
84
|
+
form.append('refine', 'ai');
|
|
85
|
+
if (export_glb)
|
|
86
|
+
form.append('export', 'glb');
|
|
55
87
|
form.append('fingers', '1');
|
|
56
88
|
let res;
|
|
57
89
|
try {
|
|
58
90
|
res = await fetch(`${API_URL}/api/v1/attach`, {
|
|
59
91
|
method: 'POST',
|
|
60
|
-
headers: {
|
|
92
|
+
headers: {
|
|
93
|
+
'x-api-key': API_KEY,
|
|
94
|
+
'x-gripforge-client': 'mcp',
|
|
95
|
+
'user-agent': 'gripforge-mcp/0.1',
|
|
96
|
+
},
|
|
61
97
|
body: form,
|
|
62
98
|
});
|
|
63
99
|
}
|
|
@@ -67,6 +103,9 @@ server.tool('gripforge_attach', 'Attach a prop (sword, shield, gun, staff/scythe
|
|
|
67
103
|
const data = (await res.json().catch(() => ({})));
|
|
68
104
|
if (res.status === 401)
|
|
69
105
|
return err('Invalid GRIPFORGE_API_KEY — check it on ' + API_URL + '/account#api');
|
|
106
|
+
if (res.status === 403) {
|
|
107
|
+
return err(`MCP access requires a paid plan (indie or studio); the free plan covers the Studio only. Upgrade at ${API_URL}/#pricing`);
|
|
108
|
+
}
|
|
70
109
|
if (res.status === 402) {
|
|
71
110
|
const u = data;
|
|
72
111
|
return err(`Quota exceeded (${u.used}/${u.limit}, plan ${u.plan}) — upgrade at ${API_URL}/#pricing`);
|
|
@@ -75,6 +114,8 @@ server.tool('gripforge_attach', 'Attach a prop (sword, shield, gun, staff/scythe
|
|
|
75
114
|
return err(`Attach failed (${res.status}): ${String(data.error ?? 'unknown error')}`);
|
|
76
115
|
const exportsObj = (data.exports ?? {});
|
|
77
116
|
let wrote = null;
|
|
117
|
+
let glbFile = null;
|
|
118
|
+
let glbNotes = [];
|
|
78
119
|
if (out_dir) {
|
|
79
120
|
const dir = resolve(out_dir);
|
|
80
121
|
await mkdir(dir, { recursive: true });
|
|
@@ -85,15 +126,35 @@ server.tool('gripforge_attach', 'Attach a prop (sword, shield, gun, staff/scythe
|
|
|
85
126
|
await writeFile(join(dir, 'unity.cs.txt'), exportsObj.unity);
|
|
86
127
|
if (exportsObj.godot)
|
|
87
128
|
await writeFile(join(dir, 'godot.gd.txt'), exportsObj.godot);
|
|
129
|
+
const glb = data.glb;
|
|
130
|
+
if (glb?.base64) {
|
|
131
|
+
glbFile = join(dir, glb.filename ?? 'attached.glb');
|
|
132
|
+
await writeFile(glbFile, Buffer.from(glb.base64, 'base64'));
|
|
133
|
+
}
|
|
134
|
+
if (glb?.notes?.length)
|
|
135
|
+
glbNotes = glb.notes;
|
|
88
136
|
wrote = dir;
|
|
89
137
|
}
|
|
90
138
|
const credits = data.credits;
|
|
139
|
+
// Mitten rigs (no real finger bones): the fist is a mesh swap that cannot
|
|
140
|
+
// travel in the bind JSON — steer the caller toward export_glb.
|
|
141
|
+
const bind = data.bind;
|
|
142
|
+
const isMitten = bind?.fingerRig?.generated === true || (bind?.fingerRig && (bind.fingerRig.bones ?? 0) === 0);
|
|
143
|
+
const noPose = !bind?.gripPose || bind.gripPose.length === 0;
|
|
144
|
+
const mittenHint = !glbFile && isMitten && noPose
|
|
145
|
+
? '\nNOTE: this rig has no real finger bones, so the closed fist exists only as a mesh swap ' +
|
|
146
|
+
'and is NOT in the bind JSON — the hand will stay open in your engine. ' +
|
|
147
|
+
'Re-run gripforge_attach with export_glb: true and an out_dir to get attached.glb ' +
|
|
148
|
+
'(character with the fist closed + prop attached, textures preserved).'
|
|
149
|
+
: '';
|
|
91
150
|
return {
|
|
92
151
|
content: [
|
|
93
152
|
{
|
|
94
153
|
type: 'text',
|
|
95
|
-
text: JSON.stringify({ bind: data.bind, confidence: data.confidence, exports: exportsObj, wrote }, null, 2) +
|
|
96
|
-
(credits ? `\ncredits: ${credits.remaining}/${credits.limit} remaining (${credits.plan})` : '')
|
|
154
|
+
text: JSON.stringify({ bind: data.bind, confidence: data.confidence, exports: exportsObj, wrote, glbFile }, null, 2) +
|
|
155
|
+
(credits ? `\ncredits: ${credits.remaining}/${credits.limit} remaining (${credits.plan})` : '') +
|
|
156
|
+
(glbNotes.length ? `\nglb notes:\n- ${glbNotes.join('\n- ')}` : '') +
|
|
157
|
+
mittenHint,
|
|
97
158
|
},
|
|
98
159
|
],
|
|
99
160
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gripforgeai/mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "GripForge MCP server
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "GripForge MCP server \u2014 attach weapons & props to rigged characters from Claude Code, Cursor or any MCP client. Calls the GripForge API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -37,5 +37,6 @@
|
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^20",
|
|
39
39
|
"typescript": "^5"
|
|
40
|
-
}
|
|
41
|
-
|
|
40
|
+
},
|
|
41
|
+
"mcpName": "io.github.gripforgeai/mcp"
|
|
42
|
+
}
|