@gripforgeai/mcp 0.1.11 → 0.1.13

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 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
@@ -56,7 +56,7 @@ claude mcp add --transport http gripforge https://gripforge.ai/mcp --header "x-a
56
56
  - **Playable game kits.** Assemble modular capabilities — movement, combat, enemies, worlds — into
57
57
  a game that runs in the browser, then bind your own assets to it.
58
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", "open world adventure").
59
+ agent can search by look ("Devil May Cry like", "genshin").
60
60
 
61
61
  ## Use cases
62
62
 
@@ -172,15 +172,12 @@ writes files into the repo (`out_dir`), including `gripforge_hud`,
172
172
  `gripforge_hud_bar` and `gripforge_cape`. Hosted-only: `gripforge_make_seamless`.
173
173
  Full list: https://gripforge.ai/mcp-docs
174
174
 
175
- - `gripforge_style_kit` — resolve "Devil May Cry like" / "open world adventure" to locker ids
175
+ - `gripforge_style_kit` — resolve "Devil May Cry like" / "genshin" to locker ids
176
176
  already tagged with that look. **Call this before generating.** Reuse the ids.
177
177
  - `gripforge_generate_character` — new T-pose + auto-rig into Library (10 credits).
178
178
  `kind=enemy` or the word "enemy" in the prompt. Skip this if style_kit already
179
179
  returned a character.
180
- - `gripforge_boss` — boss configuration (stats, phases, attacks, arena, engine integration snippets). Rigging, clips and in-engine combat integration remain separate.
181
- - `gripforge_abilities` — reusable `combat.abilities` skills: `schema`, `example`, `validate`, `export`; executable adapters for Three.js, Godot, Unity and Unreal. Common timing/cost/cooldown/targeting; connect the game's combat and presentation backend. No rig or animation manufacture. Persist through gamekit config/data. 0 generation credits. See [abilities documentation](../../docs/abilities.md).
182
- - `gripforge_creature_rig_schema`, `gripforge_creature_analyze`, `gripforge_creature_rig` — reusable anatomy detection and Blender rigging for owned GLBs. Four annotated views → editable anatomy → new private rigged draft, starting clips, GLB, .blend and deformation report. Persistent progress/cancel/retry; source and validated assets stay intact. Review uncertain anatomy and inspect the result in Character Studio. [Creature rig documentation](../../docs/creature-rig.md).
183
- - `gripforge_architecture_schema`, `gripforge_generate_building`, `gripforge_generate_district` — free plan, optional building concepts (1–4 views conditioned on one master, saved to the workspace), visual review, then Meshy PBR manufacture. Separate concept/3D budgets; owned single/multi-image references, configurable geometry/texture quality, immutable Library assets and editable instances. Persistent progress/cancel/retry and Studio links; private work versions. [Architecture documentation](../../docs/architecture-generation.md).
180
+ - `gripforge_boss` — playable boss kit (stats, phases, attacks, arena, engine snippets).
184
181
  0 credits. Reuses a locker enemy. `generate=true` forges a new kind=enemy (10 credits).
185
182
  Then `gripforge_animate` with the returned archetype.
186
183
  - `gripforge_concept_correct` — concept image → strict T-pose sheet (1 credit).
@@ -208,7 +205,6 @@ Full list: https://gripforge.ai/mcp-docs
208
205
  - `gripforge_scene_kit` — locker props + suggested layout for a game look.
209
206
  - `gripforge_light_kit` / `gripforge_font` / `gripforge_navmesh` — lights+fog, webfont+ranks, walkable AABB. 0 credits.
210
207
  - `gripforge_input_kit` — FPS InputMap (WASD + arrows). Write input.json, autoload GfInput.
211
- - `gripforge_joystick_kit` — ready-to-integrate touch/mouse joystick files for Unreal, Godot, Unity and Three.js. Independent movement/look sticks, radial deadzone, continuous hold and focus/release reset. Free, shared with hosted MCP; see below.
212
208
  Optional `third_person: true` adds portable character-facing JavaScript and a
213
209
  Three.js integration example; the existing input map is unchanged. 0 credits.
214
210
  - `gripforge_viewmodel_kit` — CS-style FPS arms + gun under Camera3D/Hold. Write viewmodel.json + gf_viewmodel.gd. 0 credits.
@@ -252,10 +248,6 @@ Full parameters: https://gripforge.ai/mcp-docs
252
248
  - `gripforge_game_capabilities` — what the project provides, what is missing for a goal and which kits fill each gap.
253
249
  - `gripforge_game_project` — `action=list|create|get|bind|data|delete` on projects (bindings slot → `lib_…`, data collections, `confirm=true` to delete).
254
250
  - `gripforge_game_play_url` — browser play URL of the project's current revision (`{ url, absolute }`).
255
- - `gripforge_moba_map` — the map of a MOBA project as a gameplay plan: generate (1–5 lanes, divider), validate (lanes open, everything reachable), set.
256
- - `gripforge_ability_vfx` — a cast effect written and bound for every ability of a project (needs `fx.ability_vfx`; 0 credits).
257
- - `gripforge_terrain_map_use` — a terrain studio map (`lvl_…`) or a scene terrain becomes the world of a game with `world.terrain`: baked file bound to `terrain_map`, reference in `terrain_maps`.
258
- - `gripforge_moba_roster` — champions with an ability pack become the roster of a moba project (`moba_heroes`, `abilities`, `moba_hero_<n>` and `player_character` bindings).
259
251
 
260
252
  ## Character facing for third-person games
261
253
 
@@ -286,22 +278,6 @@ network systems. Omitting `third_person`, or setting it to `false`, returns the
286
278
  existing input kit without the additional section. The option is available in
287
279
  the hosted MCP and this source checkout; it is not in published npm `0.1.5`.
288
280
 
289
- ## Asset production for games
290
-
291
- `gripforge_asset_search` searches real Library and Community assets, independently
292
- of the Game Kit catalogue. Filter by visual role (`q`), `kind`, `source` and the
293
- connected `workspace_id`. Search is free. `gripforge_asset_clone` copies a suitable
294
- Community item into the workspace; the first take costs one generation credit
295
- (one per piece for an armor set).
296
-
297
- Generate only missing roles. Keep a stable `idempotency_key` for each generation,
298
- save its returned `job_id`, poll `gripforge_generation_read`, then download and
299
- integrate the completed asset in the actual engine. A queued job is not a model.
300
- The hosted generation responses preserve job ids and progress/Studio URLs.
301
-
302
- These additions are available in the hosted MCP and this source checkout.
303
- Updating the hosted service does not update an installed npm package.
304
-
305
281
  ## Performance diagnostics
306
282
 
307
283
  `gripforge_performance` is included in this source checkout and the hosted MCP.
@@ -348,174 +324,7 @@ CPU/driver time already included in `cpuMs`; `gpuMs` is meaningful only when
348
324
  actually measured. This tool does not control a browser, change game settings,
349
325
  publish code or automatically fix a game.
350
326
 
351
- ## Concept-first maps and Unreal references
352
-
353
- `gripforge_map_schema` describes the shared Map Studio generation workflow.
354
- `gripforge_map_generate` supports two separate persistent jobs:
355
-
356
- 1. `stage: "concept"` saves a private master image and returns its immutable
357
- `{assetId, revisionId}` reference. Optional `reference` is a pinned Library
358
- image of an existing environment: it guides the art direction of a **new**
359
- layout. Use `gripforge_scene_asset_pin` after importing a viewport screenshot.
360
- Request `views: ["overview", "top_down", "entrance", "objective"]` for a
361
- complete concept set. Every additional angle is conditioned on that same
362
- pinned master. The overview is always included; omitting `views` keeps the
363
- single-image workflow.
364
- 2. Poll `gripforge_generation_read` with `include_preview: true` to receive all
365
- available views, or add `preview_view: "entrance"` to see just one. Show the
366
- concept to the user. These illustrations convey the intended appearance;
367
- they do not establish exact geometry, scale or visibility. Verify those with
368
- camera captures from the same built 3D scene.
369
- 3. After selection, submit a new `stage: "build"` request with the returned
370
- `concept`, a new idempotency key and the same workspace. The result contains
371
- editable terrain, regions, paths, zones and independent asset instances in
372
- the shared Scene Engine. `sources` can select `workspace`, `community` and/or
373
- `catalog` resources. Inspect the actual scene before requesting visual review.
374
-
375
- Each newly generated concept image costs 1 credit: one for the master, up to
376
- three for additional views. To add angles without regenerating the chosen
377
- master, use `source_concept: {assetId, revisionId}` instead of `reference` in a
378
- new concept request. Reusing that master costs nothing; only new views are
379
- billed. A build job costs 1 credit. Cancellation and retry retain each completed
380
- view and avoid repeating successful image generation or billing. Neither stage
381
- promotes the validated version. Omitting `stage` preserves the existing build workflow.
382
- `concept_item` remains a legacy Library image shortcut; prefer `concept` to pin
383
- the precise chosen image even if the Library source is later replaced.
384
-
385
- ```json
386
- {
387
- "stage": "concept",
388
- "prompt": "A new desert canyon for third-person melee combat, two open arenas and a raised shortcut",
389
- "wizard": {
390
- "style": "stylized", "world": "linear", "terrain": "desert_canyon",
391
- "size": "medium", "dimensions": { "width": 500, "depth": 500 },
392
- "boundary": "fixed", "spawn": "single", "boss": true
393
- },
394
- "reference": { "assetId": "lib_your_image", "revisionId": "rev_pinned_image" },
395
- "views": ["overview", "top_down", "entrance", "objective"],
396
- "idempotency_key": "desert-concept-01"
397
- }
398
- ```
399
-
400
- The result exposes `concept_views` with each view's pinned `asset`, `image_url`
401
- and authenticated `preview_url`. `concept` and the suggested build request keep
402
- the master reference. To extend an existing concept, keep the prompt and wizard,
403
- replace `reference` with `source_concept`, list the additional views and use a new
404
- idempotency key. These options are part of this source checkout; they require a
405
- matching deployed API and an updated MCP client before use in production.
406
-
407
- `wizard.dimensions` sets width and depth in **metres** (100–5,000 each), overriding
408
- the size preset and world-type multiplier. Carry the same dimensions from concept
409
- to build. Explicitly dimensioned outdoor terrain uses those exact edge-to-edge
410
- bounds; decorative backdrops can extend beyond the playable footprint. Four-team
411
- maps require a square. Concept images communicate the intended scale; they are
412
- not a measured reconstruction. Omitting dimensions preserves legacy framing.
413
-
414
- For an existing Unreal project, the source checkout also provides
415
- `gripforge_map_unreal_reference_local`:
416
-
417
- ```json
418
- {
419
- "project_file": "/absolute/path/Game/Game.uproject",
420
- "content_path": "/Game/StylizedDesertEnv"
421
- }
422
- ```
423
-
424
- It inventories only the selected pack and returns native asset paths. It does
425
- not upload packages, decode mesh geometry, read editor actor transforms or
426
- write to the project. Kinds are directory hints until verified in Unreal.
427
- The hosted MCP cannot access a user's local disk. Build this checkout and run
428
- `packages/mcp-client/dist/server.js` to use the local tool; changing source code
429
- or deploying the website does not update an installed npm package.
430
-
431
- Native delivery uses the reusable `gripforge.unreal-scene.v1` adapter after concept
432
- selection. It consumes the **common SceneDocument**, a `.uproject`, a new level
433
- path, and pinned asset revision bindings. Map-specific layouts, asset choices and
434
- lighting values belong to the recipe, not the importer.
435
-
436
- For explicit 3D layouts, `gripforge_map_generate` also accepts `structure` with
437
- schema `gripforge.map-structures.v1`. Read `gripforge_map_schema` for a complete
438
- example. Terraces carry polygons and elevations; ramps/bridges name their
439
- endpoints, widths and slope/clearance limits. The common builder preserves canyon
440
- voids, rejects buried connections and creates separate surface/cliff/bridge
441
- assets with persistent per-piece checkpoints. Match `wizard.dimensions`; this
442
- branch currently supports solo maps with spawn and optional objective. Results
443
- are structural work versions requiring visual review and dressing. Existing
444
- callers without `structure` keep their previous generation pipeline.
445
-
446
- 1. `gripforge_map_unreal_prepare_local({plan_file, job_directory})` validates and
447
- snapshots the scene/bindings and packages the installed Python worker.
448
- 2. `gripforge_map_unreal_import_local({job_directory, editor_executable})` launches
449
- the local UE editor; the persistent job continues after the MCP call ends.
450
- 3. `gripforge_map_unreal_import_status_local({job_directory})` reads actual progress,
451
- errors, measurements and viewport capture paths.
452
- 4. `gripforge_map_unreal_import_cancel_local({job_directory})` requests cancellation
453
- at a saved checkpoint. Close that editor, then import the same job to resume.
454
-
455
- The worker creates a **new work level** under `/Game/GripForge/Maps/`, reuses native
456
- meshes/PBR materials, imports per-asset GLBs and preserves instance transforms,
457
- hierarchy and IDs. Paths/region/zone outlines become editable splines. Player
458
- starts bind spawn zones; the full scene data is also carried on actor tags.
459
- Coordinate conversion is metres/Y-up to centimetres/Z-up, including native pivot
460
- offsets and rotations. Material overrides affect components, not shared source
461
- meshes. Terrain geometry has a size check and complex collision on its generated
462
- asset. Source packages are not rewritten. A changed scene needs a new job and
463
- level revision; reopening the same saved job does not duplicate completed actors.
464
-
465
- See `docs/unreal-scene-import.md` in the source
466
- repository for the manifest contract and CLI. The worker is shipped in the MCP
467
- package's `runtime/` folder. It requires the full Unreal editor with Python and
468
- Editor Scripting Utilities, tested against UE 5.7. It is not a headless web service.
469
- The current adapter handles static map content and native Blueprint props;
470
- skeletal retargeting, animation/attachments, terrain layer authoring as native
471
- Landscape, navigation baking, HDRI/cloud translation and bidirectional edits
472
- need their own adapters. Unsupported features fail explicitly. A generated
473
- terrain is an editable static-mesh actor, not a sculptable Landscape.
474
-
475
- Successful import ends at **awaiting_visual_review**, not validated/current.
476
- Review the actual UE render, collision and gameplay before accepting that work
477
- version. The legacy `gripforge_map_export` merged FPS GLB is not this native import.
478
-
479
327
  ## Env
480
328
 
481
329
  - `GRIPFORGE_API_KEY` (required) — 1 credit = 1 successful attach
482
330
  - `GRIPFORGE_API_URL` (optional) — defaults to https://gripforge.ai
483
-
484
- ### Joystick for Unreal, Godot, Unity and Three.js
485
-
486
- Call `gripforge_joystick_kit` with:
487
-
488
- ```json
489
- {"target":"unreal","layout":"dual","deadzone":0.15,"radius":72,"accent":"#ff681f"}
490
- ```
491
-
492
- `target` also accepts `godot`, `unity`, `threejs` or `all`. The response includes
493
- complete `deliveries[].files[]` (relative path + source) and a README per engine.
494
- Write those files into the project and follow that README. No asset generation,
495
- API key or credit is required. Both hosted and npm MCP expose the same tool.
496
-
497
- Unreal receives a runtime Pawn component/plugin; Godot a CanvasLayer scene;
498
- Unity a UGUI component; Three.js a DOM overlay and camera-relative input example.
499
- Movement and optional camera sticks own independent fingers, hold their axes
500
- continuously, and reset on release/cancel/focus loss. The radial deadzone preserves
501
- analog magnitude. Existing movement physics and physical gamepad bindings stay
502
- with the game. Installation and actual game verification are separate from file
503
- generation; Unreal source must be compiled for the project's UE version.
504
- # Unreal environments in Map Studio
505
-
506
- The local tools `gripforge_map_unreal_export_local`,
507
- `gripforge_map_unreal_export_status_local`, `gripforge_map_unreal_export_cancel_local`
508
- and `gripforge_map_unreal_upload_local` export a saved Unreal map and import it into
509
- the authenticated workspace. Supply the installed UE 5.7 editor, `.uproject`,
510
- `/Game/...` level and a persistent job directory. Per-file progress survives closed
511
- calls; upload returns a GripForge Studio link after the persistent server job completes.
512
- The source project is not saved. Imported scenes remain private work versions.
513
-
514
- The hosted equivalents `gripforge_map_import_unreal_schema` and
515
- `gripforge_map_import_unreal` accept exported common scene data, not native `.umap`
516
- bytes. The hosted service cannot start a local editor. PBR meshes, transforms,
517
- foliage patches and Landscape data are portable; Blueprints, Niagara and custom
518
- water/sky shaders need separate adapters. Foliage patches are edited as groups.
519
- Read the live contract at `GET /api/v1/maps/import/unreal` and the public MCP/API docs.
520
-
521
- `gripforge_generation_quote` (hosted MCP / source client) estimates model, concept, texture, rig or animation operations before spending. It returns subscription/credit funding, remaining usage and an operation estimate. Propose missing assets and obtain a generation budget; a quote for one operation is not a fixed price for an entire game.
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod/v4';
2
- export const ARCHITECTURE_TOOL_NAMES = ['gripforge_architecture_schema', 'gripforge_generate_building', 'gripforge_generate_district'];
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'];
3
4
  /** Hosted and local MCP share the same recipe and durable server workflow. */
4
5
  export function registerArchitectureTools(register, options, schema = z) {
5
6
  const identifier = schema.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,159}$/);
@@ -8,6 +9,7 @@ export function registerArchitectureTools(register, options, schema = z) {
8
9
  id: schema.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,47}$/).optional(),
9
10
  name: schema.string().min(1).max(100).optional(), prompt: schema.string().min(3).max(300),
10
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.`),
11
13
  use: schema.enum(['residential', 'retail', 'office', 'industrial', 'mixed']).optional(),
12
14
  floors: schema.number().int().min(1).max(30).optional(),
13
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.'),
@@ -32,6 +34,13 @@ export function registerArchitectureTools(register, options, schema = z) {
32
34
  workspace_id: schema.string().max(100).optional(),
33
35
  idempotency_key: schema.string().regex(/^[a-zA-Z0-9_.:-]{8,160}$/).optional(),
34
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();
35
44
  const definitions = [
36
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.' },
37
46
  { name: ARCHITECTURE_TOOL_NAMES[1], kind: 'building', shape: { ...shared, recipe: building,
@@ -42,6 +51,7 @@ export function registerArchitectureTools(register, options, schema = z) {
42
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.'),
43
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.' },
44
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.' },
45
55
  ];
46
56
  for (const tool of definitions)
47
57
  register(tool.name, { title: tool.title, description: tool.description, inputSchema: tool.shape,
@@ -54,10 +64,10 @@ export function registerArchitectureTools(register, options, schema = z) {
54
64
  return { isError: true, content: [{ type: 'text', text: 'GripForge API key required.' }] };
55
65
  const { workspace_id, ...body } = parsed.data;
56
66
  try {
57
- const response = await fetch(options.apiUrl.replace(/\/$/, '') + '/api/v1/architecture', {
67
+ const response = await fetch(options.apiUrl.replace(/\/$/, '') + (tool.kind === 'module' ? '/api/v1/environment-modules' : '/api/v1/architecture'), {
58
68
  method: tool.kind === 'schema' ? 'GET' : 'POST',
59
69
  headers: { 'content-type': 'application/json', 'x-gripforge-client': 'mcp', ...(key ? { 'x-api-key': key } : {}), ...(typeof workspace_id === 'string' ? { 'x-workspace-id': workspace_id } : {}) },
60
- ...(tool.kind === 'schema' ? {} : { body: JSON.stringify({ ...body, kind: tool.kind }) }),
70
+ ...(tool.kind === 'schema' ? {} : { body: JSON.stringify(tool.kind === 'module' ? body : { ...body, kind: tool.kind }) }),
61
71
  signal: AbortSignal.any([AbortSignal.timeout(60_000), ...(extra?.signal ? [extra.signal] : [])]),
62
72
  });
63
73
  const data = await response.json();
@@ -15,6 +15,7 @@ export const GAMEKIT_TOOL_NAMES = [
15
15
  'gripforge_game_engine',
16
16
  'gripforge_game_content',
17
17
  'gripforge_game_play_url',
18
+ 'gripforge_game_web_export',
18
19
  'gripforge_moba_roster',
19
20
  'gripforge_ability_vfx',
20
21
  'gripforge_moba_map',
@@ -98,6 +99,7 @@ export function registerGameKitTools(register, options, schema = z) {
98
99
  delete out[k];
99
100
  return out;
100
101
  };
102
+ tool('gripforge_game_web_export', 'Prepare a standalone local web game', 'Prepare a download manifest for an existing web Game Kit project in the current workspace. Reuses the same player and HUD as hosted play, compiles only installed enabled kit runtimes, and lists the game configuration, static resources and Library assets to download. Does not publish the project or change its visibility. The hosted MCP cannot write local files: use gripforge game-export <project_id> <folder> locally, then npm run dev. The CLI verifies downloads and preserves local edits on updates (--check previews conflicts; --force backs up and replaces edits only when authorized). The delivered versions remain fixed until an explicit export update. Legacy kits and unpublished source overlays cannot be exported. net.* kits still need their online services. 0 credits.', { project_id: projectId }, { readOnly: false }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/export/web`, body(args), 'POST', extra?.signal));
101
103
  tool('gripforge_gamekit_search', 'Search the Game Kit catalogue', 'Start here. Search the modular Game Kit catalogue (vehicle.driveable, mission.objectives, npc.wanted, …) by free text, capability, tag or engine target. Pass project_id to score kits against that project: each hit then carries state { installed, enabled, fills, reason } and kits that fill one of its missing capabilities rank first. Kits are bricks that go together: a kit handling the same thing as an installed one is a same-scene overlap (only one active in a scene), never a reason to skip it. Returns { kits, presets }; each kit carries its presentation: name, tagline (one-line hook), tags, targets and status per engine (web / godot / unity / unreal), and cover / coverUrl, the featured landscape image (WebP) to show the user when proposing kits. legacy.* kits play through their existing client. Kart Racing is native: compose world.racetrack + vehicle.driveable + race.kart; edit race_tracks through project data. world.lighting adds shared sun/fill, point lights and spots, presets, bounded light budgets and bloom; edit world_lights through project data. world.fluids adds interactive mud / blood surfaces, displaced 3D wheel ruts and footprints; edit fluid_surfaces through project data (web renderer). world.terrain adds a seeded large landscape (terrain_edits data), movement.traversal climbing / swimming / gliding with stamina, world.elements data-driven fire, water, ice, electricity and poison (element_rules, element_materials, element_climates data). Call this BEFORE gripforge_gamekit_install. 0 credits.', {
102
104
  q: schema.string().max(200).optional().describe('Free text matched on id, name, description and tags (e.g. "drive a car", "wanted level").'),
103
105
  capability: schema.string().max(120).optional().describe('Capability the kit must provide (exact id or prefix, e.g. vehicle.drive).'),
@@ -369,9 +371,15 @@ export function registerGameKitTools(register, options, schema = z) {
369
371
  const { project_id, ...body } = args;
370
372
  return api(`gamekit-projects/${enc(project_id)}/ability-vfx`, body, 'POST', extra?.signal);
371
373
  });
372
- tool('gripforge_moba_map', 'Generate, check and set the map of a MOBA project', 'The map of a moba project as a gameplay plan: bounds, team bases and spawns, 1 to 5 lanes (the minions\' paths), structure slots (towers, inhibitors, nexus), jungle zones and camp slots, objective zones, the divider (river: walkable; chasm, lava, void: crossed only at bridges), crossings, bush zones, walls, camera limits and symmetry. action "generate" builds a fair map (mirrored through the centre) from lanes, divider, size, team_size, camps_per_jungle, objectives, bushes, walls and returns it with a playability report; apply: true puts it in the project (moba_maps, the match\'s map and team size, the camera turn). action "validate" checks a map you pass, or the project\'s: every lane open end to end, spawns, structures and camps reachable, nothing out of bounds, the declared symmetry respected. action "set" stores an edited map document (refused while it has errors, unless force). action "get" returns the current map and its report. The scenery kit dresses walls and bushes with the bound models. 0 credits.', {
374
+ tool('gripforge_moba_map', 'Generate, dress, check and set the map of a MOBA project', 'The map of a moba project as a gameplay plan: bounds, team bases and spawns, 1 to 5 lanes (the minions\' paths), structure slots (towers, inhibitors, nexus), jungle zones and camp slots, objective zones, the divider (river: walkable; chasm, lava, void: crossed only at bridges), crossings, bush zones, walls, camera limits and symmetry. action "generate" builds a fair map (mirrored through the centre) from lanes, divider, size, team_size, camps_per_jungle, objectives, bushes, walls and returns it with a playability report; apply: true puts it in the project (moba_maps, the match\'s map and team size, the camera turn). action "validate" checks a map you pass, or the project\'s: every lane open end to end, spawns, structures and camps reachable, nothing out of bounds, the declared symmetry respected. action "set" stores an edited map document (refused while it has errors, unless force). action "get" returns the current map and its report. New maps default to style {theme: "sanctum", detail: "high", seed: 11, relief: "landscape"}. action "dress" previews a reusable procedural finish for the current map: jungle plateaus, river banks and paved fords, terraced bases, walkable staircases, carved structures, clustered forests, stone guardians and lilies; apply: true saves only the map document, keeping its lane layout and camera configuration; relief updates the shared terrain mesh, ground queries and stair collisions; landscape is the default, terraced keeps base terraces only, flat keeps a level arena. Themes: sanctum, wildwood, ashen; detail: low, medium, high; seed makes the finish repeatable. Bound Library models and textures take precedence. The scenery kit dresses walls and bushes with the bound models. 0 credits.', {
373
375
  project_id: projectId,
374
- action: schema.enum(['get', 'generate', 'validate', 'set']).optional().describe('Default: get.'),
376
+ action: schema.enum(['get', 'generate', 'dress', 'validate', 'set']).optional().describe('Default: get. dress previews a finish for the existing map, without rebuilding its layout; apply: true saves it.'),
377
+ style: schema.object({
378
+ relief: schema.enum(['landscape', 'terraced', 'flat']).optional().describe('Default landscape: jungle plateaus, river banks and terraced gardens; shared terrain mesh, physics and unit heights. terraced keeps only the gardens; flat keeps a level map.'),
379
+ theme: schema.enum(['sanctum', 'wildwood', 'ashen']).optional().describe('sanctum: pale carved stone, gold, contrasting groves and lilies; wildwood: mossy forest; ashen: weathered volcanic stone. Default: sanctum.'),
380
+ detail: schema.enum(['low', 'medium', 'high']).optional().describe('Procedural decoration budget, default high. Navigation is identical at all levels.'),
381
+ seed: schema.number().int().min(0).max(2147483646).optional().describe('Repeatable finish, default 11.'),
382
+ }).strict().optional().describe('generate / dress: art direction stored with the map. Paved lanes, inset seals, bevelled stone borders, banks, plants, lilies and entrance lights. Bound Library models and textures take precedence.'),
375
383
  lanes: schema.number().int().min(1).max(5).optional().describe('generate: number of lanes (default 3).'),
376
384
  divider: schema.enum(['river', 'chasm', 'lava', 'void', 'none']).optional().describe('generate: what separates the halves (default river).'),
377
385
  size: schema.number().min(100).max(400).optional().describe('generate: side of the square map in metres.'),
@@ -382,7 +390,7 @@ export function registerGameKitTools(register, options, schema = z) {
382
390
  walls: schema.boolean().optional().describe('generate: rock walls behind the camps (default true).'),
383
391
  id: schema.string().max(41).optional().describe('generate: id of the new map (lowercase, digits, _).'),
384
392
  name: schema.string().max(80).optional().describe('generate: display name of the map.'),
385
- apply: schema.boolean().optional().describe('generate: store the map and make the project play it.'),
393
+ apply: schema.boolean().optional().describe('generate / dress: store the result in the project (default: preview only).'),
386
394
  map: schema.record(schema.string(), schema.unknown()).optional().describe('validate / set: a full map document.'),
387
395
  force: schema.boolean().optional().describe('set: store a map that has errors.'),
388
396
  }, { readOnly: false }, async (args, extra) => {
package/dist/look.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Le rendu (« look ») des outils MCP : un paramètre à part du style de jeu. Même vocabulaire que
3
+ * `LOOKS` de `@gripforge/core` (look.ts) — `qa/look-check.mjs` vérifie que les deux listes concordent.
4
+ */
5
+ export const LOOK_VALUES = ['stylized', 'painted', 'toon', 'anime', 'realistic', 'lowpoly', 'pixel'];
6
+ export const LOOK_DESCRIPTION = 'Rendering look, separate from the game style: stylized (neutral) | painted | toon | anime | realistic | lowpoly | pixel. Old values (handpainted, cartoon, low_poly…) are read as aliases. Omit for the style default.';
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod/v4';
2
+ import { LOOK_DESCRIPTION, LOOK_VALUES } from './look.js';
2
3
  /** UI and agents call the same scene commands, access checks and revision store. */
3
4
  export function registerSceneTools(register, options, schema = z) {
4
5
  const identifier = schema.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,159}$/);
@@ -6,7 +7,8 @@ export function registerSceneTools(register, options, schema = z) {
6
7
  const revision = schema.number().int().positive();
7
8
  const imageRef = schema.object({ assetId: identifier, revisionId: identifier, fileRole: identifier.optional() });
8
9
  const mapWizard = schema.object({
9
- style: schema.enum(['realistic', 'stylized', 'lowpoly', 'handpainted']).optional(),
10
+ style: schema.enum(['realistic', 'stylized', 'lowpoly', 'handpainted']).optional().describe('Legacy wording kept in the intent; it does not change the rendering. Use look.'),
11
+ look: schema.enum(LOOK_VALUES).optional().describe(`${LOOK_DESCRIPTION} Map: the theme gives the biome, the look the materials (LevelSpec.look).`),
10
12
  world: schema.enum(['open_world', 'closed_arena', 'dungeon', 'linear', 'battle_map']).optional(),
11
13
  terrain: schema.enum(['auto', 'plains', 'desert_canyon', 'forest', 'snow', 'island', 'volcanic']).optional(),
12
14
  size: schema.enum(['small', 'medium', 'large']).optional(),
@@ -83,7 +85,7 @@ export function registerSceneTools(register, options, schema = z) {
83
85
  }
84
86
  const tool = (name, title, description, inputSchema, readOnly, callback) => register(name, { title, description, inputSchema: { ...inputSchema, ...workspace }, annotations: { readOnlyHint: readOnly, destructiveHint: false, idempotentHint: readOnly, openWorldHint: false } }, callback);
85
87
  tool('gripforge_vehicle_doors', 'Open or close vehicle doors in the Studio', 'Save an independent door pose on one prepared vehicle instance in a saved scene. doors maps Door_FL/FR/RL/RR to 0 (closed)..1 (open). Unspecified doors retain their state; null clears the manual pose. Requires exact expectedRevision and named hinges/portable clips from prepare_vehicle. Pauses the global animation for manual control. Preserves the source asset, transforms, other instances and validated versions. Returns the actual Studio link. No generation charge.', { id: identifier, expectedRevision: revision, nodeId: identifier, doors: schema.record(schema.string().regex(/^Door_[FR][LR]$/), schema.number().min(0).max(1)).nullable() }, false, (args, extra) => call(`scenes/${args.id}/vehicle-doors`, args, 'POST', extra?.signal));
86
- tool('gripforge_prepare_vehicle', 'Prepare segmented vehicle hinges in Blender', 'Durable Blender preparation of an owned static segmented GLB. Map each Door_FL/FR/RL/RR or Wheel_FL/FR/RL/RR to exact mesh node names from the segmentation report. Keeps geometry placement, UVs and PBR; normalizes +Z forward/metres using yaw and length; adds hinge origins, seat/entry sockets and portable GLB open/close/preview clips. Optional hinge coordinates are in normalized vehicle metres. thickness adds an inward door shell, not a full cabin. Optional cuts provide strictly convex door outlines [forwardZ,heightY] and depth [near,far] measured from the centre on that side, in normalized metres. Blender cuts those explicit profiles while interpolating UVs. Does not infer outlines or guarantee clearance. Returns job_id, new private work GLB, Blender source and animated Studio review link. 0 credits; storage applies. Inspect before replacement.', { source: imageRef, parts: schema.record(schema.string(), schema.array(schema.string().min(1).max(160)).min(1).max(64)).optional(), cuts: schema.array(schema.object({ role: schema.enum(['Door_FL', 'Door_FR', 'Door_RL', 'Door_RR']), outline: schema.array(schema.tuple([schema.number(), schema.number()])).min(3).max(12), depth: schema.tuple([schema.number(), schema.number()]) })).max(4).optional(), yaw: schema.number().min(-180).max(180).optional(), length: schema.number().min(2.8).max(7).optional(), openAngle: schema.number().min(20).max(85).optional(), thickness: schema.number().min(0).max(.04).optional(), hinges: schema.record(schema.string(), schema.tuple([schema.number(), schema.number(), schema.number()])).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('vehicles/prepare', args, 'POST', extra?.signal));
88
+ tool('gripforge_prepare_vehicle', 'Prepare segmented vehicle hinges in Blender', 'Durable Blender preparation of an owned static segmented GLB. Map each Door_FL/FR/RL/RR or Wheel_FL/FR/RL/RR to exact mesh node names from the segmentation report. Keeps geometry placement, UVs and PBR; normalizes +Z forward/metres using yaw and length; adds hinge origins, seat/entry sockets and portable GLB open/close/preview clips. Optional hinge coordinates are in normalized vehicle metres. thickness adds an inward door shell, not a full cabin. Optional cuts provide strictly convex door outlines [forwardZ,heightY] and depth [near,far] measured from the centre on that side, in normalized metres. cuts[].meshes restricts each cut to exact exterior/glazing meshes so seats and dashboard cannot be cut. removeMeshes discards explicitly named obsolete trim in the work copy only. Optional clearanceDepthM clears scoped inner walls behind the door skin; trim adds measured body frames, seals and moulded cards below cardTopY. framePoints follows outline in normalized [x,y,z]. Optional trim.fitToSurface projects frames, seals and the inner card onto only the explicitly selected opaque door skin, preserving the supplied outline. attachments adds scoped [min,max] [x,y,z] volumes for mirrors extending beyond the door profile. Blender cuts those explicit profiles while interpolating UVs. Does not infer outlines or guarantee clearance. Returns job_id, new private work GLB, Blender source and animated Studio review link. 0 credits; storage applies. Inspect before replacement.', { source: imageRef, parts: schema.record(schema.string(), schema.array(schema.string().min(1).max(160)).min(1).max(64)).optional(), cuts: schema.array(schema.object({ role: schema.enum(['Door_FL', 'Door_FR', 'Door_RL', 'Door_RR']), outline: schema.array(schema.tuple([schema.number(), schema.number()])).min(3).max(12), depth: schema.tuple([schema.number(), schema.number()]), meshes: schema.array(schema.string().min(1).max(160)).min(1).max(128).optional(), clearanceDepthM: schema.number().min(0).max(3).optional(), attachments: schema.array(schema.object({ bounds: schema.tuple([schema.tuple([schema.number(), schema.number(), schema.number()]), schema.tuple([schema.number(), schema.number(), schema.number()])]), meshes: schema.array(schema.string().min(1).max(160)).min(1).max(128) })).max(4).optional(), trim: schema.object({ framePoints: schema.array(schema.tuple([schema.number(), schema.number(), schema.number()])).min(3).max(12), frameRadiusM: schema.number().min(.003).max(.04).optional(), sealRadiusM: schema.number().min(.002).max(.02).optional(), cardTopY: schema.number().optional(), cardInsetM: schema.number().min(.01).max(.12).optional(), fitToSurface: schema.boolean().optional(), cardColor: schema.tuple([schema.number().min(0).max(1), schema.number().min(0).max(1), schema.number().min(0).max(1)]).optional() }).optional() })).max(4).optional(), removeMeshes: schema.array(schema.string().min(1).max(160)).max(128).optional(), yaw: schema.number().min(-180).max(180).optional(), length: schema.number().min(2.8).max(7).optional(), openAngle: schema.number().min(20).max(85).optional(), thickness: schema.number().min(0).max(.04).optional(), hinges: schema.record(schema.string(), schema.tuple([schema.number(), schema.number(), schema.number()])).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('vehicles/prepare', args, 'POST', extra?.signal));
87
89
  tool('gripforge_vehicle_schema', 'Modular vehicle manufacturing contract', 'Read the concept-first vehicle recipe, canonical parts, hinge convention, limits, quotes and revision workflow.', {}, true, (args, extra) => call('vehicles', args, 'GET', extra?.signal));
88
90
  tool('gripforge_generate_vehicle', 'Generate a vehicle from reviewed multi-view references', 'Read vehicle_schema first. Recommended recipe.workflow=body_wheels follows concept → references (five wheel-less body views + wheel) → geometry (stop/review) → topology (stop/review) → build (PBR + wheel + rig). Each geometry review uses scene_review with the returned assetRef and must be approved before the next paid step. Rigging preserves UVs and corner normals, GLB/FBX/Blend and measured Chaos setup. Top view is review-only. parts.body.geometry accepts unfinished Hunyuan/Blender GLB; parts.body.topology pins the reviewed reduced GLB; parts.<role>.source reuses finished meshes. Legacy modular_doors remains available for articulated doors. stage=plan quotes each stage without spending; paid stages require its budget. Durable job; poll generation_read. Work draft, real-render review and target-engine driving test required; no automatic publication/replacement.', { stage: schema.enum(['plan', 'concept', 'references', 'geometry', 'topology', 'build']).default('plan'), recipe: record, budget: schema.object({ usd: schema.number().nonnegative().optional(), credits: schema.number().int().nonnegative().optional() }).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('vehicles', args, 'POST', extra?.signal));
89
91
  tool('gripforge_vehicle_segment', 'Separate an existing vehicle with Tripo', 'Quote then queue one durable Tripo v2 semantic segmentation of an owned immutable GLB, including Meshy or Blender sources. stage=plan is free; stage=build requires quoted budget. 40 Tripo provider credits, distinct from the GripForge quote. Returns job_id; inspect parts and texture preservation in generation_read. No automatic completion, retexture, articulation, publication or replacement. A segmented output still needs hinge preparation.', { stage: schema.enum(['plan', 'build']).default('plan'), source: imageRef, granularity: schema.enum(['simple', 'balanced', 'detailed']).optional(), budget: schema.object({ usd: schema.number().nonnegative().optional(), credits: schema.number().int().nonnegative().optional() }).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('vehicles/segment', args, 'POST', extra?.signal));
@@ -104,7 +106,7 @@ export function registerSceneTools(register, options, schema = z) {
104
106
  tool('gripforge_armor_repair', 'Repair unfinished armor pieces', 'Fork a failed/cancelled armor job into a new work kit. Preserve completed pieces and the exact shared concept; recreate only unfinished types by default. Optional pieces chooses types explicitly. Use prompt for targeted corrections. Original checkpoints and artifacts remain intact. Costs 11 credits per regenerated canonical type (plus a concept credit only if none survived). Use generation_retry instead for a transient provider or worker error to reuse the exact same steps without another charge.', { source_job: identifier, prompt: schema.string().min(3).max(2000).optional(), pieces: schema.array(schema.enum(['helmet', 'chest', 'belt', 'pauldron', 'bracer', 'glove', 'thigh', 'greave', 'boot', 'upperarm', 'gorget', 'tasset', 'undersuit'])).min(1).max(13).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('armor-kits', args, 'POST', extra?.signal));
105
107
  tool('gripforge_map_import', 'Migrate a saved terrain map', 'Queue a one-time materialization of an owned legacy terrain map into the shared Scene Engine. Preserve the saved spec, seed, resolution, family replacements, placed assets and removed props. Completed jobs return a canonical Studio link; source edits during migration cause a conflict rather than data loss.', { id: identifier }, false, (args, extra) => call('generation-jobs/map/import', { ...args, legacyMapId: args.id }, 'POST', extra?.signal));
106
108
  tool('gripforge_map_schema', 'Map concept and generation workflow', 'Read the concept-first map workflow, immutable image reference format, costs and an example. Concepts, map work scenes and external engine delivery are separate steps.', {}, true, (args, extra) => call('generation-jobs/map', args, 'GET', extra?.signal));
107
- tool('gripforge_map_generate', 'Create map concept views or an editable map', 'Queue a persistent map job. stage=concept creates a private master overview plus optional views and stops. views: overview,top_down,entrance,objective. All additional views are conditioned on the SAME pinned master; they are illustrations, not exact 3D renders. reference is an environment screenshot for a NEW layout. To add views to an existing concept, pass source_concept instead; the master is kept unchanged and is free. One credit per new image (full new 4-view set = 4); default is overview only. Poll generation_read with include_preview=true to see the views. After the user selects the concept, submit stage=build with the returned concept and a new idempotency key. Build costs 1 credit and is the default for existing callers; legacy concept_item accepts a Library image ID, while concept pins its exact version. wizard: style realistic|stylized|lowpoly|handpainted; world open_world|closed_arena|dungeon|linear|battle_map; terrain auto|plains|desert_canyon|forest|snow|island|volcanic; size small|medium|large; boundary fixed|infinite (open edge, finite terrain); spawn single|teams; teams 2|4; boss,safeZone,extraction booleans. sources selects catalog/workspace/community props. Build uses the common Scene Engine with independent terrain, regions, paths, zones and asset instances. Cancel/retry keeps successful images and billing. No automatic visual validation, promotion or Unreal import.', {
109
+ tool('gripforge_map_generate', 'Create map concept views or an editable map', 'Queue a persistent map job. stage=concept creates a private master overview plus optional views and stops. views: overview,top_down,entrance,objective. All additional views are conditioned on the SAME pinned master; they are illustrations, not exact 3D renders. reference is an environment screenshot for a NEW layout. To add views to an existing concept, pass source_concept instead; the master is kept unchanged and is free. One credit per new image (full new 4-view set = 4); default is overview only. Poll generation_read with include_preview=true to see the views. After the user selects the concept, submit stage=build with the returned concept and a new idempotency key. Build costs 1 credit and is the default for existing callers; legacy concept_item accepts a Library image ID, while concept pins its exact version. wizard: look stylized|painted|toon|anime|realistic|lowpoly|pixel (rendering, separate from style; omit for the theme default); style realistic|stylized|lowpoly|handpainted (legacy wording); world open_world|closed_arena|dungeon|linear|battle_map; terrain auto|plains|desert_canyon|forest|snow|island|volcanic; size small|medium|large; boundary fixed|infinite (open edge, finite terrain); spawn single|teams; teams 2|4; boss,safeZone,extraction booleans. sources selects catalog/workspace/community props. Build uses the common Scene Engine with independent terrain, regions, paths, zones and asset instances. Cancel/retry keeps successful images and billing. No automatic visual validation, promotion or Unreal import.', {
108
110
  prompt: schema.string().min(3).max(4000), wizard: mapWizard, stage: schema.enum(['concept', 'build']).optional(),
109
111
  reference: imageRef.optional().describe('Pinned source-environment screenshot for stage=concept only. Upload to Library and pin first.'),
110
112
  source_concept: imageRef.optional().describe('Existing immutable master to add views to, stage=concept only. Mutually exclusive with reference; never regenerate this master.'),
package/dist/server.js CHANGED
@@ -16,7 +16,6 @@ import { registerVfxProjectTools } from './vfx-project-tools.js';
16
16
  import { registerSceneTools } from './scene-tools.js';
17
17
  import { registerServerTools } from './server-tools.js';
18
18
  import { registerGameKitTools } from './gamekit-tools.js';
19
- import { registerStudioToolsTools } from './studio-tools-tools.js';
20
19
  import { registerJoystickTools } from './joystick-tools.js';
21
20
  import { registerAbilityTools } from './ability-tools.js';
22
21
  import { registerAssetProductionTools } from './asset-production-tools.js';
@@ -35,14 +34,13 @@ import { registerMapUnrealImportLocalTools } from './map-unreal-import-local.js'
35
34
  import { registerMapUnrealExportLocalTools } from './map-unreal-export-local.js';
36
35
  const API_URL = process.env.GRIPFORGE_API_URL ?? 'https://gripforge.ai';
37
36
  const API_KEY = process.env.GRIPFORGE_API_KEY;
38
- const MCP_SELF = '0.1.11';
37
+ const MCP_SELF = '0.1.9';
39
38
  const SUPPORTED = ['.glb', '.gltf', '.fbx', '.obj'];
40
39
  const server = new McpServer({ name: 'gripforge', version: MCP_SELF });
41
40
  registerVfxProjectTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
42
41
  registerSceneTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
43
42
  registerServerTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
44
43
  registerGameKitTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
45
- registerStudioToolsTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
46
44
  registerGameKitLocalTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY, userAgent: `gripforge-mcp/${MCP_SELF}` });
47
45
  registerMapUnrealLocalTools(server.registerTool.bind(server));
48
46
  registerMapUnrealImportLocalTools(server.registerTool.bind(server));
@@ -366,7 +364,7 @@ async function downloadTo(url, dest) {
366
364
  await writeFile(dest, Buffer.from(await res.arrayBuffer()));
367
365
  return true;
368
366
  }
369
- const KIND = z.enum(['character', 'fps-arms', 'enemy', 'weapon', 'equipment', 'prop', 'texture', 'skybox', 'loading', 'hud', 'button', 'bind', 'vfx', 'animation', 'audio']);
367
+ const KIND = z.enum(['character', 'fps-arms', 'enemy', 'weapon', 'prop', 'texture', 'skybox', 'loading', 'hud', 'button', 'bind', 'vfx', 'animation', 'audio']);
370
368
  const TEX_EXTS = ['.png', '.jpg', '.jpeg', '.webp'];
371
369
  server.tool('gripforge_texture_prep', 'Prep a game texture (terrain / props): optional 50% wrap + seam blend, then a faithful ' +
372
370
  'lanczos upscale (1× / 2× / 4×, capped at 1024 or 2048). The pattern is PRESERVED — ' +
@@ -444,7 +442,7 @@ server.tool('gripforge_texture_prep', 'Prep a game texture (terrain / props): op
444
442
  ],
445
443
  };
446
444
  });
447
- server.tool('gripforge_style_kit', 'Resolve "Devil May Cry like" / "open world adventure" to the locker kit already tagged with that game look (characters, enemies, weapons, props). Call this BEFORE generating. Reuse the returned ids.', { prompt: z.string().min(2).max(240).describe('e.g. "devil may cry like", "un ennemi open world adventure"') }, async ({ prompt }) => {
445
+ server.tool('gripforge_style_kit', 'Resolve "Devil May Cry like" / "genshin" to the locker kit already tagged with that game look (characters, enemies, weapons, props). Call this BEFORE generating. Reuse the returned ids.', { prompt: z.string().min(2).max(240).describe('e.g. "devil may cry like", "un ennemi genshin"') }, async ({ prompt }) => {
448
446
  if (!API_KEY)
449
447
  return err('GRIPFORGE_API_KEY missing.');
450
448
  const qs = new URLSearchParams({ limit: '200' }); // style= does the matching; q= would also require the prompt in the NAME and empty the kit;
@@ -876,24 +874,18 @@ server.tool('gripforge_concept_correct', 'Turn a character concept image into a
876
874
  return err(String(data.error ?? res.status));
877
875
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
878
876
  });
879
- server.tool('gripforge_library_list', 'List the GripForge Library locker (characters, enemies, weapons, props, textures, HUD, buttons, binds). Filter by kind and/or game style. Every item carries `rank` {score/100, grade A–D, reasons}: fitness for a GripForge game; sort=rank lists the best fit first. Assets carry a QUALIFICATION, and using it beats searching by name: the KIND is the main type and drives placement (unique per asset); SUBTYPES say what it covers or concerns and several can apply at once — body zones for equipment (helm, chest, shoulder, sleeve, bracer, glove, belt, thigh, greave, boot, cape), usage for a sound (music, ambience, sfx, voice), anchor for a vfx (world, character, weapon). So subtype=boot finds every boot, and kind=equipment with subtype=chest finds a breastplate. roles=true also returns assets where the kind is only a SECONDARY role (the same humanoid body often serves as hero and enemy without a copy). meta.subtypes and meta.alsoKinds come back on every item; tags stay free keywords.', {
877
+ server.tool('gripforge_library_list', 'List the GripForge Library locker (characters, enemies, weapons, props, textures, HUD, buttons, binds). Filter by kind and/or game style. Every item carries `rank` {score/100, grade A–D, reasons}: fitness for a GripForge game; sort=rank lists the best fit first.', {
880
878
  kind: KIND.optional().describe('Filter by kind'),
881
879
  q: z.string().optional().describe('Search name/filename'),
882
- subtype: z.string().optional().describe('What it covers or concerns: boot, chest, sfx, world…'),
883
- roles: z.boolean().optional().describe('With kind: also match assets where this kind is a secondary role'),
884
- style: z.string().optional().describe('Game look (devil-may-cry, dmc, open-world-adventure)'),
880
+ style: z.string().optional().describe('Game look (devil-may-cry, dmc, genshin)'),
885
881
  sort: z.enum(['newest', 'rank']).optional().describe('newest (default) or rank: best fit first'),
886
882
  target: z.enum(['mobile', 'desktop']).optional().describe('Platform for the rank budgets. Defaults to mobile.'),
887
- }, async ({ kind, q, subtype, roles, style, sort, target }) => {
883
+ }, async ({ kind, q, style, sort, target }) => {
888
884
  if (!API_KEY)
889
885
  return err('GRIPFORGE_API_KEY missing.');
890
886
  const qs = new URLSearchParams();
891
887
  if (kind)
892
888
  qs.set('kind', kind);
893
- if (subtype)
894
- qs.set('subtype', subtype);
895
- if (roles)
896
- qs.set('roles', '1');
897
889
  if (q)
898
890
  qs.set('q', q);
899
891
  if (style)
@@ -1039,26 +1031,21 @@ server.tool('gripforge_library_pull', 'Download a Library item into the open rep
1039
1031
  ],
1040
1032
  };
1041
1033
  });
1042
- server.tool('gripforge_library_tag', 'Tag or REQUALIFY a Library item. Tags: add a game look so style_kit can find the item (does not overwrite grip style melee/gun). Qualification: kind is the main type and drives placement (only kinds the FILE can carry: a mesh never becomes a HUD); subtypes are cumulative (body zones for equipment: helm, chest, shoulder, sleeve, bracer, glove, belt, thigh, greave, boot, cape; sound usage; vfx anchor) and each must belong to the main type or a secondary role; also_kinds are secondary roles (a humanoid body that is both character and enemy). A refused value comes back with the field it concerns. Fields left out are kept.', {
1034
+ server.tool('gripforge_library_tag', 'Add a game look to tags so style_kit can find the item. Does not overwrite grip style melee/gun.', {
1043
1035
  id: z.string().describe('Library id (lib_…)'),
1044
1036
  style: z.string().optional().describe('Game look id or alias (devil-may-cry, dmc)'),
1045
1037
  tags: z.array(z.string()).optional().describe('Replace tags. Omit to keep existing and just add style.'),
1046
- kind: z.string().optional().describe('Requalify: the main type (character, enemy, equipment, weapon, prop, texture, hud, vfx, animation, audio…)'),
1047
- subtypes: z.array(z.string()).optional().describe('Requalify: replace the cumulative subtypes ([] clears them)'),
1048
- also_kinds: z.array(z.string()).optional().describe('Requalify: replace the secondary roles ([] clears them)'),
1049
- }, async ({ id, style, tags, kind, subtypes, also_kinds }) => {
1038
+ }, async ({ id, style, tags }) => {
1050
1039
  if (!API_KEY)
1051
1040
  return err('GRIPFORGE_API_KEY missing.');
1052
1041
  const res = await fetch(`${API_URL}/api/v1/library/${encodeURIComponent(id)}`, {
1053
1042
  method: 'PATCH',
1054
1043
  headers: { ...apiHeaders(), 'content-type': 'application/json' },
1055
- body: JSON.stringify({ style, tags, kind, subtypes, alsoKinds: also_kinds }),
1044
+ body: JSON.stringify({ style, tags }),
1056
1045
  });
1057
1046
  const data = await res.json().catch(() => ({}));
1058
- if (!res.ok) {
1059
- const fields = data.fields;
1060
- return err(fields ? Object.entries(fields).map(([f, m]) => `${f}: ${m.join('; ')}`).join(' | ') : String(data.error ?? res.status));
1061
- }
1047
+ if (!res.ok)
1048
+ return err(String(data.error ?? res.status));
1062
1049
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
1063
1050
  });
1064
1051
  server.tool('gripforge_hand_rig', 'Complete a rigged Library character with finger bones (3 phalanges × 5 fingers per hand, Mixamo names) built from its hand mesh — no provider rigs fingers, so without this a hand cannot close on a weapon and pistol/knife clips have nothing to drive. New characters get it automatically in gripforge_generate_character; use this on characters generated before. Updates the item in place. 0 credits. Then re-run gripforge_animate.', { character_id: z.string().describe('Library character / enemy / bind (lib_…)') }, async ({ character_id }) => {
@@ -1444,24 +1431,22 @@ server.tool('gripforge_vehicle_wheels', 'Split the 4 wheels out of a fused kart/
1444
1431
  return err(String(data.error ?? res.status));
1445
1432
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
1446
1433
  });
1447
- server.tool('gripforge_vehicle_finish', 'Repair generated automotive surfaces. Preferred: source (immutable static GLB with Body/Wheel_* groups) + repair recipe queues a persistent Blender job; guided glazing/panel regions, paint cleanup, coloured caliper extraction, measured replacement panels/curves with PBR and fitted boundaries, optional rebuilt wheels and rigid wheel rig. features.marks adds surface-fitted pinned PNG logos, relief badges and mesh lettering; wheel markings follow their wheel, named authored marks are replaced in the new work copy. For grilles or trim on a reconstructed recess, set curves.fitSurface.panel to its non-structural panel name to follow the final part. Returns a NEW private work version with GLB/Blend/optional FBX and GripForge review link. Does not promote or overwrite the source. Regions use +Y up/+Z forward metres, bounded displacement; region-only glass stays opaque, authored transmissive panes need an authored cabin. cabin fabricates a measured interior; shellDenoise refines replaced coachwork; material.microSurface embeds reusable PBR detail. render_profile configures shared automotive lighting and post-processing. Poll/cancel/retry generation-jobs. 0 provider credits. Legacy id without repair uses one-time material/wheel finish and rewrites its Library file.', {
1434
+ server.tool('gripforge_vehicle_finish', 'Repair generated automotive surfaces. Preferred: source (immutable static GLB with Body/Wheel_* groups) + repair recipe queues a persistent Blender job; guided glazing/panel regions, paint cleanup, coloured caliper extraction, measured replacement panels/curves with PBR and fitted boundaries, optional rebuilt wheels and rigid wheel rig. features.marks adds surface-fitted pinned PNG logos, relief badges and mesh lettering; wheel markings follow their wheel, named authored marks are replaced in the new work copy. For grilles or trim on a reconstructed recess, set curves.fitSurface.panel to its non-structural panel name to follow the final part. Returns a NEW private work version with GLB/Blend/optional FBX and GripForge review link. Does not promote or overwrite the source. Regions use +Y up/+Z forward metres, bounded displacement; region-only glass stays opaque, authored transmissive panes need an authored cabin. Poll/cancel/retry generation-jobs. 0 provider credits. Legacy id without repair uses one-time material/wheel finish and rewrites its Library file.', {
1448
1435
  id: z.string().optional().describe('Library id; with repair pins source, without repair uses legacy finish'),
1449
1436
  source: z.object({ assetId: z.string(), revisionId: z.string(), fileRole: z.string().optional() }).optional().describe('Pinned static source, instead of id'),
1450
1437
  repair: z.object({
1451
1438
  regions: z.array(z.object({ name: z.string(), bounds: z.tuple([z.tuple([z.number(), z.number(), z.number()]), z.tuple([z.number(), z.number(), z.number()])]), select: z.enum(['neutral', 'paint', 'all']), surface: z.enum(['glass', 'paint']), fitAxis: z.enum(['x', 'y', 'z']), maxOffsetM: z.number().min(0).max(.05).optional() })).max(16),
1452
1439
  denoise: z.object({ iterations: z.number().int().min(1).max(80).optional(), maxOffsetM: z.number().min(0).max(.05).optional(), normalIterations: z.number().int().min(0).max(30).optional() }).optional().describe('Bounded Body smoothing across UV seams; pins open boundaries, retains UVs and wheel/caliper transforms; review small details'),
1453
1440
  cleanPaint: z.boolean().optional(), paintColor: z.tuple([z.number(), z.number(), z.number()]).optional(),
1454
- cabin: z.object({ bounds: z.tuple([z.tuple([z.number(), z.number(), z.number()]), z.tuple([z.number(), z.number(), z.number()])]), seats: z.union([z.literal(2), z.literal(4)]).optional(), steeringSide: z.enum(['left', 'right']).optional(), glassMeshes: z.array(z.string().min(1).max(120)).max(12).optional(), glassTransmission: z.number().min(0).max(1).optional(), glassThicknessM: z.number().min(.001).max(.02).optional(), upholsteryColor: z.tuple([z.number(), z.number(), z.number()]).optional(), glassColor: z.tuple([z.number(), z.number(), z.number()]).optional() }).optional().describe('Measured parametric cabin: seats, dashboard, steering, console, floor and trim. Exact isolated transmissive Body pane names only; no inferred segmentation.'),
1455
1441
  calipers: z.object({ color: z.tuple([z.number(), z.number(), z.number()]), tolerance: z.number().min(.05).max(.4).optional() }).optional(),
1456
1442
  features: z.object({
1457
1443
  replaceBody: z.boolean().optional().describe('Explicitly replace coachwork in a new work copy using structural panels; preserve wheels and Body calipers.'),
1458
- shellDenoise: z.object({ iterations: z.number().int().min(1).max(80).optional(), normalIterations: z.number().int().min(0).max(30).optional(), maxOffsetM: z.number().min(0).max(.05).optional() }).optional().describe('Refine only a newly replaced structural shell; preserves window/cut boundaries and never smooths badges, lamps or wheels.'),
1459
1444
  panels: z.array(z.object({
1460
1445
  name: z.string(), points: z.array(z.array(z.tuple([z.number(), z.number(), z.number()])).min(2).max(12)).min(2).max(12),
1461
1446
  structural: z.boolean().optional().describe('New coachwork shell, required with replaceBody; cannot fit or clip the discarded source.'),
1462
1447
  normal: z.tuple([z.number(), z.number(), z.number()]).optional(),
1463
1448
  segmentsU: z.number().int().min(4).max(96).optional(), segmentsV: z.number().int().min(4).max(96).optional(),
1464
- mirrorX: z.boolean().optional(), material: z.object({ color: z.tuple([z.number(), z.number(), z.number()]).optional(), metallic: z.number().min(0).max(1).optional(), roughness: z.number().min(.02).max(1).optional(), coat: z.number().min(0).max(1).optional(), transmission: z.number().min(0).max(1).optional(), ior: z.number().min(1).max(2.5).optional(), emission: z.number().min(0).max(10).optional(), coatRoughness: z.number().min(0).max(1).optional(), microSurface: z.enum(['paint', 'leather', 'rubber', 'metal']).optional(), detail: z.number().min(0).max(1).optional() }).optional(),
1449
+ mirrorX: z.boolean().optional(), material: z.object({ color: z.tuple([z.number(), z.number(), z.number()]).optional(), metallic: z.number().min(0).max(1).optional(), roughness: z.number().min(.02).max(1).optional(), coat: z.number().min(0).max(1).optional(), transmission: z.number().min(0).max(1).optional(), ior: z.number().min(1).max(2.5).optional(), emission: z.number().min(0).max(10).optional() }).optional(),
1465
1450
  replace: z.object({ axis: z.enum(['x', 'y', 'z']), depthM: z.number().min(.001).max(.5).optional() }).optional(),
1466
1451
  fitBoundary: z.object({ axis: z.enum(['x', 'y', 'z']), direction: z.union([z.literal(1), z.literal(-1)]).optional(), offsetM: z.number().min(0).max(.01).optional(), fullSurface: z.boolean().optional() }).optional(), sealRadiusM: z.number().min(.0005).max(.03).optional(),
1467
1452
  })).max(32).optional(),
@@ -1469,7 +1454,7 @@ server.tool('gripforge_vehicle_finish', 'Repair generated automotive surfaces. P
1469
1454
  name: z.string(), points: z.array(z.tuple([z.number(), z.number(), z.number()])).min(2).max(128),
1470
1455
  fitSurface: z.object({ axis: z.enum(['x', 'y', 'z']), direction: z.union([z.literal(1), z.literal(-1)]).optional(), offsetM: z.number().min(0).max(.01).optional(), panel: z.string().trim().min(1).max(80).optional() }).optional(),
1471
1456
  radiusM: z.number().min(.0005).max(.08).optional(), closed: z.boolean().optional(), mirrorX: z.boolean().optional(),
1472
- material: z.object({ color: z.tuple([z.number(), z.number(), z.number()]).optional(), metallic: z.number().min(0).max(1).optional(), roughness: z.number().min(.02).max(1).optional(), coat: z.number().min(0).max(1).optional(), transmission: z.number().min(0).max(1).optional(), ior: z.number().min(1).max(2.5).optional(), emission: z.number().min(0).max(10).optional(), coatRoughness: z.number().min(0).max(1).optional(), microSurface: z.enum(['paint', 'leather', 'rubber', 'metal']).optional(), detail: z.number().min(0).max(1).optional() }).optional(),
1457
+ material: z.object({ color: z.tuple([z.number(), z.number(), z.number()]).optional(), metallic: z.number().min(0).max(1).optional(), roughness: z.number().min(.02).max(1).optional(), coat: z.number().min(0).max(1).optional(), transmission: z.number().min(0).max(1).optional(), ior: z.number().min(1).max(2.5).optional(), emission: z.number().min(0).max(10).optional() }).optional(),
1473
1458
  })).max(256).optional(),
1474
1459
  marks: z.array(z.object({
1475
1460
  name: z.string().min(1).max(80), kind: z.enum(['decal', 'badge', 'text']), target: z.enum(['Body', 'Wheel_FL', 'Wheel_FR', 'Wheel_RL', 'Wheel_RR']).optional(),
@@ -1477,13 +1462,12 @@ server.tool('gripforge_vehicle_finish', 'Repair generated automotive surfaces. P
1477
1462
  widthM: z.number().min(.005).max(.8), heightM: z.number().min(.005).max(.8), offsetM: z.number().min(.0002).max(.01).optional(), depthM: z.number().min(0).max(.01).optional(), maxDistanceM: z.number().min(.001).max(.12).optional(), segments: z.number().int().min(4).max(32).optional(), mirrorX: z.boolean().optional(),
1478
1463
  outline: z.array(z.tuple([z.number().min(-.5).max(.5), z.number().min(-.5).max(.5)])).min(3).max(64).optional(),
1479
1464
  artwork: z.object({ assetId: z.string(), revisionId: z.string(), fileRole: z.string().optional() }).optional(), text: z.string().min(1).max(48).optional(),
1480
- material: z.object({ color: z.tuple([z.number(), z.number(), z.number()]).optional(), metallic: z.number().min(0).max(1).optional(), roughness: z.number().min(.02).max(1).optional(), coat: z.number().min(0).max(1).optional(), transmission: z.number().min(0).max(1).optional(), ior: z.number().min(1).max(2.5).optional(), emission: z.number().min(0).max(10).optional(), coatRoughness: z.number().min(0).max(1).optional(), microSurface: z.enum(['paint', 'leather', 'rubber', 'metal']).optional(), detail: z.number().min(0).max(1).optional() }).optional(),
1465
+ material: z.object({ color: z.tuple([z.number(), z.number(), z.number()]).optional(), metallic: z.number().min(0).max(1).optional(), roughness: z.number().min(.02).max(1).optional(), coat: z.number().min(0).max(1).optional(), transmission: z.number().min(0).max(1).optional(), ior: z.number().min(1).max(2.5).optional(), emission: z.number().min(0).max(10).optional() }).optional(),
1481
1466
  })).max(32).optional().describe('Surface-fitted logos and relief: decals require pinned PNG artwork; badges add thickness and a convex outline; text makes mesh lettering. Explicit normal/up frame; target wheel marks spin with that wheel. Optional receiver scopes fitting to a mesh in the target. PNGs <=1 MiB/2048px, max 16 distinct sources. No URLs or local paths.'),
1482
1467
  }).optional().describe('Measured panels, curves, badges and decals; explicit PBR, clipping, symmetry, 250k vertex budget.'),
1483
1468
  rig: z.object({ wheelRadius: z.number(), wheelWidth: z.number() }).optional(),
1484
1469
  wheelRebuild: z.object({ radiusM: z.number().min(.1).max(1.5), widthM: z.number().min(.05).max(.8), spokes: z.number().int().min(3).max(12).optional(), segments: z.number().int().min(24).max(128).optional(), trackM: z.number().min(.3).max(6).optional() }).optional().describe('Replace generated wheel meshes with measured tyres, dished spokes and brake rotors. Optional trackM adjusts axle track and matching explicit Body calipers together; omitted preserves wheel pivots.'),
1485
1470
  }).optional().describe('Guided surface repair; never edits the source'),
1486
- render_profile: z.object({ look: z.enum(['studio', 'daylight']).optional(), quality: z.enum(['performance', 'balanced', 'quality']).optional(), lengthM: z.number().min(1).max(25).optional(), ground: z.boolean().optional(), depthOfField: z.boolean().optional(), focusDistanceM: z.number().min(.1).max(1000).optional() }).optional().describe('Shared automotive review scene profile; balanced studio by default. DOF off unless explicitly requested.'),
1487
1471
  name: z.string().optional(), idempotency_key: z.string().min(8).max(160).optional(),
1488
1472
  smooth: z.number().int().min(0).max(60).optional().describe('body smoothing passes (default 15, 0 = none)'),
1489
1473
  wheels: z.boolean().optional().describe('false keeps the generated wheels'),
@@ -2,6 +2,7 @@
2
2
  // mcp-handler requires v4 field schemas for raw shapes. zod 3.25 ships v4
3
3
  // alongside v3, so existing stdio tools can retain their current v3 schemas.
4
4
  import { z } from 'zod/v4';
5
+ import { LOOK_DESCRIPTION, LOOK_VALUES } from './look.js';
5
6
  // The hosted SDK uses a newer Zod v4 serializer than the npm SDK. Inject its
6
7
  // matching factory so schemas remain compatible with each host's serializer.
7
8
  export function registerVfxProjectTools(register, options, schema = z) {
@@ -44,5 +45,5 @@ export function registerVfxProjectTools(register, options, schema = z) {
44
45
  tool('gripforge_vfx_project_render', 'Render actual VFX frames for inspection', 'Queue a durable render of an owned id OR an unsaved project at 1–4 timestamps. Returns job_id immediately. Poll gripforge_generation_read with include_preview for the actual rendered image, coverage, clipping and shader diagnostics. Uses the shared GripForge SceneViewport. Saves preview evidence only, no project revision. Requires workspace write access because the job and preview consume storage. Render timings are not browser FPS.', { idempotency_key: schema.string().min(8).max(160).optional(), id: id.optional(), project: project.optional(), times }, false, (args, extra) => api('/render', args, 'POST', extra?.signal));
45
46
  tool('gripforge_vfx_project_review', 'Review an existing work revision', 'Queue a fresh GripForge capture and independent visual review without writing another source revision. Poll gripforge_generation_read; inspect its preview, then explicitly promote if approved.', { id, expected_revision: schema.number().int().positive(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => api(`/${args.id}/review`, args, 'POST', extra?.signal));
46
47
  tool('gripforge_vfx_project_promote', 'Set the visually approved VFX version', 'Explicitly promote work after a trusted positive review matching this source, dependencies and renderer. expectedCurrent is the existing validated revision, or null.', { id, revision: schema.number().int().positive(), expectedCurrent: schema.number().int().positive().nullable() }, false, (args, extra) => api(`/${args.id}/promote`, args, 'POST', extra?.signal));
47
- tool('gripforge_vfx_project_generate', 'Create, render and refine a custom VFX', 'Text/image -> durable generation job -> original geometry/shaders -> actual GripForge rendering -> visual critique/correction -> workspace work revision. Returns immediately with job_id, status_url and studio_url. Poll gripforge_generation_read, then open its source_url to inspect the saved source and review. Closing the request does not cancel the job. Completed steps survive worker restart. Pass id + expected_revision to refine; image_id must belong to this workspace. A visually rejected candidate never replaces the validated version.', { idempotency_key: schema.string().min(8).max(160).optional(), prompt: schema.string().max(2000).optional(), visual_style: schema.enum(['realistic', 'stylized', 'anime', 'lowpoly']).optional(), image_mode: schema.enum(['spatial', 'animated-artwork']).optional().describe('Default spatial: build a 3D effect. animated-artwork explicitly opts into a flat animated illustration.'), image_id: id.optional(), image_data: schema.string().max(12 * 1024 * 1024).optional(), id: id.optional(), expected_revision: schema.number().int().positive().optional() }, false, (args, extra) => api('/generate', args, 'POST', extra?.signal), true);
48
+ tool('gripforge_vfx_project_generate', 'Create, render and refine a custom VFX', 'Text/image -> durable generation job -> original geometry/shaders -> actual GripForge rendering -> visual critique/correction -> workspace work revision. Returns immediately with job_id, status_url and studio_url. Poll gripforge_generation_read, then open its source_url to inspect the saved source and review. Closing the request does not cancel the job. Completed steps survive worker restart. Pass id + expected_revision to refine; image_id must belong to this workspace. A visually rejected candidate never replaces the validated version.', { idempotency_key: schema.string().min(8).max(160).optional(), prompt: schema.string().max(2000).optional(), visual_style: schema.enum(LOOK_VALUES).optional().describe('Rendering look of the effect (same vocabulary as look). Default stylized.'), look: schema.enum(LOOK_VALUES).optional().describe(LOOK_DESCRIPTION), image_mode: schema.enum(['spatial', 'animated-artwork']).optional().describe('Default spatial: build a 3D effect. animated-artwork explicitly opts into a flat animated illustration.'), image_id: id.optional(), image_data: schema.string().max(12 * 1024 * 1024).optional(), id: id.optional(), expected_revision: schema.number().int().positive().optional() }, false, (args, extra) => api('/generate', args, 'POST', extra?.signal), true);
48
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gripforgeai/mcp",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "The tools your AI needs to make your games. Turn prompts into production-ready game assets — animated characters with their weapons attached, seamless textures, terrain, VFX, HUDs — and playable game kits Unity, Godot, Unreal or Three.js can load.",
5
5
  "license": "MIT",
6
6
  "type": "module",