@gripforgeai/mcp 0.1.14 → 0.1.16

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.
@@ -47,7 +47,7 @@ export function registerAssetProductionTools(register, options, schema = z) {
47
47
  }));
48
48
  register('gripforge_asset_search', {
49
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.',
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. In a Game Kit project, use it to fill what gripforge_game_audit reports missing (a creature slot gets ranked proposals from gripforge_gamekit_creatures; a character without clips is animated, not left idle).',
51
51
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
52
52
  inputSchema: {
53
53
  q: schema.string().max(200).describe('Visual role or style, e.g. sci-fi turret, rock, soldier.'),
@@ -13,7 +13,7 @@ export function registerGameCreationTools(register, options, schema = z) {
13
13
  };
14
14
  register(GAME_CREATION_TOOL_NAMES[0], {
15
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.',
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). After a build, get also returns creatures (the models bound so that no unit is a capsule) and audit { verdict: complete | playable_with_defects | incomplete, headline, fixed (the free fixes applied: models, standard clips, control scheme), defects [{ severity, message, fix { tool, args, credits? } }], next }: show the user the verdict and what is left, work through next (gripforge_game_audit reads it again), and never present a verdict other than complete as a finished game. 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
17
  inputSchema: shape, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
18
18
  }, async (args, extra) => {
19
19
  const parsed = schema.object(shape).strict().safeParse(args);
@@ -12,16 +12,44 @@ export const GAMEKIT_TOOL_NAMES = [
12
12
  'gripforge_gamekit_deliver',
13
13
  'gripforge_game_capabilities',
14
14
  'gripforge_game_project',
15
+ 'gripforge_gamekit_creatures',
15
16
  'gripforge_game_engine',
16
17
  'gripforge_game_content',
17
18
  'gripforge_game_play_url',
18
19
  'gripforge_game_test',
20
+ 'gripforge_game_audit',
19
21
  'gripforge_game_web_export',
20
22
  'gripforge_moba_roster',
21
23
  'gripforge_ability_vfx',
22
24
  'gripforge_moba_map',
23
25
  'gripforge_terrain_map_use',
24
26
  ];
27
+ /**
28
+ * Where each tool stands in the making of a game, appended to its description: when to call it, and the audit it
29
+ * leads to. One table, so the journey reads the same from every tool — and one place to change it.
30
+ */
31
+ export const TOOL_JOURNEY = {
32
+ gripforge_gamekit_search: 'Journey — first: find the preset of the genre. Its `done` says what a finished game of that genre has beyond the request, and gripforge_game_audit checks it at the end.',
33
+ gripforge_gamekit_installed: 'Journey — first on an existing project; gripforge_game_audit then says what stands between it and a finished game.',
34
+ gripforge_gamekit_install: 'Journey — while composing. A kit installed and left empty is not done (audio.core without a sound, ui.endscreen without an end condition): gripforge_game_audit names them.',
35
+ gripforge_gamekit_configure: 'Journey — while composing, and to fix what gripforge_game_audit reports (its defects carry the exact config to write).',
36
+ gripforge_game_capabilities: 'Journey — while composing; gripforge_game_audit includes this report, with the creatures, the controls, the playtest and the definition of done of the genre.',
37
+ gripforge_game_project: 'Journey — create, then bind a model to every creature slot (get carries the creatures report) and set the controls: action=controls applies the control scheme of the gameplay, a proposal to adapt. Before telling the user the game is done: gripforge_game_audit.',
38
+ gripforge_gamekit_creatures: 'Journey — right after creating or binding: no unit stays a capsule, every model gets its movement and action clips. gripforge_game_audit folds this report with the rest and applies the free steps with fix=true.',
39
+ gripforge_game_engine: 'Journey — the build order of the content; its last step is gripforge_game_audit, not "it loads".',
40
+ gripforge_game_content: 'Journey — the content of the game. Once written, gripforge_game_audit says what a finished game of the genre still lacks (a quest, a way to win and to lose, dialogues…).',
41
+ gripforge_game_test: 'Journey — after each change. A playtest that passes is not a finished game: gripforge_game_audit (playtest="run" runs this test) gives the verdict and everything the tester cannot see.',
42
+ gripforge_game_play_url: 'Journey — to look at the running game. Share it as finished only when gripforge_game_audit says complete; otherwise say what is left.',
43
+ gripforge_game_web_export: 'Journey — last. Run gripforge_game_audit first: export a game whose verdict is complete, or tell the user what is left.',
44
+ gripforge_gamekit_deliver: 'Journey — last. Run gripforge_game_audit first: deliver a game whose verdict is complete, or tell the user what is left.',
45
+ gripforge_moba_roster: 'Journey — a MOBA needs distinct heroes; then gripforge_game_audit for the minions, towers, jungle monsters and the rest of a finished MOBA.',
46
+ gripforge_ability_vfx: 'Journey — after the roster; gripforge_game_audit lists it among what a finished MOBA has.',
47
+ };
48
+ /** The tool's place in the journey, then its description (whose closing credit rule stays last). */
49
+ function withJourney(name, description) {
50
+ const journey = TOOL_JOURNEY[name];
51
+ return journey ? `${journey} ${description}` : description;
52
+ }
25
53
  const enc = (v) => encodeURIComponent(String(v));
26
54
  const HOSTED_DELIVERY_NOTE = 'This hosted endpoint cannot write into your project: write bundle.files following plan.actions in order, or use npx @gripforgeai/mcp with gripforge_gamekit_deliver_local { project_dir }, or the editor bridge.';
27
55
  export function registerGameKitTools(register, options, schema = z) {
@@ -43,7 +71,7 @@ export function registerGameKitTools(register, options, schema = z) {
43
71
  .record(schema.string(), schema.unknown())
44
72
  .optional()
45
73
  .describe('Kit config values, validated against the kit configSchema returned by gripforge_gamekit_get. Merged over the current config.');
46
- async function api(path, args, method, signal, query) {
74
+ async function api(path, args, method, signal, query, timeoutMs = 120_000) {
47
75
  const key = options.getApiKey();
48
76
  if (!key)
49
77
  return { isError: true, content: [{ type: 'text', text: 'GripForge API key required.' }] };
@@ -66,7 +94,7 @@ export function registerGameKitTools(register, options, schema = z) {
66
94
  ...(typeof workspace_id === 'string' ? { 'x-workspace-id': workspace_id } : {}),
67
95
  },
68
96
  ...(method === 'GET' || method === 'DELETE' ? {} : { body: JSON.stringify(body) }),
69
- signal: AbortSignal.any([AbortSignal.timeout(120_000), ...(signal ? [signal] : [])]),
97
+ signal: AbortSignal.any([AbortSignal.timeout(timeoutMs), ...(signal ? [signal] : [])]),
70
98
  });
71
99
  const data = (await res.json());
72
100
  return {
@@ -82,7 +110,7 @@ export function registerGameKitTools(register, options, schema = z) {
82
110
  function tool(name, title, description, inputSchema, opts, callback) {
83
111
  register(name, {
84
112
  title,
85
- description,
113
+ description: withJourney(name, description),
86
114
  inputSchema: { ...inputSchema, ...workspace },
87
115
  annotations: {
88
116
  readOnlyHint: opts.readOnly,
@@ -101,7 +129,7 @@ export function registerGameKitTools(register, options, schema = z) {
101
129
  return out;
102
130
  };
103
131
  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));
104
- 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.', {
132
+ 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). Also returns packs: content packs (ready-made game content — documents and kit configs, e.g. a realistic atmosphere, a tree of life, a star system) that gripforge_gamekit_install { pack } installs into a project. Call this BEFORE gripforge_gamekit_install. 0 credits.', {
105
133
  q: schema.string().max(200).optional().describe('Free text matched on id, name, description and tags (e.g. "drive a car", "wanted level").'),
106
134
  capability: schema.string().max(120).optional().describe('Capability the kit must provide (exact id or prefix, e.g. vehicle.drive).'),
107
135
  tag: schema.string().max(60).optional().describe('Tag filter (e.g. vehicle, mission, npc, legacy).'),
@@ -127,12 +155,16 @@ export function registerGameKitTools(register, options, schema = z) {
127
155
  tool('gripforge_gamekit_installed', 'Installed kits of a project (use these first)', 'Start here for an existing project. Lists the kits installed in a Game Kit project (gkp_…), first and marked priority: id, name, enabled, addedBy (user / agent / auto / preset / default — the interface kits every new game gets), provides, requires and config, plus the rule that goes with them. Versions are for your information (projects always follow the latest): each kit also has version (in the project), latest, delivered { godot | unity | unreal } (the version the last engine delivery carries — a delivered game can run an older one) and pendingMigrations (manual migrations left to you). Installed kits are choices already made for this game: build on them for what they cover and configure them as needed; search the catalogue only for what they do not cover. 0 credits.', { project_id: projectId }, { readOnly: true }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/kits`, args, 'GET', extra?.signal));
128
156
  tool('gripforge_gamekit_install', 'Install a Game Kit into a project', 'Add a gameplay kit (a brick) to a Game Kit project (gkp_…); installing marks that the game uses it. Decide it yourself, no confirmation needed. Always the latest version; dependencies are added automatically and listed in added [{ kit, version, requiredBy, capability }] (e.g. game.engine for a kit that requires it). Kits are bricks that go together: two kits handling the same thing (e.g. two lightings) are a same-scene overlap, never a game-level conflict — only one is active in a scene. Returns { plan, applied, added, project }; dry_run=true previews without writing. Check gripforge_gamekit_installed first, then gripforge_gamekit_search; tune with gripforge_gamekit_configure. Legacy genre kits (legacy.*) use gripforge_kit. 0 credits.', {
129
157
  project_id: projectId,
130
- id: kitId,
158
+ id: kitId.optional().describe('Kit id to install (e.g. vehicle.driveable). Required unless pack is given.'),
159
+ pack: schema.string().max(80).optional().describe('Instead of id: a content pack listed by gripforge_gamekit_search (packs[].id, e.g. tree-of-life) — its kits are installed, its configs merged and its documents upserted by id (the project\'s own documents are kept). Returns { pack, applied, plan: { installed, configured, data }, added, project }.'),
160
+ existing: schema.enum(['replace', 'keep']).optional().describe('With pack: a pack document whose id the project already has — replace (default) or keep the project\'s.'),
131
161
  config,
132
162
  dry_run: schema.boolean().optional().describe('true to return the install plan without writing the project.'),
133
163
  allow_planned: schema.boolean().optional().describe('true to accept kits still marked planned (0.x, no runtime yet). Default false.'),
134
164
  reason: schema.string().max(300).optional().describe('One sentence: why you do this, recorded in the project history (required in spirit when you go against the installed-kits rule).'),
135
- }, { readOnly: false }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/kits`, { ...body(args), id: args.id }, 'POST', extra?.signal));
165
+ }, { readOnly: false }, (args, extra) => typeof args.pack === 'string' && args.pack
166
+ ? api(`gamekit-projects/${enc(args.project_id)}/packs`, { pack: args.pack, existing: args.existing, dry_run: args.dry_run, reason: args.reason }, 'POST', extra?.signal)
167
+ : api(`gamekit-projects/${enc(args.project_id)}/kits`, { ...body(args), id: args.id }, 'POST', extra?.signal));
136
168
  tool('gripforge_gamekit_remove', 'Uninstall a Game Kit from a project', 'Uninstall a kit from a Game Kit project (gkp_…). Remove a kit the user installed (addedBy user in gripforge_gamekit_installed) only when the user asks. game.engine (the engine of the game) and the default kits (ui.menu, input.remap, settings.graphics, ui.prompts, ui.endscreen, save.persistence, audio.core, i18n.text, ui.theme, input.virtualpad, addedBy default) are removed only when the user explicitly asks — never to "clean up". A kit other kits depend on, and game.engine always, is not removed by a first call: it answers 409 (kit_in_use, or confirm_required) with details { dependents, confirm } — nothing applied; show the user the dependents, then call again with with_dependents=true and confirm=<token> to remove the kit and its dependents together (the token is bound to the project revision). dry_run=true returns the plan, the dependents and the token without writing. force=true (legacy) removes it and disables its dependents instead; prune=true also drops the dependencies nothing else needs (never game.engine nor a default kit). Returns { applied, removed[], dependents, project }. 0 credits.', {
137
169
  project_id: projectId,
138
170
  id: kitId,
@@ -247,18 +279,23 @@ export function registerGameKitTools(register, options, schema = z) {
247
279
  }, { readOnly: true }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/capabilities`, args, 'GET', extra?.signal, {
248
280
  goal: typeof args.goal === 'string' ? args.goal : undefined,
249
281
  }));
250
- tool('gripforge_game_project', 'List, create, read, bind, feed or delete Game Kit projects', 'Manage Game Kit projects (gkp_…): the container holding installed kits, their lockfile, asset bindings and data collections. action=list lists the workspace projects; create needs name and takes a preset (see gripforge_gamekit_search; adventure is the reference game of game.engine) or an explicit kits map — every new project gets game.engine first (the engine of the game: scenes, archetypes, scripts, life cycle; its answer carries the engine start pack { structure, scene, blocks, order, next, rules } and added, the kits installed for dependencies), then the default kits ui.menu (title / pause / options / load and save screen), input.remap (key remapping), settings.graphics (graphics options), ui.prompts (on-screen key prompts that follow remapping and the gamepad), ui.endscreen (end screen and credits), save.persistence (save slots, quick save, autosave), audio.core (game audio: mixer, music, effects, event → sound table), i18n.text (game texts in the player’s language: translation tables in the translations collection, ICU plurals) ui.theme (the design of every interface as kit config: colours, font, shapes, key glyphs; unset = built-in look) and input.virtualpad (the on-screen gamepad of touch screens, mode auto: shown on a touch screen without a gamepad, Touch controls tab), addedBy default: keep and configure them, leave one out ({ "ui.menu": false }) or remove it only if the user asks; get returns the full ProjectDetail (installed kits, bindings, capabilities, missing, playUrl); bind maps asset slots to Library items (lib_…, null to clear); data reads a collection or upserts / removes documents validated against the kits’ schemas (replace=true swaps the whole collection); delete needs confirm=true. Call this first to create or pick the project, then gripforge_gamekit_installed (existing project) or gripforge_gamekit_install. 0 credits.', {
251
- action: schema.enum(['list', 'create', 'get', 'bind', 'data', 'delete']).describe('list | create | get | bind | data | delete.'),
252
- project_id: projectId.optional().describe('Game Kit project id (gkp_…). Required for get, bind, data and delete.'),
282
+ tool('gripforge_game_project', 'List, create, read, bind, feed, set the controls of or delete Game Kit projects', 'Manage Game Kit projects (gkp_…): the container holding installed kits, their lockfile, asset bindings and data collections. action=list lists the workspace projects; create needs name and takes a preset (see gripforge_gamekit_search; adventure is the reference game of game.engine) or an explicit kits map — every new project gets game.engine first (the engine of the game: scenes, archetypes, scripts, life cycle; its answer carries the engine start pack { structure, scene, blocks, order, next, rules } and added, the kits installed for dependencies), then the default kits ui.menu (title / pause / options / load and save screen), input.remap (key remapping), settings.graphics (graphics options), ui.prompts (on-screen key prompts that follow remapping and the gamepad), ui.endscreen (end screen and credits), save.persistence (save slots, quick save, autosave), audio.core (game audio: mixer, music, effects, event → sound table), i18n.text (game texts in the player’s language: translation tables in the translations collection, ICU plurals) ui.theme (the design of every interface as kit config: colours, font, shapes, key glyphs; unset = built-in look) and input.virtualpad (the on-screen gamepad of touch screens, mode auto: shown on a touch screen without a gamepad, Touch controls tab), addedBy default: keep and configure them, leave one out ({ "ui.menu": false }) or remove it only if the user asks; get returns the full ProjectDetail (installed kits, bindings, capabilities, missing, playUrl); bind maps asset slots to Library items (lib_…, null to clear); data reads a collection or upserts / removes documents validated against the kits’ schemas (replace=true swaps the whole collection); controls reads or sets the controls: without scheme it returns the control schemes of the catalogue (fps, third_person, moba_click, top_down, platformer, fighting, vehicle, rts, point_click: movement by keys or by click, what the mouse is for, expected actions and keys, touch layout), the project scheme, the one its kits suggest, every action with its effective key and the keys two actions share in one mode (warnings with a proposed reassignment, never a refusal — also in get → report.controls); with scheme and dry_run=true it returns the diff of keys the scheme would write; with scheme alone it applies it (input.actions overrides marked source: scheme, the touch layout of input.virtualpad, the help bar of ui.prompts). A scheme is a starting proposal: a preset applies its own at creation (create takes controls to pick another one, or none), and any key stays changeable with gripforge_gamekit_configure input.actions { overrides: [{ action, keys, buttons?, disabled? }] } — the action of any kit; those entries always win over the scheme. delete needs confirm=true. Call this first to create or pick the project, then gripforge_gamekit_installed (existing project) or gripforge_gamekit_install. 0 credits.', {
283
+ action: schema.enum(['list', 'create', 'get', 'bind', 'data', 'controls', 'delete']).describe('list | create | get | bind | data | controls | delete.'),
284
+ project_id: projectId.optional().describe('Game Kit project id (gkp_…). Required for get, bind, data, controls and delete.'),
253
285
  name: schema.string().min(1).max(120).optional().describe('create: project name.'),
254
286
  preset: schema.string().max(80).optional().describe('create: preset id whose default kits are installed (gripforge_gamekit_search returns presets).'),
255
287
  kits: schema.record(schema.string(), schema.boolean()).optional().describe('create: explicit kit toggles { "vehicle.driveable": true, … } on top of the preset. Unchecking a required kit fails with toggle_requires. The default kits (ui.menu, input.remap, settings.graphics, ui.prompts, ui.endscreen, save.persistence, audio.core, i18n.text, ui.theme, input.virtualpad) are included unless set to false here — only when the user asked for it.'),
256
288
  target: schema.enum(['web', 'godot', 'unity', 'unreal']).optional().describe('create: pass the confirmed engine explicitly (Three.js=web). Ask the user to choose Unity / Godot / Unreal Engine / Three.js before creating if unknown. API default web is for compatibility, not an engine choice.'),
257
- bindings: schema.record(schema.string(), schema.string().nullable()).optional().describe('bind: { slot: lib_… | null } — Library item per asset slot declared by the installed kits, null clears.'),
289
+ bindings: schema.record(schema.string(), schema.string().nullable()).optional().describe('bind: { slot: lib_… | null } — Library item per asset slot declared by the installed kits, null clears. A model bound to a creature slot gets its animations by itself as far as that is free (the answer carries the chain in creatures); see gripforge_gamekit_creatures.'),
258
290
  collection: schema.string().max(80).optional().describe('data: collection name declared by an installed kit (e.g. missions, npcs, zones).'),
259
291
  documents: schema.array(schema.record(schema.string(), schema.unknown())).max(500).optional().describe('data: documents to upsert (each with its id), validated against the kit schema.'),
260
292
  remove: schema.array(schema.string().max(120)).max(500).optional().describe('data: document ids to remove.'),
261
293
  replace: schema.boolean().optional().describe('data: true to replace the whole collection with documents.'),
294
+ scheme: schema.string().max(40).optional().describe('controls: the control scheme to show (dry_run) or apply — fps, third_person, moba_click, top_down, platformer, fighting, vehicle, rts, point_click. Omit to read the schemes and the project controls.'),
295
+ dry_run: schema.boolean().optional().describe('controls: true returns the diff of keys without writing.'),
296
+ force: schema.boolean().optional().describe('controls: also replace a touch layout or a help bar the game edited (kept by default).'),
297
+ reset: schema.boolean().optional().describe('controls: also drop the key overrides the game wrote itself (kept by default), so the game ends exactly on the scheme — what a game made before the schemes needs to move to one. Preview with dry_run.'),
298
+ controls: schema.string().max(40).optional().describe('create: control scheme applied at creation instead of the preset’s own (or of the one the kits suggest); none keeps the kits’ own keys.'),
262
299
  confirm: schema.boolean().optional().describe('delete: must be true. The project, its revisions and data are removed.'),
263
300
  }, { readOnly: false }, (args, extra) => {
264
301
  const action = String(args.action);
@@ -267,13 +304,14 @@ export function registerGameKitTools(register, options, schema = z) {
267
304
  if (action === 'create') {
268
305
  if (typeof args.name !== 'string' || !args.name.trim())
269
306
  return Promise.resolve(fail('create needs name.'));
270
- return api('gamekit-projects', body(args, 'bindings', 'collection', 'documents', 'remove', 'replace'), 'POST', extra?.signal);
307
+ return api('gamekit-projects', body(args, 'bindings', 'collection', 'documents', 'remove', 'replace', 'scheme', 'dry_run', 'force', 'reset'), 'POST', extra?.signal);
271
308
  }
272
309
  if (typeof args.project_id !== 'string')
273
310
  return Promise.resolve(fail(`${action} needs project_id (gkp_…).`));
274
311
  const project = enc(args.project_id);
312
+ // creatures=1: the answer carries ranked models for every creature slot that would draw a capsule.
275
313
  if (action === 'get')
276
- return api(`gamekit-projects/${project}`, args, 'GET', extra?.signal);
314
+ return api(`gamekit-projects/${project}`, args, 'GET', extra?.signal, { creatures: 1 });
277
315
  if (action === 'bind') {
278
316
  if (!args.bindings || typeof args.bindings !== 'object')
279
317
  return Promise.resolve(fail('bind needs bindings { slot: lib_… | null }.'));
@@ -287,6 +325,18 @@ export function registerGameKitTools(register, options, schema = z) {
287
325
  return api(`gamekit-projects/${project}/data`, args, 'GET', extra?.signal, { collection: args.collection });
288
326
  return api(`gamekit-projects/${project}/data`, { workspace_id: args.workspace_id, collection: args.collection, upsert: args.documents, remove: args.remove, replace: args.replace }, 'PATCH', extra?.signal);
289
327
  }
328
+ if (action === 'controls') {
329
+ if (typeof args.scheme === 'string' && args.scheme) {
330
+ return api(`gamekit-projects/${project}/controls`, { workspace_id: args.workspace_id, scheme: args.scheme, dry_run: args.dry_run, force: args.force, reset: args.reset }, 'POST', extra?.signal);
331
+ }
332
+ return (async () => {
333
+ const [schemes, current] = await Promise.all([api('gamekits/control-schemes', args, 'GET', extra?.signal), api(`gamekit-projects/${project}/controls`, args, 'GET', extra?.signal)]);
334
+ if (current.isError)
335
+ return current;
336
+ const data = { ...current.structuredContent, schemes: schemes.structuredContent?.schemes ?? [] };
337
+ return { structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
338
+ })();
339
+ }
290
340
  if (action === 'delete') {
291
341
  if (args.confirm !== true)
292
342
  return Promise.resolve(fail('delete needs confirm=true.'));
@@ -294,6 +344,22 @@ export function registerGameKitTools(register, options, schema = z) {
294
344
  }
295
345
  return Promise.resolve(fail(`Unknown action ${action}.`));
296
346
  });
347
+ tool('gripforge_gamekit_creatures', 'Creatures of a project: a model and its animations for every unit', 'The living units of a Game Kit project (gkp_…) — player, heroes, enemies, bosses, minions, monsters, NPCs: the creature slots its kits declare. Two rules: NO creature ships as a capsule, and a model ships WITH its animations (movement AND action). action=report (default): every creature slot with its status (bound | fallback = a capsule in game), the model bound, the clips the slot plays (locomotion, action), the ones the model lacks, and its chain (steps: clone | rig | animate | more_clips, each done / running / proposed with its tool, free or its credits). action=propose: ranked Library models for each unbound slot — the workspace first, then the Community — with the reasons of the rank (source, slot and role words, game theme, size, rigged, animated, triangle budget). YOU judge: read the reasons, pick the model that fits the game (any monster beats a capsule), bind it with gripforge_game_project { action: "bind" }; nothing is final, rebind any time. A slot with no proposal at all carries defect creature_no_model: generate a model (paid — quote and ask first), never leave the capsule. action=animate: run the free steps of the chain now for the bound models (a free Community model is copied into the workspace and bound in place of the original; the standard clip pack is retargeted, 0 credits; wait=true waits for the result, else it runs in the background — read report again). action=fill: bind the best free proposal to every unbound creature slot, then animate (what the hosted creation does). Nothing here spends credits: the automatic rig, a priced Community asset and generated motions always come back as proposed steps for you to quote and ask about. Binding a model through gripforge_game_project already starts its chain. 0 credits.', {
348
+ project_id: projectId,
349
+ action: schema.enum(['report', 'propose', 'animate', 'fill']).optional().describe('report (default) | propose | animate | fill.'),
350
+ slot: schema.string().max(80).optional().describe('report / propose: only this creature slot (e.g. enemy_grunt, moba_hero_3).'),
351
+ slots: schema.array(schema.string().max(120)).max(64).optional().describe('animate: only these slots (slot or slot:variant). Default: every bound creature slot.'),
352
+ limit: schema.number().int().min(1).max(12).optional().describe('propose: models per slot (default 5).'),
353
+ wait: schema.boolean().optional().describe('animate / fill: wait for the retargets instead of running them in the background.'),
354
+ brief: schema.string().max(2000).optional().describe('fill: what the game is about, to rank the models (theme, mood).'),
355
+ }, { readOnly: false }, (args, extra) => {
356
+ const action = typeof args.action === 'string' ? args.action : 'report';
357
+ const path = `gamekit-projects/${enc(args.project_id)}/creatures`;
358
+ if (action === 'report' || action === 'propose') {
359
+ return api(path, args, 'GET', extra?.signal, { propose: action === 'propose' ? 1 : undefined, slot: typeof args.slot === 'string' ? args.slot : undefined, limit: typeof args.limit === 'number' ? args.limit : undefined });
360
+ }
361
+ return api(path, { workspace_id: args.workspace_id, action, slots: args.slots, wait: args.wait, brief: args.brief }, 'POST', extra?.signal);
362
+ });
297
363
  tool('gripforge_game_engine', 'The engine of a game: structure, bricks, report, next steps', 'Read game.engine of a Game Kit project (gkp_…) — a direction, never a gate. action=start (default): the start pack { version, active, structure { engine, kits, content counts, code }, scene, blocks digest, order (scene → playable → assets → archetypes → story → scripts → ui → validate → deliver → code), next [{ step, tool, args, why }], code (where game code goes per engine), rules } and the full content report. action=blocks: the brick catalogue computed from the manifests — entity kinds, archetype components (kit, schema, example), script verbs (conditions / actions with arguments and shorthand), script sugars (interact, enter, leave, scene, talked, quest, state, gives), quest step types, NPC behaviours, worlds, layers, events, variables; scope=catalog adds the bricks of kits not installed (install <kit>). action=report: { ok, errors, warnings, stats } — an error means that piece is left out of the game (the rest plays), a warning that it loads with something missing; each issue may carry a fix { tool, args }. action=next: the next steps only. A game with everything in code is valid: content first, kits second, code for the rest. 0 credits.', {
298
364
  project_id: projectId,
299
365
  action: schema.enum(['start', 'structure', 'blocks', 'report', 'next']).optional().describe('start (default) | structure | blocks | report | next.'),
@@ -363,6 +429,14 @@ export function registerGameKitTools(register, options, schema = z) {
363
429
  replay: schema.record(schema.string(), schema.unknown()).optional().describe('report.replay of an earlier run: plays the same inputs again (seed and budget come from it).'),
364
430
  lang: schema.enum(['en', 'fr']).optional().describe('Language of text (default en).'),
365
431
  }, { readOnly: true }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/playtest`, body(args), 'POST', extra?.signal));
432
+ tool('gripforge_game_audit', 'Audit a game: what is left before it is finished', 'One report of everything that stands between a Game Kit project (gkp_…) and a FINISHED game of its genre — run it before telling the user a game is done, and again after each round of fixes. It aggregates, by severity (blocker / major / minor): creatures (slots drawn as a capsule, models without a skeleton, missing movement or action clips, the animation chain in progress), controls (no control scheme, the suggested one, keys shared by two actions in one mode, playable actions without a key), content (missing capabilities, required slots, the game.engine report), the last headless playtest (failures, warnings, the keys / models / animations probes, capabilities no probe covers) and completeness: what a finished game of the genre has beyond the literal request (MOBA: distinct heroes with Q/W/E/R, minions, towers, jungle monsters, shop, respawn; FPS: weapon, ammo, reload, crosshair, enemies; platformer: checkpoints, collectibles, end of level; racing: laps, ranking, AI opponents… and for every game a way to win AND to lose, title / pause / end screens, sound, a HUD, touch controls), checked against facts of the project, never a declaration. Each defect carries fix { tool, args, note, auto?, credits? }: call it as given. Returns { verdict: complete | playable_with_defects | incomplete, headline, genre, counts, defects, next (ordered calls), sections, text, play_url }. fix=true first applies what is free and safe (the best free Library model on each capsule — rebind at will —, the free standard clips, the suggested control scheme) and reports it in fixes. playtest="run" plays the game headless first (5–20 s); the default reads the last playtest and says when it is stale. Never refuses a write and never spends: paid fixes (a priced model, a rig, a generation) come back with their price to ask the user about. Aim for verdict complete; if you stop before, tell the user exactly what is left. 0 credits.', {
433
+ project_id: projectId,
434
+ fix: schema.boolean().optional().describe('true: apply the free and safe fixes first (free models on capsules, standard clips, suggested control scheme), then read. Nothing paid is ever done.'),
435
+ playtest: schema.enum(['last', 'run']).optional().describe('last (default): read the stored playtest of the project. run: play it headless now, then read.'),
436
+ brief: schema.string().max(2000).optional().describe('Free words about the game (theme, setting): they weigh in the model proposals.'),
437
+ }, { readOnly: false }, (args, extra) => (args.fix === true || args.playtest === 'run'
438
+ ? api(`gamekit-projects/${enc(args.project_id)}/audit`, body(args), 'POST', extra?.signal, undefined, 280_000)
439
+ : api(`gamekit-projects/${enc(args.project_id)}/audit`, { workspace_id: args.workspace_id }, 'GET', extra?.signal, { brief: typeof args.brief === 'string' ? args.brief : undefined })));
366
440
  tool('gripforge_moba_roster', 'Set the champions of a MOBA project', 'Turn champions into the playable roster of a moba project (preset "moba"). Pass the Library ids of 1 to 10 champions that carry an ability pack (the items made by gripforge_abilities_generate: rigged model + basic_attack and ability_q…r clips). Writes the moba_heroes and abilities collections (ids <hero>__ability_q), binds each champion to moba_hero_<n> and the player\'s champion (player, else the first) to player_character. Bots play the other champions. Returns the heroes with their attack and Q/W/E/R. Then gripforge_game_play_url to play. 0 credits.', {
367
441
  project_id: projectId,
368
442
  heroes: schema.array(schema.string()).min(1).max(10).describe('Library ids (lib_…) of champions with an ability pack, in roster order.'),
@@ -123,6 +123,8 @@ export function registerSceneTools(register, options, schema = z) {
123
123
  tool('gripforge_scene_proposal_apply', 'Apply a reviewed proposal to work', 'Explicitly accept or discard a proposal after inspecting it. Accept requires expectedRevision to match the exact base and current work revision. Atomic: concurrent changes or discard abort the save. Creates work, never promotes validated.', { id: identifier, proposal: identifier, action: schema.enum(['accept', 'discard']), expectedRevision: revision.optional() }, false, (args, extra) => call(`scenes/${args.id}/proposals/${args.proposal}`, args, 'POST', extra?.signal));
124
124
  tool('gripforge_scene_schema', 'Shared scene engine format', 'Start here. Read the shared scene model, kinds, commands, limits and an editable example. Assets are immutable revisions; placed objects are independent instances. Scene saves create work revisions, never automatic visual approval.', {}, true, (args, extra) => call('scenes/schema', args, 'GET', extra?.signal));
125
125
  tool('gripforge_scene_realism', 'Shared materials, automotive lighting and post-processing', 'Apply reusable architectural surface profiles and GTAO to a saved work scene. Read gripforge_scene_schema.realism first. assignments [{nodeId,material:"*" or a named slot,surface:{preset:preserve|brick|stone|metal|glass,detail:0..1,weathering:0..1,regions:[]}}]; surface:null removes only this local profile. Optional glass regions use explicit normalized mesh-local min/max bounds and interior parallax settings. Never guess masks from colours. ambientOcclusion {enabled,radius:0.02..3 metres,intensity:0..1,quality:low|medium|high}, or null to remove. Preserves transforms, source PBR maps, other instances and validated revisions. Returns Studio link for visual comparison. Optional quality {preset:performance|balanced|quality,resolutionScale:0.5..1,sharpness:0..0.5}, or null to restore the original presentation. Quality supports viewport resolution up to a bounded 4K budget. Optional automotive {look:studio|daylight,quality,lengthM,depthOfField:false,focusDistanceM} explicitly applies a whole-scene look: HDR strip reflections, contact AO, quality and restrained post-processing. Studio replaces an existing HDRI; daylight keeps it. Optional colorGrading/depthOfField overrides or null. DOF is perspective/capture only, disabled with volumetric passes. No generation charge, DLSS or automatic approval.', { id: identifier, expectedRevision: revision, assignments: schema.array(schema.object({ nodeId: identifier, material: schema.string().min(1).max(256).optional(), surface: record.nullable() })).max(100).optional(), ambientOcclusion: schema.object({ enabled: schema.boolean(), radius: schema.number().min(.02).max(3), intensity: schema.number().min(0).max(1), quality: schema.enum(['low', 'medium', 'high']) }).nullable().optional(), quality: schema.object({ preset: schema.enum(['performance', 'balanced', 'quality']), resolutionScale: schema.number().min(.5).max(1).optional(), sharpness: schema.number().min(0).max(.5).optional() }).nullable().optional(), automotive: schema.object({ look: schema.enum(['studio', 'daylight']).optional(), quality: schema.enum(['performance', 'balanced', 'quality']).optional(), lengthM: schema.number().min(1).max(25).optional(), ground: schema.boolean().optional(), depthOfField: schema.boolean().optional(), focusDistanceM: schema.number().min(.1).max(1000).optional() }).optional(), colorGrading: schema.object({ contrast: schema.number().min(.5).max(1.5), saturation: schema.number().min(0).max(1.5), temperature: schema.number().min(-1).max(1) }).nullable().optional(), depthOfField: schema.object({ enabled: schema.boolean(), focusDistanceM: schema.number().min(.1).max(1000), aperture: schema.number().min(0).max(.01), maxBlur: schema.number().min(0).max(.02) }).nullable().optional() }, false, (args, extra) => call(`scenes/${args.id}/realism`, args, 'POST', extra?.signal));
126
+ tool('gripforge_apply_realism', 'Orchestrate scene realism', 'Plan or apply render.realism across shared PBR materials, immutable textures, surface shaders, lighting, post-processing, environment and skinned character rendering, with a bounded render quality and render.lod configuration suggestion. Defaults to mode=plan (no save); mode=apply writes one work revision atomically, preserving assets, transforms, animation and validated versions. Explicit nodeIds restrict instance assignments; lighting/post-processing are global. materials/characters rows use {nodeId,material:"*"|exactName,override:{roughness,metalness,map,normalMap,surface,...}}; pinned map references, null removes overrides. Characters require character/enemy/boss/fps-arms role. Does not fabricate missing detail, execute paid generation, add game LOD automatically, or approve realism visually. Request a real scene_review afterwards. WEB only; not DLSS.', { id: identifier, expectedRevision: revision, mode: schema.enum(['plan', 'apply']).default('plan'), quality: schema.enum(['performance', 'balanced', 'quality']).optional(), lighting: schema.enum(['preserve', 'daylight', 'golden_hour', 'overcast', 'night']).optional(), nodeIds: schema.array(identifier).min(1).max(100).optional(), materials: schema.array(schema.object({ nodeId: identifier, material: schema.string().min(1).max(256).optional(), override: record })).max(100).optional(), characters: schema.array(schema.object({ nodeId: identifier, material: schema.string().min(1).max(256).optional(), override: record })).max(100).optional(), environment: record.optional(), postProcess: record.optional() }, false, (args, extra) => call(`scenes/${args.id}/render-realism`, args, 'POST', extra?.signal));
127
+ tool('gripforge_realism_schema', 'Read realism orchestration contract', 'Read render.realism systems, modes, bounds and immutable-source workflow for an accessible scene.', { id: identifier }, true, (args, extra) => call(`scenes/${args.id}/render-realism`, args, 'GET', extra?.signal));
126
128
  tool('gripforge_scene_optimize', 'Prepare a scene for fast browser loading', 'Queue resumable browser texture variants for a saved scene. Preserve original GLBs, geometry, transforms, PBR, animations and Library heads. Color textures up to 1024px, data maps up to 512px and terrain color up to 4096px; WebP encoding. Saves a new work revision only if expectedRevision still matches. Does not promote validated. Poll generation_read for byte savings and studio_url. 0 credits; storage quota applies. New Unreal imports do this automatically.', { id: identifier, expectedRevision: revision, idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call(`scenes/${args.id}/optimize`, args, 'POST', extra?.signal));
127
129
  tool('gripforge_scene_list', 'List workspace scenes', 'List scenes, work/validated revision pointers and links in the authenticated workspace.', {}, true, (args, extra) => call('scenes', args, 'GET', extra?.signal));
128
130
  tool('gripforge_scene_read', 'Read scene source', 'Read the editable SceneDocument at work, validated, or a numbered revision. Keep work_revision for optimistic writes. Validated can be absent.', { id: identifier, version: schema.union([schema.enum(['work', 'validated']), schema.number().int().positive()]).optional() }, true, (args, extra) => call(`scenes/${encodeURIComponent(String(args.id))}?version=${args.version ?? 'work'}`, args, 'GET', extra?.signal));
package/dist/server.js CHANGED
@@ -1095,7 +1095,7 @@ server.tool('gripforge_hand_check', 'Verify that a rigged character\'s fingers f
1095
1095
  return err(String(data.error ?? res.statusText));
1096
1096
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
1097
1097
  });
1098
- server.tool('gripforge_animate', 'Retarget the standard clip pack onto a Mixamo-named Library character/enemy/armed bind. Base slots (idle, walk, run, attack1-3, hit, death, dodge) + extras (taunt, stinger, knockback, block, jump_start/loop/land, idle_guns, shoot, reload, aim, crouch_idle, crouch_walk, knife). archetype remaps the base slots: sword (default), claws (zombie scratch), heavy (slow + heavy combo), brawler (punches), puppet (stiff walk + throws), operator (CS-style: knife/punch strikes). Opt-in slots fire_breath (mouth exhalation) and energy_cast (two-hand azure wave) use the Studio VFX character motions. Saves kind=animation. 0 credits.', {
1098
+ server.tool('gripforge_animate', 'Retarget the standard clip pack onto a Mixamo-named Library character/enemy/armed bind. In a Game Kit project gripforge_gamekit_creatures runs it for free on every bound creature, and gripforge_game_audit lists the models still lacking movement or action clips. Base slots (idle, walk, run, attack1-3, hit, death, dodge) + extras (taunt, stinger, knockback, block, jump_start/loop/land, idle_guns, shoot, reload, aim, crouch_idle, crouch_walk, knife). archetype remaps the base slots: sword (default), claws (zombie scratch), heavy (slow + heavy combo), brawler (punches), puppet (stiff walk + throws), operator (CS-style: knife/punch strikes). Opt-in slots fire_breath (mouth exhalation) and energy_cast (two-hand azure wave) use the Studio VFX character motions. Saves kind=animation. 0 credits.', {
1099
1099
  character_id: z.string().describe('Library character, enemy, or armed bind'),
1100
1100
  archetype: z.enum(['sword', 'claws', 'heavy', 'brawler', 'puppet', 'operator']).optional().describe('Movement/strike archetype — remaps the base slots (default sword); operator = CS-style (knife/punch strikes, guns via idle_guns/shoot/reload/aim, crouch_idle/crouch_walk)'),
1101
1101
  slots: z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gripforgeai/mcp",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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",