@gripforgeai/mcp 0.1.10 → 0.1.12
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 +82 -2
- package/dist/ability-tools.js +21 -0
- package/dist/architecture-tools.js +83 -0
- package/dist/asset-production-tools.js +101 -0
- package/dist/creature-rig-tools.js +41 -0
- package/dist/game-concept-tools.js +56 -0
- package/dist/game-creation-tools.js +45 -0
- package/dist/gamekit-deliver-local.js +5 -7
- package/dist/gamekit-tools.js +133 -24
- package/dist/hero-ability-tools.js +48 -0
- package/dist/joystick-kit/godot.js +146 -0
- package/dist/joystick-kit/index.js +32 -0
- package/dist/joystick-kit/threejs.js +93 -0
- package/dist/joystick-kit/unity.js +182 -0
- package/dist/joystick-kit/unreal.js +231 -0
- package/dist/joystick-tools.js +26 -0
- package/dist/look.js +6 -0
- package/dist/map-unreal-export-local.js +285 -0
- package/dist/map-unreal-import-local.js +421 -0
- package/dist/map-unreal-local.js +75 -0
- package/dist/motion-tools.js +50 -0
- package/dist/projectile-tools.js +28 -0
- package/dist/scene-tools.js +79 -13
- package/dist/server-tools.js +3 -3
- package/dist/server.js +139 -13
- package/dist/shield-vfx-tools.js +29 -0
- package/dist/studio-tools-tools.js +106 -0
- package/dist/unreal-export-package.js +212 -0
- package/dist/vfx-project-tools.js +2 -1
- package/dist/weapon-motion-tools.js +34 -0
- package/package.json +6 -1
- package/runtime/unreal-scene-export.py +341 -0
- package/runtime/unreal-scene-import.py +622 -0
- package/runtime/unreal-scene-upload.mjs +7 -0
- package/runtime/unreal_scene_math.py +96 -0
- package/runtime/unreal_spawn_review.py +135 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GripForge
|
|
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
CHANGED
|
@@ -6,7 +6,87 @@ Turn prompts into production-ready game assets — animated characters with thei
|
|
|
6
6
|
|
|
7
7
|
MCP client for the [GripForge](https://gripforge.ai) API — from Claude Code, Cursor, Windsurf, VS Code or any MCP client.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## How to use
|
|
10
|
+
|
|
11
|
+
**Hosted — nothing to install.** Point any remote-capable MCP client at `https://gripforge.ai/mcp`
|
|
12
|
+
with your API key ([create one here](https://gripforge.ai/account)):
|
|
13
|
+
|
|
14
|
+
```jsonc
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"gripforge": {
|
|
18
|
+
"url": "https://gripforge.ai/mcp",
|
|
19
|
+
"headers": { "x-api-key": "gf_..." }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Claude Code**, in one line:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
claude mcp add --transport http gripforge https://gripforge.ai/mcp --header "x-api-key: gf_..."
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Local, via npm** — when the agent must write files into your repository:
|
|
32
|
+
|
|
33
|
+
```jsonc
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"gripforge": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "@gripforgeai/mcp"],
|
|
39
|
+
"env": { "GRIPFORGE_API_KEY": "gf_..." }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Key features
|
|
46
|
+
|
|
47
|
+
- **Characters that move.** Describe one, get a rigged T-pose with an animation set — idle, run,
|
|
48
|
+
attack — ready to drop into a scene.
|
|
49
|
+
- **Weapons in hand.** GripForge finds the hand bone on any rig, scales the prop, closes the fist
|
|
50
|
+
around the grip, and returns the bind plus Three.js, Unity and Godot snippets.
|
|
51
|
+
- **Seamless textures.** Tileable material sets from a prompt or from your own image, upscaled
|
|
52
|
+
without invention.
|
|
53
|
+
- **Terrain and maps.** Playable room graphs, top-down shooter layouts with spawns and lanes,
|
|
54
|
+
greybox reconstruction from references.
|
|
55
|
+
- **VFX and HUD.** Animated effects and interface kits exported for your engine.
|
|
56
|
+
- **Playable game kits.** Assemble modular capabilities — movement, combat, enemies, worlds — into
|
|
57
|
+
a game that runs in the browser, then bind your own assets to it.
|
|
58
|
+
- **One Library.** Everything generated lands in a locker your engine can pull from, and that your
|
|
59
|
+
agent can search by look ("Devil May Cry like", "genshin").
|
|
60
|
+
|
|
61
|
+
## Use cases
|
|
62
|
+
|
|
63
|
+
- *"Attach this sword to my knight, right hand, then export the armed GLB."* — the agent handles the
|
|
64
|
+
bone, the scale and the fist; you get a file that loads.
|
|
65
|
+
- *"Make me a boss for an ashen underworld, with three phases and an arena."* — stats, attacks and
|
|
66
|
+
engine snippets come back together.
|
|
67
|
+
- *"Give this floor a mossy cobblestone texture that tiles."* — a seamless set, saved to the Library.
|
|
68
|
+
- *"Build a playable roguelike slice I can try in the browser."* — a game kit project with its
|
|
69
|
+
assets bound, playable from a link.
|
|
70
|
+
|
|
71
|
+
## FAQ
|
|
72
|
+
|
|
73
|
+
**Do I need to install anything?** No. The hosted endpoint works from any MCP client that speaks
|
|
74
|
+
streamable HTTP. The npm package exists for one reason: letting the agent write files directly into
|
|
75
|
+
your repository.
|
|
76
|
+
|
|
77
|
+
**Which engines are supported?** Unity, Godot, Unreal and Three.js — assets come with the binds and
|
|
78
|
+
snippets each one expects.
|
|
79
|
+
|
|
80
|
+
**How is it billed?** Credits, per generation. Reading the Library, animating an existing character,
|
|
81
|
+
level and map kits cost nothing; generating a character or a weapon costs 10.
|
|
82
|
+
|
|
83
|
+
**Where do my assets live?** In your workspace Library. You can pull them into your repository, push
|
|
84
|
+
your own, and share them with the community catalog.
|
|
85
|
+
|
|
86
|
+
**Can I use my own concept art?** Yes — pass an image and GripForge builds the T-pose sheet from it
|
|
87
|
+
rather than inventing a character.
|
|
88
|
+
|
|
89
|
+
## Hosted endpoint
|
|
10
90
|
|
|
11
91
|
No Node required — point any remote-capable MCP client at:
|
|
12
92
|
|
|
@@ -85,7 +165,7 @@ Also works with Cursor, Windsurf and any MCP-compatible client — same
|
|
|
85
165
|
`command` / `args` / `env` triple.
|
|
86
166
|
|
|
87
167
|
|
|
88
|
-
##
|
|
168
|
+
## Tool reference
|
|
89
169
|
|
|
90
170
|
Hosted HTTP MCP (`https://gripforge.ai/mcp`) is always current. This npm package
|
|
91
171
|
writes files into the repo (`out_dir`), including `gripforge_hud`,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export const ABILITY_TOOL_NAMES = ['gripforge_abilities'];
|
|
3
|
+
export function registerAbilityTools(register, options, schema = z) {
|
|
4
|
+
const shape = { action: schema.enum(['schema', 'example', 'validate', 'export']), target: schema.enum(['threejs', 'godot', 'unity', 'unreal', 'all']).optional(), pack: schema.record(schema.string(), schema.unknown()).optional().describe('Versioned ability pack. Read schema/example first; author a reusable pack then validate it.'), model_asset: schema.string().regex(/^lib_[A-Za-z0-9_-]{8,64}$/).optional().describe('Library model id bound to the example. Does not rig or animate it.') };
|
|
5
|
+
register('gripforge_abilities', { title: 'Abilities / skills · four engines', description: 'combat.abilities: inspect schema, get an editable Chrono Crab example, validate a pack or export executable Three.js, Godot, Unity and Unreal source adapters. Shared costs, cooldowns, conditions, targeting, windup/active/recovery and presentation cues. Native adapters require the game combat backend and asset bindings. Returns relative path/content files; no rig/animation/VFX generation or automatic installation. Persist through gripforge_gamekit_configure config.pack or project data. 0 credits.', inputSchema: shape, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } }, async (args) => {
|
|
6
|
+
const parsed = schema.object(shape).strict().safeParse(args);
|
|
7
|
+
if (!parsed.success)
|
|
8
|
+
return { isError: true, content: [{ type: 'text', text: parsed.error.message }] };
|
|
9
|
+
const key = options.getApiKey();
|
|
10
|
+
if (!key)
|
|
11
|
+
return { isError: true, content: [{ type: 'text', text: 'GripForge API key required.' }] };
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch(options.apiUrl.replace(/\/$/, '') + '/api/v1/abilities', { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': key, 'x-gripforge-client': 'mcp' }, body: JSON.stringify(parsed.data), signal: AbortSignal.timeout(30000) });
|
|
14
|
+
const data = await response.json();
|
|
15
|
+
return { ...(!response.ok ? { isError: true } : {}), content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data };
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
return { isError: true, content: [{ type: 'text', text: e instanceof Error ? e.message : 'Ability request failed' }] };
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { LOOK_DESCRIPTION, LOOK_VALUES } from './look.js';
|
|
3
|
+
export const ARCHITECTURE_TOOL_NAMES = ['gripforge_architecture_schema', 'gripforge_generate_building', 'gripforge_generate_district', 'gripforge_environment_module'];
|
|
4
|
+
/** Hosted and local MCP share the same recipe and durable server workflow. */
|
|
5
|
+
export function registerArchitectureTools(register, options, schema = z) {
|
|
6
|
+
const identifier = schema.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,159}$/);
|
|
7
|
+
const ref = schema.object({ assetId: identifier, revisionId: identifier, fileRole: identifier.optional() }).strict();
|
|
8
|
+
const building = schema.object({
|
|
9
|
+
id: schema.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,47}$/).optional(),
|
|
10
|
+
name: schema.string().min(1).max(100).optional(), prompt: schema.string().min(3).max(300),
|
|
11
|
+
style: schema.enum(['realistic', 'stylized', 'lowpoly', 'handpainted']).optional(),
|
|
12
|
+
look: schema.enum(LOOK_VALUES).optional().describe(`${LOOK_DESCRIPTION} Fills style when style is omitted; toon, anime and pixel add their phrase to the prompt.`),
|
|
13
|
+
use: schema.enum(['residential', 'retail', 'office', 'industrial', 'mixed']).optional(),
|
|
14
|
+
floors: schema.number().int().min(1).max(30).optional(),
|
|
15
|
+
dimensions: schema.object({ width: schema.number().min(3).max(100).optional(), depth: schema.number().min(3).max(100).optional(), height: schema.number().min(3).max(150).optional() }).strict().optional().describe('Maximum footprint and height in metres. Uniform fit preserves proportions; actual dimensions are returned.'),
|
|
16
|
+
roof: schema.enum(['flat', 'pitched']).optional(), facade: schema.string().max(120).optional(),
|
|
17
|
+
polycount: schema.number().int().min(4000).max(80000).optional(),
|
|
18
|
+
concept: ref.optional().describe('Owned PNG/JPEG/WebP revision for Meshy image-to-3D. At most 12 MiB. Mutually exclusive with source.'),
|
|
19
|
+
concepts: schema.array(ref).min(1).max(4).optional().describe('Coherent views of ONE building, front first, for Meshy multi-image-to-3D. Exclusive with concept/source. Review coherence before building.'),
|
|
20
|
+
meshQuality: schema.object({ geometry: schema.enum(['standard', '2k', '4k']).optional(), texture: schema.enum(['2k', '4k', '8k']).optional() }).strict().optional().describe('Meshy 7.1 quality. Defaults standard geometry and 2K PBR. Multi-image supports standard/2k geometry only. Texture resolution is preserved during optimization. Read the updated quote.'),
|
|
21
|
+
source: ref.optional().describe('Reuse an owned static GLB revision instead of paying for this building. The source is never modified.'),
|
|
22
|
+
}).strict();
|
|
23
|
+
const material = schema.record(schema.string(), schema.unknown()).describe('SceneMaterialOverride: color, roughness, metalness, map/normalMap/roughnessMap/metalnessMap as owned immutable SceneAssetRef, repeat:[u,v]. Validated by the shared Scene Engine.');
|
|
24
|
+
const district = schema.object({
|
|
25
|
+
version: schema.literal(1).optional(), name: schema.string().min(1).max(100).optional(),
|
|
26
|
+
buildings: schema.array(building).min(1).max(8).describe('Unique building definitions with distinct ids. Each new model is manufactured once.'),
|
|
27
|
+
repetitions: schema.number().int().min(1).max(4).optional().describe('Instances per definition, without additional model generation cost.'),
|
|
28
|
+
streetWidth: schema.number().min(6).max(30).optional(), sidewalkWidth: schema.number().min(1).max(8).optional(), gap: schema.number().min(1).max(30).optional(),
|
|
29
|
+
materials: schema.object({ road: material.optional(), sidewalk: material.optional() }).strict().optional(),
|
|
30
|
+
}).strict();
|
|
31
|
+
const shared = {
|
|
32
|
+
stage: schema.enum(['plan', 'build']).optional().describe('Default plan is free and does not generate. Read its quote, then explicitly build.'),
|
|
33
|
+
budget: schema.object({ credits: schema.number().nonnegative().optional(), usd: schema.number().nonnegative().optional() }).strict().optional().describe('Required for paid concept/build: maximum GripForge credits OR subscription USD accepted. Use the quote funding currency: insufficient AI allowance falls back to GripForge credits for the whole new job, never both. Use plan.concept_quote for images, plan.quote for Meshy. Provider credits are separate.'),
|
|
34
|
+
workspace_id: schema.string().max(100).optional(),
|
|
35
|
+
idempotency_key: schema.string().regex(/^[a-zA-Z0-9_.:-]{8,160}$/).optional(),
|
|
36
|
+
};
|
|
37
|
+
const moduleRecipe = schema.object({
|
|
38
|
+
module: schema.enum(['pillar', 'arch', 'fountain', 'stairs', 'wall', 'pavement', 'rock', 'cliff', 'ruin', 'bridge', 'watchtower', 'shrine', 'guardian']),
|
|
39
|
+
width: schema.number().min(.3).max(40).optional(), depth: schema.number().min(.3).max(40).optional(), height: schema.number().min(.05).max(20).optional(),
|
|
40
|
+
seed: schema.number().int().min(0).max(2147483646).optional(),
|
|
41
|
+
pattern: schema.enum(['radial', 'botanical']).optional().describe('Pavement only: botanical adds a broad leaf rosette inlay, radial keeps the original restrained rings.'),
|
|
42
|
+
palette: schema.object({ stone: schema.string().regex(/^#[0-9a-f]{6}$/i).optional(), recess: schema.string().regex(/^#[0-9a-f]{6}$/i).optional(), trim: schema.string().regex(/^#[0-9a-f]{6}$/i).optional(), water: schema.string().regex(/^#[0-9a-f]{6}$/i).optional() }).strict().optional(),
|
|
43
|
+
}).strict();
|
|
44
|
+
const definitions = [
|
|
45
|
+
{ name: ARCHITECTURE_TOOL_NAMES[0], kind: 'schema', shape: {}, title: 'Architecture · schema', description: 'Read the reusable Meshy building and district contract, example, limits and plan → build workflow. Separate immutable Library assets, scene instances and shared SceneDocument. No paid generation.' },
|
|
46
|
+
{ name: ARCHITECTURE_TOOL_NAMES[1], kind: 'building', shape: { ...shared, recipe: building,
|
|
47
|
+
stage: schema.enum(['plan', 'concept', 'build']).optional().describe('plan is free: exact prompts and separate image/3D quotes. concept creates reviewable workspace images. First review artistic scene, then isolated views of the SAME building, then build 3D from chosen isolated references.'),
|
|
48
|
+
concept_provider: schema.enum(['openai', 'xai']).optional().describe('Default openai (GPT Image 2.5 Sunburst, high quality). xai explicitly selects Imagine. Provider is pinned in the durable job; never silently falls back. Read the provider-specific concept_quote.'),
|
|
49
|
+
concept_presentation: schema.enum(['scene', 'isolated']).optional().describe('Default scene: artistic concept in its neighbourhood preserving the user atmosphere and lighting. isolated: technical views of the SAME building for Meshy. Keep rich architectural detail in both.'),
|
|
50
|
+
concept_reference: ref.optional().describe('Owned artistic reference to EDIT into new scene/isolated views. Distinct from recipe.concept, which reuses an existing front view without generation. Exclusive with recipe.concept/concepts/source and stage=build.'),
|
|
51
|
+
concept_views: schema.array(schema.enum(['front_right', 'front_left', 'rear_right', 'rear_left'])).min(1).max(4).optional().describe('Unique views starting with front_right. Default one view for scene, three for isolated. Alternate views edit the SAME master. recipe.concept reuses an existing master; concept_reference guides a NEW master.'),
|
|
52
|
+
}, title: 'Generate a building', description: 'Reusable plan → artistic concept → isolated reference views → review → Meshy build workflow. Generate 1–4 coherent images through OpenAI (1536×1024 high quality, default) or Imagine (2K), saved as private Library drafts; alternate views reference one master. Separate explicit budgets for concepts and 3D. Accepts text, owned single/multiple concept views or an owned static GLB. Meshy PBR/geometry quality, bounded geometry and uniform metric fit. Durable jobs return workspace links; poll generation_read, cancel/retry preserves finished steps. Review images before building and the real Studio render before publishing. No guaranteed interiors, collisions or LODs. Never substitutes procedural geometry or automatically replaces a game asset.' },
|
|
53
|
+
{ name: ARCHITECTURE_TOOL_NAMES[2], kind: 'district', shape: { ...shared, recipe: district }, title: 'Generate a district', description: 'Plan then build a straight-street district from 1–8 distinct Meshy buildings, each manufactured once and reused as separate editable instances. Includes road, pavements, spawn, daylight and camera in the shared Map SceneDocument. Optional owned PBR road/pavement maps; otherwise simple solid surfaces. Plan returns layout, provider-credit count and account quote without spending. Build requires a budget and returns a persistent job, then Map Studio link. Private work version requiring visual review; no automatic Community publication or game replacement.' },
|
|
54
|
+
{ name: ARCHITECTURE_TOOL_NAMES[3], kind: 'module', shape: { stage: schema.enum(['inspect', 'build']).optional(), recipe: moduleRecipe, name: schema.string().min(1).max(100).optional(), workspace_id: schema.string().max(100).optional() }, title: 'Create a reusable environment module', description: 'Free parametric 3D masonry, independent of any game: pillar, arch, fountain, stairs, wall, pavement, rock, cliff, ruin, bridge, watchtower, shrine or a stone guardian. inspect (default) returns real generated bounds, triangle count, metric recipe and assembly sockets. build saves a private textured GLB in the Library, with bevelled geometry, baked vertex shading and named stone/recess/trim/water materials. Dimensions are nominal metres; bounds report actual moulding overhang. Ground origin, stairs ascend toward -Z, <=23 cm risers; top/bottom sockets support assembly. Seed and palette are repeatable. Does not place assets, generate arbitrary AI models, publish, or guarantee navigation/collisions in a target game. 0 credits.' },
|
|
55
|
+
];
|
|
56
|
+
for (const tool of definitions)
|
|
57
|
+
register(tool.name, { title: tool.title, description: tool.description, inputSchema: tool.shape,
|
|
58
|
+
annotations: { readOnlyHint: tool.kind === 'schema', destructiveHint: false, idempotentHint: tool.kind === 'schema', openWorldHint: tool.kind !== 'schema' } }, async (args, extra) => {
|
|
59
|
+
const parsed = schema.object(tool.shape).strict().safeParse(args);
|
|
60
|
+
if (!parsed.success)
|
|
61
|
+
return { isError: true, content: [{ type: 'text', text: parsed.error.message }] };
|
|
62
|
+
const key = options.getApiKey();
|
|
63
|
+
if (!key && tool.kind !== 'schema')
|
|
64
|
+
return { isError: true, content: [{ type: 'text', text: 'GripForge API key required.' }] };
|
|
65
|
+
const { workspace_id, ...body } = parsed.data;
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(options.apiUrl.replace(/\/$/, '') + (tool.kind === 'module' ? '/api/v1/environment-modules' : '/api/v1/architecture'), {
|
|
68
|
+
method: tool.kind === 'schema' ? 'GET' : 'POST',
|
|
69
|
+
headers: { 'content-type': 'application/json', 'x-gripforge-client': 'mcp', ...(key ? { 'x-api-key': key } : {}), ...(typeof workspace_id === 'string' ? { 'x-workspace-id': workspace_id } : {}) },
|
|
70
|
+
...(tool.kind === 'schema' ? {} : { body: JSON.stringify(tool.kind === 'module' ? body : { ...body, kind: tool.kind }) }),
|
|
71
|
+
signal: AbortSignal.any([AbortSignal.timeout(60_000), ...(extra?.signal ? [extra.signal] : [])]),
|
|
72
|
+
});
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
for (const field of ['studio_url', 'status_url'])
|
|
75
|
+
if (typeof data[field] === 'string' && data[field].startsWith('/'))
|
|
76
|
+
data[field] = new URL(data[field], options.apiUrl).href;
|
|
77
|
+
return { ...(!response.ok ? { isError: true } : {}), structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Architecture request failed.' }] };
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
/** Shared hosted/npm contract: assets are discovered independently of game modules. */
|
|
3
|
+
export function registerAssetProductionTools(register, options, schema = z) {
|
|
4
|
+
const origin = options.apiUrl.replace(/\/$/, '');
|
|
5
|
+
const workspace = schema.string().max(100).optional().describe('Authorized workspace; defaults to the connected workspace.');
|
|
6
|
+
const result = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], structuredContent: data });
|
|
7
|
+
async function request(path, workspaceId, method = 'GET', body) {
|
|
8
|
+
const key = options.getApiKey();
|
|
9
|
+
if (!key)
|
|
10
|
+
throw Error('GripForge API key required.');
|
|
11
|
+
const response = await fetch(origin + '/api/v1/' + path, { method, headers: { 'x-api-key': key, 'x-gripforge-client': 'mcp', ...(body ? { 'content-type': 'application/json' } : {}), ...(typeof workspaceId === 'string' ? { 'x-workspace-id': workspaceId } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}), signal: AbortSignal.timeout(60000) });
|
|
12
|
+
const data = await response.json();
|
|
13
|
+
if (!response.ok)
|
|
14
|
+
throw Error(String(data.error ?? `HTTP ${response.status}`));
|
|
15
|
+
return data;
|
|
16
|
+
}
|
|
17
|
+
const wrap = (run) => async (args) => {
|
|
18
|
+
try {
|
|
19
|
+
return result(await run(args));
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
return { isError: true, content: [{ type: 'text', text: e instanceof Error ? e.message : String(e) }] };
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
register('gripforge_generate_weapon_catalog', {
|
|
26
|
+
title: 'Generate a weapon catalogue',
|
|
27
|
+
description: 'Plan then manufacture 1–64 original static weapon models using the existing Meshy PBR pipeline. plan is free and returns an exact aggregate quote and plan_hash. build requires that hash, an explicit budget covering the quote and a stable idempotency_key. One durable job, two bounded fabrication lanes, per-weapon checkpoints and billing; cancellation/retry preserves finished work and provider task IDs. Results remain private work assets. Inspect geometry, textures and actual renderer output before publishing. Does not create attack/reload animations, grip bindings, or flexible-chain physics. Poll gripforge_generation_read; result.items contains Library IDs and Studio links.',
|
|
28
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
29
|
+
inputSchema: {
|
|
30
|
+
stage: schema.enum(['plan', 'build']), workspace_id: workspace,
|
|
31
|
+
entries: schema.array(schema.object({ key: schema.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/), name: schema.string().min(1).max(160), prompt: schema.string().min(3).max(600), subtype: schema.string().min(1).max(60), style: schema.enum(['gun', 'melee', 'shield', 'staff']).optional(), polycount: schema.number().int().min(500).max(20000).optional(), tags: schema.array(schema.string().min(1).max(60)).max(16).optional() }).strict()).min(1).max(64),
|
|
32
|
+
plan_hash: schema.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
33
|
+
budget: schema.object({ credits: schema.number().nonnegative().optional(), usd: schema.number().nonnegative().optional() }).strict().optional(),
|
|
34
|
+
idempotency_key: schema.string().regex(/^[a-zA-Z0-9_.:-]{8,160}$/).optional(),
|
|
35
|
+
},
|
|
36
|
+
}, wrap(args => request('weapons/catalog', args.workspace_id, 'POST', args)));
|
|
37
|
+
register('gripforge_generation_quote', {
|
|
38
|
+
title: 'Estimate generation usage before spending',
|
|
39
|
+
description: 'Free quote for a model, concept, texture, rig or animation. Reports the account funding mode, available balance and estimated operation cost. A subscription combines AI and paid asset generation in one monthly limit without also charging generation credits. Quote every missing role, propose the plan, and obtain a generation budget before submitting paid jobs. Estimates cover one operation, not the full concept/rig/review pipeline.',
|
|
40
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
41
|
+
inputSchema: { kind: schema.enum(['model', 'concept', 'texture', 'rig', 'animation']), provider: schema.enum(['meshy', 'tripo', 'xai', 'openai']).optional(), count: schema.number().int().min(1).max(20).optional(), workspace_id: workspace },
|
|
42
|
+
}, wrap(args => {
|
|
43
|
+
const params = new URLSearchParams({ kind: String(args.kind), count: String(args.count ?? 1) });
|
|
44
|
+
if (args.provider)
|
|
45
|
+
params.set('provider', String(args.provider));
|
|
46
|
+
return request(`generation-quote?${params}`, args.workspace_id);
|
|
47
|
+
}));
|
|
48
|
+
register('gripforge_asset_search', {
|
|
49
|
+
title: 'Search workspace and Community assets',
|
|
50
|
+
description: 'Search actual asset models, materials and animations, independently of Game Kit modules. Returns provenance, import requirements and Library ids. Results are ranked best first by fitness for a GripForge game (score/100, grade A–D, reasons: triangle budget for the target, rigged and clean rig, clips, empty-handed characters, attachable weapons, classified, file size, adoption); `best` is the top pick across sources. Prefer grade A/B; read the reasons before choosing a C/D. Search both sources before manufacturing missing roles. An empty kit search does not mean there are no assets. Search is free; never substitutes proxy geometry for finished models.',
|
|
51
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
52
|
+
inputSchema: {
|
|
53
|
+
q: schema.string().max(200).describe('Visual role or style, e.g. sci-fi turret, rock, soldier.'),
|
|
54
|
+
kind: schema.enum(['character', 'fps-arms', 'enemy', 'weapon', 'equipment', 'prop', 'texture', 'skybox', 'vfx', 'animation', 'audio', 'kit']).optional().describe('Optional asset kind.'),
|
|
55
|
+
source: schema.enum(['workspace', 'community', 'both']).optional().describe('Defaults to both.'),
|
|
56
|
+
limit: schema.number().int().min(1).max(48).optional().describe('Results per source; default 12.'),
|
|
57
|
+
offset: schema.number().int().min(0).max(10000).optional().describe('Pagination offset per source; default 0.'),
|
|
58
|
+
target: schema.enum(['mobile', 'desktop']).optional().describe('Platform the game ships on: sets the triangle and file budgets of the rank. Defaults to mobile.'),
|
|
59
|
+
sort: schema.enum(['rank', 'newest']).optional().describe('rank (default): best fit first; newest: most recent first.'),
|
|
60
|
+
workspace_id: workspace,
|
|
61
|
+
},
|
|
62
|
+
}, wrap(async (args) => {
|
|
63
|
+
const limit = Number(args.limit ?? 12), offset = Number(args.offset ?? 0);
|
|
64
|
+
const target = args.target === 'desktop' ? 'desktop' : 'mobile';
|
|
65
|
+
const qs = new URLSearchParams({ q: String(args.q ?? ''), limit: String(limit), offset: String(offset), sort: args.sort === 'newest' ? 'newest' : 'rank', target });
|
|
66
|
+
if (args.kind)
|
|
67
|
+
qs.set('kind', String(args.kind));
|
|
68
|
+
const sources = args.source === 'workspace' ? ['workspace'] : args.source === 'community' ? ['community'] : ['workspace', 'community'];
|
|
69
|
+
const data = await Promise.all(sources.map(async (source) => {
|
|
70
|
+
const response = await request(`${source === 'workspace' ? 'library' : 'community'}?${qs}`, args.workspace_id);
|
|
71
|
+
const items = (Array.isArray(response.items) ? response.items : []).slice(0, limit);
|
|
72
|
+
return { source, total: response.total ?? items.length, items: items.map(item => {
|
|
73
|
+
const meta = (item.meta && typeof item.meta === 'object' ? item.meta : {});
|
|
74
|
+
const rank = (item.rank && typeof item.rank === 'object' ? item.rank : null);
|
|
75
|
+
return { id: item.id, name: item.name, kind: item.kind, filename: item.filename, source,
|
|
76
|
+
score: rank?.score ?? null, grade: rank?.grade ?? null, reasons: (rank?.reasons ?? []).slice(0, 5).map(r => `${r.points > 0 ? '+' : ''}${r.points} ${r.note}`),
|
|
77
|
+
triangles: meta.triangles, tags: meta.tags, rigged: meta.rigged, placeholder: meta.placeholder === true || /proxy_|volume de travail/i.test(String(item.filename) + ' ' + String(item.name)),
|
|
78
|
+
next: source === 'community' ? 'gripforge_asset_clone, then gripforge_library_pull' : 'gripforge_library_get / gripforge_library_pull',
|
|
79
|
+
};
|
|
80
|
+
}) };
|
|
81
|
+
}));
|
|
82
|
+
// The top pick across sources; a workspace copy wins a tie (no credit to take it).
|
|
83
|
+
const best = data.flatMap(d => d.items).filter(i => typeof i.score === 'number' && !i.placeholder)
|
|
84
|
+
.sort((a, b) => b.score - a.score || (a.source === 'workspace' ? -1 : 1))[0] ?? null;
|
|
85
|
+
return { target, best, results: data, note: 'Ranked for a GripForge game (see reasons). Inspect suitability and textures/animations before importing. Downloads alone are not runtime integration.' };
|
|
86
|
+
}));
|
|
87
|
+
register('gripforge_asset_clone', {
|
|
88
|
+
title: 'Copy a Community asset to the workspace',
|
|
89
|
+
description: 'Copy a published Community asset into the connected workspace. Assets and armor sets are free by default. Only an explicit creator price is charged, using paid credits. Repeated takes are not billed again but may create another copy: reuse an existing workspace id when possible. Returns the private Library item and signed file URL. Download it with gripforge_library_pull, then import into the actual game.',
|
|
90
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
91
|
+
inputSchema: { id: schema.string().regex(/^lib_[A-Za-z0-9_-]+$/).describe('Published Community Library id.'), workspace_id: workspace },
|
|
92
|
+
}, wrap(args => request(`community/${encodeURIComponent(String(args.id))}/clone`, args.workspace_id, 'POST')));
|
|
93
|
+
}
|
|
94
|
+
/** 202 response: do not strip job ids or misrepresent a queued model as a Library item. */
|
|
95
|
+
export function assetGenerationOutput(schema = z) {
|
|
96
|
+
return schema.object({
|
|
97
|
+
job: schema.string().optional(), job_id: schema.string().optional(), status: schema.string().optional(), stage: schema.string().optional(),
|
|
98
|
+
status_url: schema.string().optional(), studio_url: schema.string().optional(), library_url: schema.string().optional(),
|
|
99
|
+
item: schema.record(schema.string(), schema.unknown()).optional(), provider: schema.string().optional(), notes: schema.array(schema.string()).optional(),
|
|
100
|
+
}).passthrough();
|
|
101
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export const CREATURE_RIG_TOOL_NAMES = ['gripforge_creature_rig_schema', 'gripforge_creature_analyze', 'gripforge_creature_rig'];
|
|
3
|
+
/** Same contract for hosted MCP and the local client; Blender runs in the durable worker. */
|
|
4
|
+
export function registerCreatureRigTools(register, options, schema = z) {
|
|
5
|
+
const shared = { workspace_id: schema.string().max(100).optional(), name: schema.string().min(1).max(100).optional(), idempotency_key: schema.string().regex(/^[a-zA-Z0-9_.:-]{8,160}$/).optional() };
|
|
6
|
+
const analyze = { ...shared, source_id: schema.string().regex(/^lib_[A-Za-z0-9_-]{8,64}$/), kind: schema.enum(['enemy', 'character']).optional() };
|
|
7
|
+
const rig = { ...shared, analysis_job: schema.string().regex(/^gen_[A-Za-z0-9_-]{8,64}$/), profile: schema.record(schema.string(), schema.unknown()).optional().describe('Optional corrected anatomy in the exact coordinate system returned by analyze. Read the schema and inspect the multiview preview first.'), reviewed_anatomy: schema.boolean().optional().describe('Required: true only after inspecting/correcting the anatomy and its skeleton overlay.'), allow_rerig: schema.boolean().optional().describe('Explicitly create a NEW rigged copy if the source already contains bones. The source is never overwritten.') };
|
|
8
|
+
async function call(action, args, signal) {
|
|
9
|
+
const key = options.getApiKey();
|
|
10
|
+
if (!key && action !== 'schema')
|
|
11
|
+
return { isError: true, content: [{ type: 'text', text: 'GripForge API key required.' }] };
|
|
12
|
+
const { workspace_id, ...body } = args;
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(options.apiUrl.replace(/\/$/, '') + '/api/v1/creature-rigs', {
|
|
15
|
+
method: action === 'schema' ? 'GET' : 'POST', headers: { 'content-type': 'application/json', 'x-gripforge-client': 'mcp', ...(key ? { 'x-api-key': key } : {}), ...(typeof workspace_id === 'string' ? { 'x-workspace-id': workspace_id } : {}) },
|
|
16
|
+
...(action === 'schema' ? {} : { body: JSON.stringify({ action, ...body }) }), signal: AbortSignal.any([AbortSignal.timeout(60_000), ...(signal ? [signal] : [])]),
|
|
17
|
+
});
|
|
18
|
+
const data = await response.json();
|
|
19
|
+
for (const field of ['studio_url', 'status_url'])
|
|
20
|
+
if (typeof data[field] === 'string' && data[field].startsWith('/'))
|
|
21
|
+
data[field] = new URL(data[field], options.apiUrl).href;
|
|
22
|
+
return { ...(!response.ok ? { isError: true } : {}), structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Creature rig request failed.' }] };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const tools = [
|
|
29
|
+
{ name: CREATURE_RIG_TOOL_NAMES[0], action: 'schema', title: 'Creature rig · schema', shape: {}, description: 'Read the reusable creature anatomy/rig workflow, coordinate contract, limits and export formats. Detect anatomy before authoring; source GLB, asset instances and draft rig stay separate. Blender is the worker, GripForge is the review renderer.' },
|
|
30
|
+
{ name: CREATURE_RIG_TOOL_NAMES[1], action: 'analyze', title: 'Creature rig · detect anatomy', shape: analyze, description: 'Queue multiview mesh inspection and vision anatomy detection for an OWNED GLB. Detect actual body, legs, arms, claws, wings or tail without imposing a humanoid/eight-leg template. Returns a persistent job_id. Poll gripforge_generation_read with include_preview=true for the annotated views, editable profile and uncertainty flags. No rig mutation. 0 GripForge credits; configured server vision/Blender required.' },
|
|
31
|
+
{ name: CREATURE_RIG_TOOL_NAMES[2], action: 'rig', title: 'Creature rig · manufacture', shape: rig, description: 'Create a NEW private draft character/enemy from a completed creature analysis and optional corrected anatomy. Blender builds the skeleton, surface-smoothed skin weights and procedural starting/diagnostic clips. Grounded leg chains get in-place walk/run with IK. Returns job_id; poll gripforge_generation_read for the Character Studio link, skinned GLB, editable .blend, anatomy and deformation report. Never automatically replaces or validates the source. Every anatomy proposal requires explicit review; use generation_cancel/retry to control durable work. Finger/facial authoring and finished combat choreography are outside v1. 0 GripForge credits.' },
|
|
32
|
+
];
|
|
33
|
+
for (const tool of tools)
|
|
34
|
+
register(tool.name, { title: tool.title, description: tool.description, inputSchema: tool.shape,
|
|
35
|
+
annotations: { readOnlyHint: tool.action === 'schema', destructiveHint: false, idempotentHint: tool.action === 'schema', openWorldHint: false } }, async (args, extra) => {
|
|
36
|
+
const parsed = schema.object(tool.shape).strict().safeParse(args);
|
|
37
|
+
if (!parsed.success)
|
|
38
|
+
return { isError: true, content: [{ type: 'text', text: parsed.error.message }] };
|
|
39
|
+
return call(tool.action, parsed.data, extra?.signal);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export const GAME_CONCEPT_TOOL_NAMES = ['gripforge_game_concepts'];
|
|
3
|
+
const SLOTS = ['heroes', 'bosses', 'enemies', 'npcs', 'weapons', 'vehicles', 'buildings', 'props', 'textures', 'vfx', 'maps'];
|
|
4
|
+
/**
|
|
5
|
+
* Concepts of a whole game, reused before generated. Hosted and local MCP share the same REST
|
|
6
|
+
* workflow: GET the catalog, POST stage=plan (free), then stage=build with an explicit budget.
|
|
7
|
+
*/
|
|
8
|
+
export function registerGameConceptTools(register, options, schema = z) {
|
|
9
|
+
const counts = schema.object(Object.fromEntries(SLOTS.map((s) => [s, schema.number().int().min(0).max(96).optional()]))).strict();
|
|
10
|
+
const shape = {
|
|
11
|
+
stage: schema.enum(['catalog', 'plan', 'build']).optional().describe('catalog: known games, blueprints (slot counts) and theme packs. plan (default, free): what already exists and is reused, what would be generated, and the price. build: generate the missing concepts, requires budget.'),
|
|
12
|
+
game: schema.string().min(2).max(120).optional().describe('The game the user named, as written ("God of War", "like Dark Souls", "a GTA"). Matched against titles and aliases of the catalog.'),
|
|
13
|
+
custom: schema.object({
|
|
14
|
+
title: schema.string().min(2).max(80),
|
|
15
|
+
blueprint: schema.string().max(60).describe('Closest blueprint from stage=catalog, e.g. third-person-action, open-world-rpg.'),
|
|
16
|
+
pack: schema.string().max(60).describe('Closest theme pack from stage=catalog, e.g. nordic-mythology, cyberpunk.'),
|
|
17
|
+
build: schema.string().min(10).max(300).describe('One sentence describing the original game to make.'),
|
|
18
|
+
}).strict().optional().describe('Only when `game` is not in the catalog: describe the game with the closest blueprint and pack.'),
|
|
19
|
+
counts: counts.optional().describe('Concepts to generate per slot (heroes up to 24, maps up to 6: the first is the whole playfield from above, the others are zones seen through the game camera). Default: what the blueprint needs minus what is already reusable.'),
|
|
20
|
+
hero_kits: schema.boolean().optional().describe('Each hero comes with its ability kit — role, lane, basic attack, passive + Q/W/E/R (type, range, cooldown, cost, effect, VFX, cast animation) — and a sheet of 5 ability icons in its colours (1 more image per hero). Default from the blueprint: on for lane-arena (MOBA). Turn it on for any game whose heroes have abilities (hero shooter, ARPG).'),
|
|
21
|
+
provider: schema.enum(['openai', 'xai']).optional().describe('Default openai (GPT Image 2.5 sunburst: final quality for style bible, heroes and bosses, medium-quality sheets of 6 for the rest).'),
|
|
22
|
+
budget: schema.object({ credits: schema.number().nonnegative().optional(), usd: schema.number().nonnegative().optional() }).strict().optional().describe('Required for build: maximum credits OR USD, at least the plan price.'),
|
|
23
|
+
workspace_id: schema.string().max(100).optional(),
|
|
24
|
+
idempotency_key: schema.string().regex(/^[a-zA-Z0-9_.:-]{8,160}$/).optional(),
|
|
25
|
+
};
|
|
26
|
+
register(GAME_CONCEPT_TOOL_NAMES[0], {
|
|
27
|
+
title: 'Game concepts',
|
|
28
|
+
description: 'Concept art for a whole game, reused before generated. Use it when a user asks for a game "like God of War", "a GTA", "a Zelda": call stage=plan with the game they named. The plan lists concepts that already exist for that game (GripForge community catalog or the workspace) and theme assets already in the library: reuse them first. It then prices the missing concepts: one style bible, heroes and bosses one image each (MOBA and hero games: each hero with its ability kit and ability icons, see hero_kits), enemies, NPCs, weapons, vehicles, buildings, props and VFX in sheets of 6 cut automatically, textures in sheets of 2 turned into seamless 1024 px textures. Maps (counts.maps) are environment paintings, one image each: the level is then built and dressed to match them. Show the plan and price to the user; only call stage=build with a budget after they agree. Concepts are private review images; nothing is sent to 3D. The plan also returns ambient_kits: the kits of the theme\'s atmosphere (weather, lighting, snow tracks) with their config; install them in the game project next to the blueprint\'s gameplay kits.',
|
|
29
|
+
inputSchema: shape,
|
|
30
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
31
|
+
}, async (args, extra) => {
|
|
32
|
+
const parsed = schema.object(shape).strict().safeParse(args);
|
|
33
|
+
if (!parsed.success)
|
|
34
|
+
return { isError: true, content: [{ type: 'text', text: parsed.error.message }] };
|
|
35
|
+
const key = options.getApiKey();
|
|
36
|
+
const { workspace_id, stage = 'plan', ...body } = parsed.data;
|
|
37
|
+
if (!key && stage !== 'catalog')
|
|
38
|
+
return { isError: true, content: [{ type: 'text', text: 'GripForge API key required.' }] };
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(options.apiUrl.replace(/\/$/, '') + '/api/v1/game-concepts', {
|
|
41
|
+
method: stage === 'catalog' ? 'GET' : 'POST',
|
|
42
|
+
headers: { 'content-type': 'application/json', 'x-gripforge-client': 'mcp', ...(key ? { 'x-api-key': key } : {}), ...(typeof workspace_id === 'string' ? { 'x-workspace-id': workspace_id } : {}) },
|
|
43
|
+
...(stage === 'catalog' ? {} : { body: JSON.stringify({ ...body, stage }) }),
|
|
44
|
+
signal: AbortSignal.any([AbortSignal.timeout(60_000), ...(extra?.signal ? [extra.signal] : [])]),
|
|
45
|
+
});
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
for (const field of ['studio_url', 'status_url'])
|
|
48
|
+
if (typeof data[field] === 'string' && data[field].startsWith('/'))
|
|
49
|
+
data[field] = new URL(data[field], options.apiUrl).href;
|
|
50
|
+
return { ...(!response.ok ? { isError: true } : {}), structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Game concepts request failed.' }] };
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export const GAME_CREATION_TOOL_NAMES = ['gripforge_game_creation'];
|
|
3
|
+
export function registerGameCreationTools(register, options, schema = z) {
|
|
4
|
+
const shape = {
|
|
5
|
+
action: schema.enum(['list', 'get', 'assets', 'create', 'select', 'use_reference', 'approve_concept', 'approve_asset', 'quote', 'accept', 'cancel', 'retry', 'dismiss']),
|
|
6
|
+
id: schema.string().max(100).optional(), workspace_id: schema.string().max(100).optional(),
|
|
7
|
+
expected_revision: schema.number().int().positive().optional(), prompt: schema.string().max(2000).optional(),
|
|
8
|
+
engine: schema.enum(['web', 'unity', 'godot', 'unreal']).optional(), slot_id: schema.string().max(100).optional(),
|
|
9
|
+
mode: schema.enum(['select', 'generate', 'default']).optional(), asset_id: schema.string().max(100).optional(),
|
|
10
|
+
operation: schema.enum(['plan', 'refine', 'concept', 'generate', 'build']).optional(), quote_token: schema.string().max(100).optional(),
|
|
11
|
+
message: schema.string().trim().min(3).max(1500).optional(), allow_incomplete: schema.boolean().optional(),
|
|
12
|
+
source: schema.enum(['all', 'workspace', 'community']).optional(), q: schema.string().max(100).optional(),
|
|
13
|
+
};
|
|
14
|
+
register(GAME_CREATION_TOOL_NAMES[0], {
|
|
15
|
+
title: 'Prepare and resume a game',
|
|
16
|
+
description: 'The website and CLI share this saved 3D game creation workflow. Ask which engine (Unity, Godot, Unreal, Three.js) if unspecified; do not choose silently. create saves a brief without spending. quote(operation=plan) estimates AI preparation, accept(quote_token) starts its durable job ONLY after user approval of the shown price. get returns a persistent operation log, slots, studio_url and the same project to resume. Planning can select an appropriate preset OR compose real engine-compatible modules. A missing preset never means the genre needs a native engine. composition.modules pins all module versions; composition.tasks lists the custom logic still to implement. Assembly is a foundation, not a completed custom game: use the coding agent to finish those tasks on the selected engine (including web/Three.js). quote refine with message updates the same plan after approval, preserves selected assets and reviewed work, and persists the conversation. use_reference with slot_id and an owned image asset_id supplies an uploaded concept for visual review. assets searches compatible workspace/community items ranked by the brief and role; select chooses one or sets mode=generate with a prompt. For generation: quote concept, obtain approval, accept, poll get, show the concept, approve_concept only after visual user review; quote generate, obtain approval, accept, poll, show the model in its studio, approve_asset only after visual review. Never infer visual approval from technical success. quote build then accept acquires the selected community assets and binds them to a Game Kit prototype, without generating extra assets. With explicit allow_incomplete=true on the build quote, unreviewed models are excluded and missing slots use module defaults where available or remain empty. Show missing_assets before approval; never imply that an incomplete prototype is finished. Native engine projects still need CLI delivery/import; a web project has a play_url. Revision required for edits. Cancel/retry preserve completed steps. 2D unsupported. No automatic spending, invented asset IDs, or unrelated template substitutions.',
|
|
17
|
+
inputSchema: shape, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
18
|
+
}, async (args, extra) => {
|
|
19
|
+
const parsed = schema.object(shape).strict().safeParse(args);
|
|
20
|
+
if (!parsed.success)
|
|
21
|
+
return { isError: true, content: [{ type: 'text', text: parsed.error.message }] };
|
|
22
|
+
const key = options.getApiKey();
|
|
23
|
+
if (!key)
|
|
24
|
+
return { isError: true, content: [{ type: 'text', text: 'GripForge login required.' }] };
|
|
25
|
+
const { workspace_id, ...body } = parsed.data;
|
|
26
|
+
const read = ['list', 'get', 'assets'].includes(body.action);
|
|
27
|
+
const url = new URL('/api/v1/game-creations', options.apiUrl);
|
|
28
|
+
if (read)
|
|
29
|
+
for (const [k, v] of Object.entries(body))
|
|
30
|
+
if (v !== undefined)
|
|
31
|
+
url.searchParams.set(k, String(v));
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(url, { method: read ? 'GET' : 'POST',
|
|
34
|
+
headers: { 'content-type': 'application/json', 'x-api-key': key, 'x-gripforge-client': 'mcp', ...(workspace_id ? { 'x-workspace-id': workspace_id } : {}) },
|
|
35
|
+
...(read ? {} : { body: JSON.stringify(body) }), signal: AbortSignal.any([AbortSignal.timeout(60_000), ...(extra?.signal ? [extra.signal] : [])]) });
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
if (typeof data.studio_url === 'string')
|
|
38
|
+
data.studio_url = new URL(data.studio_url, options.apiUrl).href;
|
|
39
|
+
return { ...(!response.ok ? { isError: true } : {}), structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Creation request failed.' }] };
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -19,8 +19,9 @@ export const GAMEKIT_LOCAL_TOOL_NAMES = ['gripforge_gamekit_deliver_local', 'gri
|
|
|
19
19
|
const LOCK_PATH = 'gripforge/gamekits.lock.json';
|
|
20
20
|
const PREV_LOCK_PATH = 'gripforge/gamekits.lock.prev.json';
|
|
21
21
|
const BACKUP_DIR = 'gripforge/.backup';
|
|
22
|
-
const SCAN_ROOTS = ['gripforge', 'assets/gripforge'];
|
|
23
|
-
|
|
22
|
+
const SCAN_ROOTS = ['gripforge', 'assets/gripforge', 'addons/gripforge_gameplay', 'addons/gripforge_net'];
|
|
23
|
+
/** Godot: the kits, bound assets, and the GripForge addons (gameplay core, network transport) the delivery owns. */
|
|
24
|
+
const GODOT_ROOTS = ['gripforge/', 'assets/gripforge/', 'addons/gripforge_gameplay/', 'addons/gripforge_net/'];
|
|
24
25
|
const MAX_FILES = 5000;
|
|
25
26
|
const MAX_BINARY_BYTES = 256 * 1024 * 1024;
|
|
26
27
|
const DERIVED = ['.uid', '.import'];
|
|
@@ -67,7 +68,7 @@ function projectPath(dir, rel, opts) {
|
|
|
67
68
|
if (!norm.startsWith(`${BACKUP_DIR}/`) && segments.some((s) => s.startsWith('.')))
|
|
68
69
|
throw new LocalError('forbidden_path', `${what} ${rel} has a hidden segment`, { path: rel });
|
|
69
70
|
if (opts.godot && !GODOT_ROOTS.some((root) => norm.startsWith(root)))
|
|
70
|
-
throw new LocalError('forbidden_path', `${what} ${rel} is outside gripforge
|
|
71
|
+
throw new LocalError('forbidden_path', `${what} ${rel} is outside gripforge/, assets/gripforge/ and the GripForge addons`, { path: rel });
|
|
71
72
|
const absolute = resolve(dir, ...segments);
|
|
72
73
|
const back = relative(dir, absolute);
|
|
73
74
|
if (!back || back.startsWith('..') || isAbsolute(back))
|
|
@@ -423,14 +424,13 @@ export function registerGameKitLocalTools(register, options, schema = z) {
|
|
|
423
424
|
}
|
|
424
425
|
register('gripforge_gamekit_deliver_local', {
|
|
425
426
|
title: 'Deliver Game Kits into a local engine project',
|
|
426
|
-
description: 'npm client only. Deliver Game Kits straight into an engine project folder on this machine: hashes project_dir/gripforge/**, reads gripforge/gamekits.lock.json, asks the GripForge API for the plan, then executes plan.actions with fs — every overwritten or deleted file is copied to gripforge/.backup/<plan id>/ first and the lock is written last. Paths are checked inside project_dir (Godot: gripforge
|
|
427
|
+
description: 'npm client only. Deliver Game Kits straight into an engine project folder on this machine: hashes project_dir/gripforge/**, reads gripforge/gamekits.lock.json, asks the GripForge API for the plan, then executes plan.actions with fs — every overwritten or deleted file is copied to gripforge/.backup/<plan id>/ first and the lock is written last. Paths are checked inside project_dir (Godot: gripforge/, assets/gripforge/ and addons/gripforge_gameplay|gripforge_net only) and every hash is verified before anything is written. Run dry_run=true first. A blocked plan (files edited in the project) returns the conflicts without touching files: re-run with force to back them up and overwrite. Kits always deliver at their latest version. verify=true runs Godot headless (import, then each kit smoke.gd) when GODOT_BIN is set. Undo with gripforge_gamekit_rollback_local. Credits: the first delivery of a kit major version to an engine costs 1 credit per workspace; re-deliveries, updates within a major and dry runs are free.',
|
|
427
428
|
inputSchema: {
|
|
428
429
|
project_dir: projectDirField,
|
|
429
430
|
target: schema.enum(TARGETS).optional().describe('Engine of the project: godot (default), unity or unreal.'),
|
|
430
431
|
kits: schema
|
|
431
432
|
.array(schema.object({
|
|
432
433
|
id: kitId,
|
|
433
|
-
version: schema.string().max(40).optional().describe('Exact version; only the latest is deliverable.'),
|
|
434
434
|
config: schema.record(schema.string(), schema.unknown()).optional().describe('Kit config, validated against its configSchema.'),
|
|
435
435
|
bindings: schema.record(schema.string(), schema.string()).optional().describe('Asset slot → Library item id (lib_…).'),
|
|
436
436
|
}))
|
|
@@ -441,7 +441,6 @@ export function registerGameKitLocalTools(register, options, schema = z) {
|
|
|
441
441
|
mode: schema.enum(['install', 'update', 'uninstall']).optional().describe('install (default), update to the latest catalogue version, or uninstall (uses the lock).'),
|
|
442
442
|
dry_run: schema.boolean().optional().describe('true → plan only; nothing is written or charged.'),
|
|
443
443
|
force: schema.boolean().optional().describe('true → back up then overwrite edited managed files and existing seed files.'),
|
|
444
|
-
accept_breaking: schema.boolean().optional().describe('true → allow a breaking (major) update.'),
|
|
445
444
|
verify: schema.boolean().optional().describe('true → after writing, run Godot headless (import + smoke.gd per kit) when GODOT_BIN is set.'),
|
|
446
445
|
...workspace,
|
|
447
446
|
},
|
|
@@ -477,7 +476,6 @@ export function registerGameKitLocalTools(register, options, schema = z) {
|
|
|
477
476
|
project: { target, ...(engineVersion ? { engineVersion } : {}), lock, files },
|
|
478
477
|
dryRun,
|
|
479
478
|
force: args.force === true,
|
|
480
|
-
acceptBreaking: args.accept_breaking === true,
|
|
481
479
|
}, typeof args.workspace_id === 'string' ? args.workspace_id : undefined, extra?.signal);
|
|
482
480
|
if (status >= 400)
|
|
483
481
|
return toolResult({ ...context, applied: false, error: data.error ?? `HTTP ${status}`, code: data.code ?? 'http_error', details: data.details ?? {}, plan: planView(data.plan) }, true);
|