@gripforgeai/mcp 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +275 -4
- package/dist/ability-tools.js +21 -0
- package/dist/architecture-tools.js +73 -0
- package/dist/asset-production-tools.js +101 -0
- package/dist/creature-rig-tools.js +41 -0
- package/dist/game-concept-tools.js +56 -0
- package/dist/game-creation-tools.js +45 -0
- package/dist/gamekit-deliver-local.js +5 -7
- package/dist/gamekit-tools.js +127 -24
- package/dist/hero-ability-tools.js +48 -0
- package/dist/joystick-kit/godot.js +146 -0
- package/dist/joystick-kit/index.js +32 -0
- package/dist/joystick-kit/threejs.js +93 -0
- package/dist/joystick-kit/unity.js +182 -0
- package/dist/joystick-kit/unreal.js +231 -0
- package/dist/joystick-tools.js +26 -0
- package/dist/map-unreal-export-local.js +285 -0
- package/dist/map-unreal-import-local.js +421 -0
- package/dist/map-unreal-local.js +75 -0
- package/dist/motion-tools.js +50 -0
- package/dist/projectile-tools.js +28 -0
- package/dist/scene-tools.js +77 -13
- package/dist/server-tools.js +3 -3
- package/dist/server.js +164 -22
- package/dist/shield-vfx-tools.js +29 -0
- package/dist/studio-tools-tools.js +106 -0
- package/dist/unreal-export-package.js +212 -0
- package/dist/weapon-motion-tools.js +34 -0
- package/package.json +6 -1
- package/runtime/unreal-scene-export.py +341 -0
- package/runtime/unreal-scene-import.py +622 -0
- package/runtime/unreal-scene-upload.mjs +7 -0
- package/runtime/unreal_scene_math.py +96 -0
- package/runtime/unreal_spawn_review.py +135 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export const GAME_CONCEPT_TOOL_NAMES = ['gripforge_game_concepts'];
|
|
3
|
+
const SLOTS = ['heroes', 'bosses', 'enemies', 'npcs', 'weapons', 'vehicles', 'buildings', 'props', 'textures', 'vfx', 'maps'];
|
|
4
|
+
/**
|
|
5
|
+
* Concepts of a whole game, reused before generated. Hosted and local MCP share the same REST
|
|
6
|
+
* workflow: GET the catalog, POST stage=plan (free), then stage=build with an explicit budget.
|
|
7
|
+
*/
|
|
8
|
+
export function registerGameConceptTools(register, options, schema = z) {
|
|
9
|
+
const counts = schema.object(Object.fromEntries(SLOTS.map((s) => [s, schema.number().int().min(0).max(96).optional()]))).strict();
|
|
10
|
+
const shape = {
|
|
11
|
+
stage: schema.enum(['catalog', 'plan', 'build']).optional().describe('catalog: known games, blueprints (slot counts) and theme packs. plan (default, free): what already exists and is reused, what would be generated, and the price. build: generate the missing concepts, requires budget.'),
|
|
12
|
+
game: schema.string().min(2).max(120).optional().describe('The game the user named, as written ("God of War", "like Dark Souls", "a GTA"). Matched against titles and aliases of the catalog.'),
|
|
13
|
+
custom: schema.object({
|
|
14
|
+
title: schema.string().min(2).max(80),
|
|
15
|
+
blueprint: schema.string().max(60).describe('Closest blueprint from stage=catalog, e.g. third-person-action, open-world-rpg.'),
|
|
16
|
+
pack: schema.string().max(60).describe('Closest theme pack from stage=catalog, e.g. nordic-mythology, cyberpunk.'),
|
|
17
|
+
build: schema.string().min(10).max(300).describe('One sentence describing the original game to make.'),
|
|
18
|
+
}).strict().optional().describe('Only when `game` is not in the catalog: describe the game with the closest blueprint and pack.'),
|
|
19
|
+
counts: counts.optional().describe('Concepts to generate per slot (heroes up to 24, maps up to 6: the first is the whole playfield from above, the others are zones seen through the game camera). Default: what the blueprint needs minus what is already reusable.'),
|
|
20
|
+
hero_kits: schema.boolean().optional().describe('Each hero comes with its ability kit — role, lane, basic attack, passive + Q/W/E/R (type, range, cooldown, cost, effect, VFX, cast animation) — and a sheet of 5 ability icons in its colours (1 more image per hero). Default from the blueprint: on for lane-arena (MOBA). Turn it on for any game whose heroes have abilities (hero shooter, ARPG).'),
|
|
21
|
+
provider: schema.enum(['openai', 'xai']).optional().describe('Default openai (GPT Image 2.5 sunburst: final quality for style bible, heroes and bosses, medium-quality sheets of 6 for the rest).'),
|
|
22
|
+
budget: schema.object({ credits: schema.number().nonnegative().optional(), usd: schema.number().nonnegative().optional() }).strict().optional().describe('Required for build: maximum credits OR USD, at least the plan price.'),
|
|
23
|
+
workspace_id: schema.string().max(100).optional(),
|
|
24
|
+
idempotency_key: schema.string().regex(/^[a-zA-Z0-9_.:-]{8,160}$/).optional(),
|
|
25
|
+
};
|
|
26
|
+
register(GAME_CONCEPT_TOOL_NAMES[0], {
|
|
27
|
+
title: 'Game concepts',
|
|
28
|
+
description: 'Concept art for a whole game, reused before generated. Use it when a user asks for a game "like God of War", "a GTA", "a Zelda": call stage=plan with the game they named. The plan lists concepts that already exist for that game (GripForge community catalog or the workspace) and theme assets already in the library: reuse them first. It then prices the missing concepts: one style bible, heroes and bosses one image each (MOBA and hero games: each hero with its ability kit and ability icons, see hero_kits), enemies, NPCs, weapons, vehicles, buildings, props and VFX in sheets of 6 cut automatically, textures in sheets of 2 turned into seamless 1024 px textures. Maps (counts.maps) are environment paintings, one image each: the level is then built and dressed to match them. Show the plan and price to the user; only call stage=build with a budget after they agree. Concepts are private review images; nothing is sent to 3D. The plan also returns ambient_kits: the kits of the theme\'s atmosphere (weather, lighting, snow tracks) with their config; install them in the game project next to the blueprint\'s gameplay kits.',
|
|
29
|
+
inputSchema: shape,
|
|
30
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
31
|
+
}, async (args, extra) => {
|
|
32
|
+
const parsed = schema.object(shape).strict().safeParse(args);
|
|
33
|
+
if (!parsed.success)
|
|
34
|
+
return { isError: true, content: [{ type: 'text', text: parsed.error.message }] };
|
|
35
|
+
const key = options.getApiKey();
|
|
36
|
+
const { workspace_id, stage = 'plan', ...body } = parsed.data;
|
|
37
|
+
if (!key && stage !== 'catalog')
|
|
38
|
+
return { isError: true, content: [{ type: 'text', text: 'GripForge API key required.' }] };
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(options.apiUrl.replace(/\/$/, '') + '/api/v1/game-concepts', {
|
|
41
|
+
method: stage === 'catalog' ? 'GET' : 'POST',
|
|
42
|
+
headers: { 'content-type': 'application/json', 'x-gripforge-client': 'mcp', ...(key ? { 'x-api-key': key } : {}), ...(typeof workspace_id === 'string' ? { 'x-workspace-id': workspace_id } : {}) },
|
|
43
|
+
...(stage === 'catalog' ? {} : { body: JSON.stringify({ ...body, stage }) }),
|
|
44
|
+
signal: AbortSignal.any([AbortSignal.timeout(60_000), ...(extra?.signal ? [extra.signal] : [])]),
|
|
45
|
+
});
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
for (const field of ['studio_url', 'status_url'])
|
|
48
|
+
if (typeof data[field] === 'string' && data[field].startsWith('/'))
|
|
49
|
+
data[field] = new URL(data[field], options.apiUrl).href;
|
|
50
|
+
return { ...(!response.ok ? { isError: true } : {}), structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Game concepts request failed.' }] };
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export const GAME_CREATION_TOOL_NAMES = ['gripforge_game_creation'];
|
|
3
|
+
export function registerGameCreationTools(register, options, schema = z) {
|
|
4
|
+
const shape = {
|
|
5
|
+
action: schema.enum(['list', 'get', 'assets', 'create', 'select', 'use_reference', 'approve_concept', 'approve_asset', 'quote', 'accept', 'cancel', 'retry', 'dismiss']),
|
|
6
|
+
id: schema.string().max(100).optional(), workspace_id: schema.string().max(100).optional(),
|
|
7
|
+
expected_revision: schema.number().int().positive().optional(), prompt: schema.string().max(2000).optional(),
|
|
8
|
+
engine: schema.enum(['web', 'unity', 'godot', 'unreal']).optional(), slot_id: schema.string().max(100).optional(),
|
|
9
|
+
mode: schema.enum(['select', 'generate', 'default']).optional(), asset_id: schema.string().max(100).optional(),
|
|
10
|
+
operation: schema.enum(['plan', 'refine', 'concept', 'generate', 'build']).optional(), quote_token: schema.string().max(100).optional(),
|
|
11
|
+
message: schema.string().trim().min(3).max(1500).optional(), allow_incomplete: schema.boolean().optional(),
|
|
12
|
+
source: schema.enum(['all', 'workspace', 'community']).optional(), q: schema.string().max(100).optional(),
|
|
13
|
+
};
|
|
14
|
+
register(GAME_CREATION_TOOL_NAMES[0], {
|
|
15
|
+
title: 'Prepare and resume a game',
|
|
16
|
+
description: 'The website and CLI share this saved 3D game creation workflow. Ask which engine (Unity, Godot, Unreal, Three.js) if unspecified; do not choose silently. create saves a brief without spending. quote(operation=plan) estimates AI preparation, accept(quote_token) starts its durable job ONLY after user approval of the shown price. get returns a persistent operation log, slots, studio_url and the same project to resume. Planning can select an appropriate preset OR compose real engine-compatible modules. A missing preset never means the genre needs a native engine. composition.modules pins all module versions; composition.tasks lists the custom logic still to implement. Assembly is a foundation, not a completed custom game: use the coding agent to finish those tasks on the selected engine (including web/Three.js). quote refine with message updates the same plan after approval, preserves selected assets and reviewed work, and persists the conversation. use_reference with slot_id and an owned image asset_id supplies an uploaded concept for visual review. assets searches compatible workspace/community items ranked by the brief and role; select chooses one or sets mode=generate with a prompt. For generation: quote concept, obtain approval, accept, poll get, show the concept, approve_concept only after visual user review; quote generate, obtain approval, accept, poll, show the model in its studio, approve_asset only after visual review. Never infer visual approval from technical success. quote build then accept acquires the selected community assets and binds them to a Game Kit prototype, without generating extra assets. With explicit allow_incomplete=true on the build quote, unreviewed models are excluded and missing slots use module defaults where available or remain empty. Show missing_assets before approval; never imply that an incomplete prototype is finished. Native engine projects still need CLI delivery/import; a web project has a play_url. Revision required for edits. Cancel/retry preserve completed steps. 2D unsupported. No automatic spending, invented asset IDs, or unrelated template substitutions.',
|
|
17
|
+
inputSchema: shape, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
18
|
+
}, async (args, extra) => {
|
|
19
|
+
const parsed = schema.object(shape).strict().safeParse(args);
|
|
20
|
+
if (!parsed.success)
|
|
21
|
+
return { isError: true, content: [{ type: 'text', text: parsed.error.message }] };
|
|
22
|
+
const key = options.getApiKey();
|
|
23
|
+
if (!key)
|
|
24
|
+
return { isError: true, content: [{ type: 'text', text: 'GripForge login required.' }] };
|
|
25
|
+
const { workspace_id, ...body } = parsed.data;
|
|
26
|
+
const read = ['list', 'get', 'assets'].includes(body.action);
|
|
27
|
+
const url = new URL('/api/v1/game-creations', options.apiUrl);
|
|
28
|
+
if (read)
|
|
29
|
+
for (const [k, v] of Object.entries(body))
|
|
30
|
+
if (v !== undefined)
|
|
31
|
+
url.searchParams.set(k, String(v));
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(url, { method: read ? 'GET' : 'POST',
|
|
34
|
+
headers: { 'content-type': 'application/json', 'x-api-key': key, 'x-gripforge-client': 'mcp', ...(workspace_id ? { 'x-workspace-id': workspace_id } : {}) },
|
|
35
|
+
...(read ? {} : { body: JSON.stringify(body) }), signal: AbortSignal.any([AbortSignal.timeout(60_000), ...(extra?.signal ? [extra.signal] : [])]) });
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
if (typeof data.studio_url === 'string')
|
|
38
|
+
data.studio_url = new URL(data.studio_url, options.apiUrl).href;
|
|
39
|
+
return { ...(!response.ok ? { isError: true } : {}), structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Creation request failed.' }] };
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -19,8 +19,9 @@ export const GAMEKIT_LOCAL_TOOL_NAMES = ['gripforge_gamekit_deliver_local', 'gri
|
|
|
19
19
|
const LOCK_PATH = 'gripforge/gamekits.lock.json';
|
|
20
20
|
const PREV_LOCK_PATH = 'gripforge/gamekits.lock.prev.json';
|
|
21
21
|
const BACKUP_DIR = 'gripforge/.backup';
|
|
22
|
-
const SCAN_ROOTS = ['gripforge', 'assets/gripforge'];
|
|
23
|
-
|
|
22
|
+
const SCAN_ROOTS = ['gripforge', 'assets/gripforge', 'addons/gripforge_gameplay', 'addons/gripforge_net'];
|
|
23
|
+
/** Godot: the kits, bound assets, and the GripForge addons (gameplay core, network transport) the delivery owns. */
|
|
24
|
+
const GODOT_ROOTS = ['gripforge/', 'assets/gripforge/', 'addons/gripforge_gameplay/', 'addons/gripforge_net/'];
|
|
24
25
|
const MAX_FILES = 5000;
|
|
25
26
|
const MAX_BINARY_BYTES = 256 * 1024 * 1024;
|
|
26
27
|
const DERIVED = ['.uid', '.import'];
|
|
@@ -67,7 +68,7 @@ function projectPath(dir, rel, opts) {
|
|
|
67
68
|
if (!norm.startsWith(`${BACKUP_DIR}/`) && segments.some((s) => s.startsWith('.')))
|
|
68
69
|
throw new LocalError('forbidden_path', `${what} ${rel} has a hidden segment`, { path: rel });
|
|
69
70
|
if (opts.godot && !GODOT_ROOTS.some((root) => norm.startsWith(root)))
|
|
70
|
-
throw new LocalError('forbidden_path', `${what} ${rel} is outside gripforge
|
|
71
|
+
throw new LocalError('forbidden_path', `${what} ${rel} is outside gripforge/, assets/gripforge/ and the GripForge addons`, { path: rel });
|
|
71
72
|
const absolute = resolve(dir, ...segments);
|
|
72
73
|
const back = relative(dir, absolute);
|
|
73
74
|
if (!back || back.startsWith('..') || isAbsolute(back))
|
|
@@ -423,14 +424,13 @@ export function registerGameKitLocalTools(register, options, schema = z) {
|
|
|
423
424
|
}
|
|
424
425
|
register('gripforge_gamekit_deliver_local', {
|
|
425
426
|
title: 'Deliver Game Kits into a local engine project',
|
|
426
|
-
description: 'npm client only. Deliver Game Kits straight into an engine project folder on this machine: hashes project_dir/gripforge/**, reads gripforge/gamekits.lock.json, asks the GripForge API for the plan, then executes plan.actions with fs — every overwritten or deleted file is copied to gripforge/.backup/<plan id>/ first and the lock is written last. Paths are checked inside project_dir (Godot: gripforge
|
|
427
|
+
description: 'npm client only. Deliver Game Kits straight into an engine project folder on this machine: hashes project_dir/gripforge/**, reads gripforge/gamekits.lock.json, asks the GripForge API for the plan, then executes plan.actions with fs — every overwritten or deleted file is copied to gripforge/.backup/<plan id>/ first and the lock is written last. Paths are checked inside project_dir (Godot: gripforge/, assets/gripforge/ and addons/gripforge_gameplay|gripforge_net only) and every hash is verified before anything is written. Run dry_run=true first. A blocked plan (files edited in the project) returns the conflicts without touching files: re-run with force to back them up and overwrite. Kits always deliver at their latest version. verify=true runs Godot headless (import, then each kit smoke.gd) when GODOT_BIN is set. Undo with gripforge_gamekit_rollback_local. Credits: the first delivery of a kit major version to an engine costs 1 credit per workspace; re-deliveries, updates within a major and dry runs are free.',
|
|
427
428
|
inputSchema: {
|
|
428
429
|
project_dir: projectDirField,
|
|
429
430
|
target: schema.enum(TARGETS).optional().describe('Engine of the project: godot (default), unity or unreal.'),
|
|
430
431
|
kits: schema
|
|
431
432
|
.array(schema.object({
|
|
432
433
|
id: kitId,
|
|
433
|
-
version: schema.string().max(40).optional().describe('Exact version; only the latest is deliverable.'),
|
|
434
434
|
config: schema.record(schema.string(), schema.unknown()).optional().describe('Kit config, validated against its configSchema.'),
|
|
435
435
|
bindings: schema.record(schema.string(), schema.string()).optional().describe('Asset slot → Library item id (lib_…).'),
|
|
436
436
|
}))
|
|
@@ -441,7 +441,6 @@ export function registerGameKitLocalTools(register, options, schema = z) {
|
|
|
441
441
|
mode: schema.enum(['install', 'update', 'uninstall']).optional().describe('install (default), update to the latest catalogue version, or uninstall (uses the lock).'),
|
|
442
442
|
dry_run: schema.boolean().optional().describe('true → plan only; nothing is written or charged.'),
|
|
443
443
|
force: schema.boolean().optional().describe('true → back up then overwrite edited managed files and existing seed files.'),
|
|
444
|
-
accept_breaking: schema.boolean().optional().describe('true → allow a breaking (major) update.'),
|
|
445
444
|
verify: schema.boolean().optional().describe('true → after writing, run Godot headless (import + smoke.gd per kit) when GODOT_BIN is set.'),
|
|
446
445
|
...workspace,
|
|
447
446
|
},
|
|
@@ -477,7 +476,6 @@ export function registerGameKitLocalTools(register, options, schema = z) {
|
|
|
477
476
|
project: { target, ...(engineVersion ? { engineVersion } : {}), lock, files },
|
|
478
477
|
dryRun,
|
|
479
478
|
force: args.force === true,
|
|
480
|
-
acceptBreaking: args.accept_breaking === true,
|
|
481
479
|
}, typeof args.workspace_id === 'string' ? args.workspace_id : undefined, extra?.signal);
|
|
482
480
|
if (status >= 400)
|
|
483
481
|
return toolResult({ ...context, applied: false, error: data.error ?? `HTTP ${status}`, code: data.code ?? 'http_error', details: data.details ?? {}, plan: planView(data.plan) }, true);
|
package/dist/gamekit-tools.js
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from 'zod/v4';
|
|
|
3
3
|
export const GAMEKIT_TOOL_NAMES = [
|
|
4
4
|
'gripforge_gamekit_search',
|
|
5
5
|
'gripforge_gamekit_get',
|
|
6
|
+
'gripforge_gamekit_installed',
|
|
6
7
|
'gripforge_gamekit_install',
|
|
7
8
|
'gripforge_gamekit_remove',
|
|
8
9
|
'gripforge_gamekit_configure',
|
|
@@ -11,7 +12,13 @@ export const GAMEKIT_TOOL_NAMES = [
|
|
|
11
12
|
'gripforge_gamekit_deliver',
|
|
12
13
|
'gripforge_game_capabilities',
|
|
13
14
|
'gripforge_game_project',
|
|
15
|
+
'gripforge_game_engine',
|
|
16
|
+
'gripforge_game_content',
|
|
14
17
|
'gripforge_game_play_url',
|
|
18
|
+
'gripforge_moba_roster',
|
|
19
|
+
'gripforge_ability_vfx',
|
|
20
|
+
'gripforge_moba_map',
|
|
21
|
+
'gripforge_terrain_map_use',
|
|
15
22
|
];
|
|
16
23
|
const enc = (v) => encodeURIComponent(String(v));
|
|
17
24
|
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.';
|
|
@@ -30,7 +37,6 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
30
37
|
.max(120)
|
|
31
38
|
.regex(/^[a-z0-9][a-z0-9._-]*$/i)
|
|
32
39
|
.describe('Kit id from the catalogue, dotted (e.g. vehicle.driveable, mission.objectives).');
|
|
33
|
-
const range = schema.string().max(80).optional().describe('Semver range to satisfy (e.g. ^1.2.0, 1.x). Default: latest compatible version.');
|
|
34
40
|
const config = schema
|
|
35
41
|
.record(schema.string(), schema.unknown())
|
|
36
42
|
.optional()
|
|
@@ -92,11 +98,11 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
92
98
|
delete out[k];
|
|
93
99
|
return out;
|
|
94
100
|
};
|
|
95
|
-
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,
|
|
101
|
+
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.', {
|
|
96
102
|
q: schema.string().max(200).optional().describe('Free text matched on id, name, description and tags (e.g. "drive a car", "wanted level").'),
|
|
97
103
|
capability: schema.string().max(120).optional().describe('Capability the kit must provide (exact id or prefix, e.g. vehicle.drive).'),
|
|
98
104
|
tag: schema.string().max(60).optional().describe('Tag filter (e.g. vehicle, mission, npc, legacy).'),
|
|
99
|
-
target: schema.enum(['web', 'godot', 'unity', 'unreal']).optional().describe('Engine target the kit must support.
|
|
105
|
+
target: schema.enum(['web', 'godot', 'unity', 'unreal']).optional().describe('Engine target the kit must support. For game creation, use the engine chosen by the user or identified in their project; ask Unity / Godot / Unreal Engine / Three.js if unknown. The API web default is not user consent.'),
|
|
100
106
|
project_id: projectId.optional().describe('Game Kit project id (gkp_…) to score results against. Default: the workspace’s most recently updated project.'),
|
|
101
107
|
limit: schema.number().int().min(1).max(100).optional().describe('Max kits returned (default 20).'),
|
|
102
108
|
}, { readOnly: true }, (args, extra) => api('gamekits', args, 'GET', extra?.signal, {
|
|
@@ -107,7 +113,7 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
107
113
|
project: typeof args.project_id === 'string' ? args.project_id : undefined,
|
|
108
114
|
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
109
115
|
}));
|
|
110
|
-
tool('gripforge_gamekit_get', 'Read one Game Kit', 'Read one Game Kit: its manifest (provides / requires capabilities, asset slots, data collections, events), README docs, config JSON schema with defaults, and
|
|
116
|
+
tool('gripforge_gamekit_get', 'Read one Game Kit', 'Read one Game Kit: its manifest (provides / requires capabilities, asset slots, data collections, events), README docs, config JSON schema with defaults, version and changelog (information only: projects always run the latest version), and summary { tagline, tags, targets, status, cover, coverUrl } with the featured image URL. Pass with_usage=true to also list the workspace projects that have it installed. Call this BEFORE gripforge_gamekit_configure to know which config keys exist. 0 credits.', {
|
|
111
117
|
id: kitId,
|
|
112
118
|
version: schema.string().max(40).optional().describe('Exact version to read (e.g. 1.0.0). Default: latest.'),
|
|
113
119
|
with_usage: schema.boolean().optional().describe('true to include usedBy: the workspace projects where this kit is installed.'),
|
|
@@ -115,30 +121,41 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
115
121
|
version: typeof args.version === 'string' ? args.version : undefined,
|
|
116
122
|
with: args.with_usage === true ? 'usage' : undefined,
|
|
117
123
|
}));
|
|
118
|
-
tool('
|
|
124
|
+
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));
|
|
125
|
+
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.', {
|
|
119
126
|
project_id: projectId,
|
|
120
127
|
id: kitId,
|
|
121
|
-
range,
|
|
122
128
|
config,
|
|
123
129
|
dry_run: schema.boolean().optional().describe('true to return the install plan without writing the project.'),
|
|
124
130
|
allow_planned: schema.boolean().optional().describe('true to accept kits still marked planned (0.x, no runtime yet). Default false.'),
|
|
131
|
+
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).'),
|
|
125
132
|
}, { readOnly: false }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/kits`, { ...body(args), id: args.id }, 'POST', extra?.signal));
|
|
126
|
-
tool('gripforge_gamekit_remove', 'Uninstall a Game Kit from a project', 'Uninstall a kit from a Game Kit project (gkp_…).
|
|
133
|
+
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.', {
|
|
127
134
|
project_id: projectId,
|
|
128
135
|
id: kitId,
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
dry_run: schema.boolean().optional().describe('true to return the removal plan (with the dependents and the confirm token) without writing.'),
|
|
137
|
+
with_dependents: schema.boolean().optional().describe('true to remove the kit AND the kits that depend on it (needs confirm).'),
|
|
138
|
+
confirm: schema.string().max(80).optional().describe('The confirm token of the previous answer, after the user saw the dependents.'),
|
|
139
|
+
force: schema.boolean().optional().describe('Legacy: remove even when other kits depend on it; they are disabled, not removed.'),
|
|
140
|
+
prune: schema.boolean().optional().describe('true to also remove dependencies that no remaining kit requires (never game.engine nor a default kit).'),
|
|
141
|
+
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).'),
|
|
131
142
|
}, { readOnly: false, destructive: true }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/kits/${enc(args.id)}`, args, 'DELETE', extra?.signal, {
|
|
132
143
|
force: args.force === true ? 1 : undefined,
|
|
133
144
|
prune: args.prune === true ? 1 : undefined,
|
|
145
|
+
dry_run: args.dry_run === true ? 1 : undefined,
|
|
146
|
+
with_dependents: args.with_dependents === true ? 1 : undefined,
|
|
147
|
+
confirm: typeof args.confirm === 'string' && args.confirm ? args.confirm : undefined,
|
|
148
|
+
reason: typeof args.reason === 'string' && args.reason.trim() ? args.reason : undefined,
|
|
134
149
|
}));
|
|
135
|
-
tool('gripforge_gamekit_configure', 'Configure an installed Game Kit', 'Tune an installed kit: merge config values validated against the kit configSchema (read it with gripforge_gamekit_get), or toggle enabled to switch the kit off without uninstalling it. Returns { kit, project } with the new project revision. Call this AFTER gripforge_gamekit_install. 0 credits.', {
|
|
150
|
+
tool('gripforge_gamekit_configure', 'Configure an installed Game Kit', 'Tune an installed kit: merge config values validated against the kit configSchema (read it with gripforge_gamekit_get), or toggle enabled to switch the kit off without uninstalling it. Configure installed kits freely rather than recoding what they do (game.engine: startScene, startEntry, flow.title, story flags). Switching game.engine off follows the confirmation of a removal: the first call answers 409 confirm_required with details.confirm; send it back with enabled=false after the user agreed — never on your own. Returns { kit, project } with the new project revision. Call this AFTER gripforge_gamekit_install. 0 credits.', {
|
|
136
151
|
project_id: projectId,
|
|
137
152
|
id: kitId,
|
|
138
153
|
config,
|
|
139
154
|
enabled: schema.boolean().optional().describe('false to disable the kit at runtime while keeping it installed; true to re-enable.'),
|
|
140
|
-
|
|
141
|
-
|
|
155
|
+
confirm: schema.string().max(80).optional().describe('game.engine enabled=false: the confirm token of the previous answer.'),
|
|
156
|
+
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).'),
|
|
157
|
+
}, { readOnly: false }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/kits/${enc(args.id)}`, { ...body(args), ...(typeof args.confirm === 'string' ? { confirm: args.confirm } : {}) }, 'PATCH', extra?.signal));
|
|
158
|
+
tool('gripforge_gamekit_dependencies', 'Dependency graph of a project or kit', 'Dependency graph of a Game Kit project: nodes (installed kits with version, provides, requires), edges labelled by capability, same-scene overlaps (conflicts: several enabled providers of one exclusive capability) and unresolved requirements. Pass project_id for the installed graph; pass a kit id instead to read the declared requires / provides tree of a catalogue kit before installing it. Call this BEFORE gripforge_gamekit_remove with force. 0 credits.', {
|
|
142
159
|
project_id: projectId.optional().describe('Game Kit project id (gkp_…). Default: the workspace’s most recently updated project when id is not given.'),
|
|
143
160
|
id: kitId.optional().describe('Catalogue kit id to inspect instead of a project (returns its manifest tree).'),
|
|
144
161
|
}, { readOnly: true }, (args, extra) => {
|
|
@@ -148,11 +165,10 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
148
165
|
return Promise.resolve(fail('Pass project_id (gkp_…) or a kit id.'));
|
|
149
166
|
return api(`gamekit-projects/${enc(args.project_id)}/dependencies`, args, 'GET', extra?.signal);
|
|
150
167
|
});
|
|
151
|
-
tool('gripforge_gamekit_update', '
|
|
168
|
+
tool('gripforge_gamekit_update', 'Bring Game Kits to their latest version', 'Projects always follow the latest version of their kits: the studio updates them automatically when a project is opened. Use this to do it now. Dry-run by default: returns the plan (from / to version, migrations). Pass apply=true to apply it. A migration with auto=false is never applied by itself: it is your task — apply its note (config, data) then call again. Omit id to check every installed kit. 0 credits.', {
|
|
152
169
|
project_id: projectId,
|
|
153
170
|
id: kitId.optional().describe('Installed kit to update. Omit to check every installed kit.'),
|
|
154
|
-
|
|
155
|
-
apply: schema.boolean().optional().describe('true to apply the update (new lockfile + migrations). Default false = plan only.'),
|
|
171
|
+
apply: schema.boolean().optional().describe('true to apply the update to the latest version (automatic migrations run). Default false = plan only.'),
|
|
156
172
|
}, { readOnly: false }, async (args, extra) => {
|
|
157
173
|
const project = enc(args.project_id);
|
|
158
174
|
const payload = body(args);
|
|
@@ -178,12 +194,11 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
178
194
|
const data = { project_id: args.project_id, kits };
|
|
179
195
|
return { ...(isError ? { isError: true } : {}), structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
180
196
|
});
|
|
181
|
-
tool('gripforge_gamekit_deliver', 'Deliver Game Kits into an engine project', 'Turn catalogue kits, or the enabled kits of a Game Kit project, into engine files: Godot gets GDScript, scenes and config under gripforge/kits/<kit>/, Unity and Unreal
|
|
182
|
-
target: schema.enum(['godot', 'unity', 'unreal', 'web']).describe('Engine to deliver to: godot (files), unity
|
|
197
|
+
tool('gripforge_gamekit_deliver', 'Deliver Game Kits into an engine project', 'Turn catalogue kits, or the enabled kits of a Game Kit project, into engine files: Godot gets GDScript, scenes and config under gripforge/kits/<kit>/, Unity gets an embedded UPM package plus C# files and Unreal a Blueprint-first plugin for every kit with an engine delivery (gameplay and net.* kits; recipes otherwise), web needs nothing. Kits always deliver at their latest version; a kit the project copied locally is delivered from its copy. Call with dry_run=true FIRST: it returns the plan (files to add / keep / modify / delete, conflicts, dependencies, post-install steps) and never charges. Pass project_state (the current gripforge/gamekits.lock.json, path → sha256 of the files under gripforge/, engineVersion) so updates keep your edits and detect conflicts. Then call without dry_run to get the bundle. The hosted endpoint cannot write into your project: write bundle.files following plan.actions in order (backup, write, writeBin from bundle.binaries, delete with .uid / .import siblings, writeLock with bundle.lock), or use the npm client with project_dir (gripforge_gamekit_deliver_local), or the editor bridge. A blocked plan answers 409 with the plan: a file edited in the engine project is reported (modified_file), never overwritten silently; force backs it up then overwrites it. Credits: the first delivery of a kit major version to an engine costs 1 credit per workspace; re-deliveries, updates within a major, dry runs and the web target are free.', {
|
|
198
|
+
target: schema.enum(['godot', 'unity', 'unreal', 'web']).describe('Engine to deliver to: godot (addon + kit files), unity (UPM package + C# files), unreal (plugins), web (native, nothing to write). Kits without an engine delivery get recipes.'),
|
|
183
199
|
kits: schema
|
|
184
200
|
.array(schema.object({
|
|
185
201
|
id: kitId,
|
|
186
|
-
version: schema.string().max(40).optional().describe('Exact version; only the latest is deliverable. Default latest.'),
|
|
187
202
|
config: schema.record(schema.string(), schema.unknown()).optional().describe('Kit config, validated against its configSchema.'),
|
|
188
203
|
bindings: schema.record(schema.string(), schema.string()).optional().describe('Asset slot → Library item id (lib_…).'),
|
|
189
204
|
}))
|
|
@@ -202,7 +217,6 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
202
217
|
.describe('What the engine project contains now. Without it the plan assumes an empty project (add-only).'),
|
|
203
218
|
dry_run: schema.boolean().optional().describe('true → plan only: no bundle, never charged. Do this first.'),
|
|
204
219
|
force: schema.boolean().optional().describe('true → back up then overwrite edited managed files and existing seed files.'),
|
|
205
|
-
accept_breaking: schema.boolean().optional().describe('true → allow a breaking (major) update.'),
|
|
206
220
|
}, { readOnly: false }, async (args, extra) => {
|
|
207
221
|
const kits = Array.isArray(args.kits) ? args.kits : [];
|
|
208
222
|
const state = args.project_state;
|
|
@@ -217,7 +231,6 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
217
231
|
project: args.project_state,
|
|
218
232
|
dryRun: args.dry_run === true,
|
|
219
233
|
force: args.force === true,
|
|
220
|
-
acceptBreaking: args.accept_breaking === true,
|
|
221
234
|
}, 'POST', extra?.signal);
|
|
222
235
|
const data = result.structuredContent;
|
|
223
236
|
if (result.isError || !data?.bundle)
|
|
@@ -225,19 +238,19 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
225
238
|
const withNote = { ...data, note: HOSTED_DELIVERY_NOTE };
|
|
226
239
|
return { structuredContent: withNote, content: [{ type: 'text', text: JSON.stringify(withNote) }] };
|
|
227
240
|
});
|
|
228
|
-
tool('gripforge_game_capabilities', 'Capabilities report of a project', '
|
|
241
|
+
tool('gripforge_game_capabilities', 'Capabilities report of a project', 'Report the capabilities a Game Kit project provides, what is still missing for a goal (a preset id from gripforge_gamekit_search or a comma-separated list of capabilities) and which catalogue kits would fill each gap. The installed kits (gripforge_gamekit_installed) come first; call this BEFORE gripforge_gamekit_install to pick the next kit. 0 credits.', {
|
|
229
242
|
project_id: projectId,
|
|
230
243
|
goal: schema.string().max(400).optional().describe('Preset id (e.g. from gripforge_gamekit_search presets) or comma-separated capabilities to reach. Default: the project’s own preset.'),
|
|
231
244
|
}, { readOnly: true }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/capabilities`, args, 'GET', extra?.signal, {
|
|
232
245
|
goal: typeof args.goal === 'string' ? args.goal : undefined,
|
|
233
246
|
}));
|
|
234
|
-
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) or an explicit kits map; 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_install. 0 credits.', {
|
|
247
|
+
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.', {
|
|
235
248
|
action: schema.enum(['list', 'create', 'get', 'bind', 'data', 'delete']).describe('list | create | get | bind | data | delete.'),
|
|
236
249
|
project_id: projectId.optional().describe('Game Kit project id (gkp_…). Required for get, bind, data and delete.'),
|
|
237
250
|
name: schema.string().min(1).max(120).optional().describe('create: project name.'),
|
|
238
251
|
preset: schema.string().max(80).optional().describe('create: preset id whose default kits are installed (gripforge_gamekit_search returns presets).'),
|
|
239
|
-
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.'),
|
|
240
|
-
target: schema.enum(['web', 'godot', 'unity', 'unreal']).optional().describe('create: engine
|
|
252
|
+
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.'),
|
|
253
|
+
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.'),
|
|
241
254
|
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.'),
|
|
242
255
|
collection: schema.string().max(80).optional().describe('data: collection name declared by an installed kit (e.g. missions, npcs, zones).'),
|
|
243
256
|
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.'),
|
|
@@ -278,6 +291,51 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
278
291
|
}
|
|
279
292
|
return Promise.resolve(fail(`Unknown action ${action}.`));
|
|
280
293
|
});
|
|
294
|
+
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.', {
|
|
295
|
+
project_id: projectId,
|
|
296
|
+
action: schema.enum(['start', 'structure', 'blocks', 'report', 'next']).optional().describe('start (default) | structure | blocks | report | next.'),
|
|
297
|
+
scope: schema.enum(['installed', 'catalog']).optional().describe('blocks: installed (default) or catalog.'),
|
|
298
|
+
scene: schema.string().max(64).optional().describe('start: also return this scene document.'),
|
|
299
|
+
}, { readOnly: true }, async (args, extra) => {
|
|
300
|
+
const action = typeof args.action === 'string' ? args.action : 'start';
|
|
301
|
+
const project = enc(args.project_id);
|
|
302
|
+
if (action === 'blocks')
|
|
303
|
+
return api(`gamekit-projects/${project}/engine/blocks`, args, 'GET', extra?.signal, { scope: typeof args.scope === 'string' ? args.scope : undefined });
|
|
304
|
+
const res = await api(`gamekit-projects/${project}/engine`, args, 'GET', extra?.signal, { scene: typeof args.scene === 'string' ? args.scene : undefined });
|
|
305
|
+
if (res.isError || action === 'start')
|
|
306
|
+
return res;
|
|
307
|
+
const data = res.structuredContent;
|
|
308
|
+
const picked = action === 'report' ? { report: data?.report } : action === 'next' ? { next: data?.engine?.next, order: data?.engine?.order } : { structure: data?.engine?.structure, scene: data?.engine?.scene, active: data?.engine?.active };
|
|
309
|
+
return { structuredContent: picked, content: [{ type: 'text', text: JSON.stringify(picked) }] };
|
|
310
|
+
});
|
|
311
|
+
tool('gripforge_game_content', 'Drop content into a game: scenes, archetypes, scripts, quests…', 'Read and write the content of a Game Kit project (gkp_…) — what game.engine builds the game from, no engine code needed. kind: archetype (an entity template: kind prop|npc|…, asset { item: lib_… } bound to content:<id> | { slot }, components claimed by kits — see gripforge_game_engine blocks — and gives { quest, offer, progress?, thanks? } for "this NPC gives this quest"), scene (world { kit, config?, doc? }, entries, instances [{ id, archetype, at, yaw?, place?, set? }], zones, layers, ui), script (gripforge.script/1: { id, on: <instance> | archetype:<id> | scene:<id>, rules: [{ when, if?, then, else?, once? }] } with sugars { "interact": "self" }, { "enter": zone }, { "state": { open: false } }, { "quest": id, "is": "completed" }, { "talked": npc }, engine verbs set_state, play_clip, set_solid, show, hide, enable_script, disable_script, load_scene, end_game, call, set_talk and kit verbs as shorthand { "give_item": "key" }), mission (alias quest), dialogue, npc, item, translations, code_module (what game code registers: verbs, components, services, entity kinds), zone, trigger. action=list (kind optional: counts), get (kind, id), put (kind, doc or docs: upsert, validated against the declaring kit schema), remove (kind, id), validate (dry run of a put: the report it would give), place (scene, archetype or asset lib_…, at, yaw?, id?, place ground|none: the placement on the ground is baked once on the server, the same on every engine), bake (re-bake the placements of a scene after its world changed). Writes return the content report after the change; the report never refuses a write — only a document invalid for its schema is. 0 credits.', {
|
|
312
|
+
project_id: projectId,
|
|
313
|
+
action: schema.enum(['list', 'get', 'put', 'remove', 'validate', 'place', 'bake']).describe('list | get | put | remove | validate | place | bake.'),
|
|
314
|
+
kind: schema.enum(['archetype', 'scene', 'script', 'mission', 'quest', 'dialogue', 'npc', 'item', 'translations', 'code_module', 'zone', 'trigger']).optional().describe('The content kind (list without kind: counts per kind).'),
|
|
315
|
+
id: schema.string().max(120).optional().describe('get / remove: the document id; place: the instance id (default <archetype>_<n>).'),
|
|
316
|
+
doc: schema.record(schema.string(), schema.unknown()).optional().describe('put / validate: the document (with its id).'),
|
|
317
|
+
docs: schema.array(schema.record(schema.string(), schema.unknown())).max(200).optional().describe('put / validate: several documents of the same kind.'),
|
|
318
|
+
scene: schema.string().max(64).optional().describe('place / bake: the scene id (place default: the first scene).'),
|
|
319
|
+
archetype: schema.string().max(64).optional().describe('place: the archetype of the instance.'),
|
|
320
|
+
asset: schema.string().max(64).optional().describe('place: a Library asset (lib_…) instead of an archetype: its archetype is the one using it, or a new prop.'),
|
|
321
|
+
at: schema.array(schema.number()).length(3).optional().describe('place: position [x, y, z] in metres (+Y up).'),
|
|
322
|
+
yaw: schema.number().optional().describe('place: rotation around +Y in radians.'),
|
|
323
|
+
place: schema.enum(['ground', 'none']).optional().describe('place: ground (default) snaps to the scene ground; none keeps y.'),
|
|
324
|
+
set: schema.record(schema.string(), schema.unknown()).optional().describe('place: component overrides of this instance.'),
|
|
325
|
+
dry_run: schema.boolean().optional().describe('put / remove: validate and report without writing.'),
|
|
326
|
+
}, { readOnly: false }, (args, extra) => {
|
|
327
|
+
const action = String(args.action);
|
|
328
|
+
const project = enc(args.project_id);
|
|
329
|
+
if (action === 'list' || action === 'get') {
|
|
330
|
+
if (action === 'get' && (typeof args.kind !== 'string' || typeof args.id !== 'string'))
|
|
331
|
+
return Promise.resolve(fail('get needs kind and id.'));
|
|
332
|
+
return api(`gamekit-projects/${project}/content`, args, 'GET', extra?.signal, { kind: typeof args.kind === 'string' ? args.kind : undefined, id: typeof args.id === 'string' ? args.id : undefined });
|
|
333
|
+
}
|
|
334
|
+
const { project_id: _p, ...payload } = args;
|
|
335
|
+
if (action === 'place')
|
|
336
|
+
return api(`gamekit-projects/${project}/content/place`, payload, 'POST', extra?.signal);
|
|
337
|
+
return api(`gamekit-projects/${project}/content`, payload, 'POST', extra?.signal);
|
|
338
|
+
});
|
|
281
339
|
tool('gripforge_game_play_url', 'Play URL of a Game Kit project', 'Get the browser play URL of a Game Kit project so a human (or a screenshot step) can run its current revision in the web runtime. Reads the project bundle and returns { url, absolute }; absolute is built on the GripForge origin of this key. Call it AFTER gripforge_gamekit_install or gripforge_game_project bind to check the result live. 0 credits.', { project_id: projectId }, { readOnly: true }, async (args, extra) => {
|
|
282
340
|
const res = await api(`gamekit-projects/${enc(args.project_id)}/bundle`, args, 'GET', extra?.signal);
|
|
283
341
|
if (res.isError)
|
|
@@ -295,4 +353,49 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
295
353
|
const data = { url: playUrl, absolute };
|
|
296
354
|
return { structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
297
355
|
});
|
|
356
|
+
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.', {
|
|
357
|
+
project_id: projectId,
|
|
358
|
+
heroes: schema.array(schema.string()).min(1).max(10).describe('Library ids (lib_…) of champions with an ability pack, in roster order.'),
|
|
359
|
+
player: schema.string().optional().describe('The champion the player controls (one of heroes); default the first.'),
|
|
360
|
+
}, { readOnly: false }, async (args, extra) => {
|
|
361
|
+
const { project_id, ...body } = args;
|
|
362
|
+
return api(`gamekit-projects/${enc(project_id)}/moba-roster`, body, 'PUT', extra?.signal);
|
|
363
|
+
});
|
|
364
|
+
tool('gripforge_ability_vfx', 'Give every ability of a game its cast effect', 'Write and bind a cast effect for each ability of a Game Kit project that has none: an area gets a ground shockwave, a line a bolt and its trail, an arc a sweeping crescent, a self cast a rising aura; colour from the ability\'s words (fire, frost, wind, holy…), size from its range, ultimates larger. The project needs the fx.ability_vfx kit (the moba preset installs it) and an abilities collection (gripforge_moba_roster writes it). Effects are procedural sources saved as VFX items and bound to ability_vfx:<ability id>, immediately. replace: true rewrites the existing ones; abilities limits to some ids. A hand-made or generated VFX bound to the same slot takes over. 0 credits.', {
|
|
365
|
+
project_id: projectId,
|
|
366
|
+
replace: schema.boolean().optional().describe('Rewrite abilities that already have an effect (default: keep them).'),
|
|
367
|
+
abilities: schema.array(schema.string()).max(80).optional().describe('Only these ability ids (default: all).'),
|
|
368
|
+
}, { readOnly: false }, async (args, extra) => {
|
|
369
|
+
const { project_id, ...body } = args;
|
|
370
|
+
return api(`gamekit-projects/${enc(project_id)}/ability-vfx`, body, 'POST', extra?.signal);
|
|
371
|
+
});
|
|
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.', {
|
|
373
|
+
project_id: projectId,
|
|
374
|
+
action: schema.enum(['get', 'generate', 'validate', 'set']).optional().describe('Default: get.'),
|
|
375
|
+
lanes: schema.number().int().min(1).max(5).optional().describe('generate: number of lanes (default 3).'),
|
|
376
|
+
divider: schema.enum(['river', 'chasm', 'lava', 'void', 'none']).optional().describe('generate: what separates the halves (default river).'),
|
|
377
|
+
size: schema.number().min(100).max(400).optional().describe('generate: side of the square map in metres.'),
|
|
378
|
+
team_size: schema.number().int().min(1).max(5).optional().describe('generate: heroes per team (default 3 for one lane, 5 otherwise).'),
|
|
379
|
+
camps_per_jungle: schema.number().int().min(0).max(3).optional().describe('generate: small camps in each jungle (default 2).'),
|
|
380
|
+
objectives: schema.number().int().min(0).max(4).optional().describe('generate: epic objectives on the divider.'),
|
|
381
|
+
bushes: schema.boolean().optional().describe('generate: stealth bushes along the lanes (default true).'),
|
|
382
|
+
walls: schema.boolean().optional().describe('generate: rock walls behind the camps (default true).'),
|
|
383
|
+
id: schema.string().max(41).optional().describe('generate: id of the new map (lowercase, digits, _).'),
|
|
384
|
+
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.'),
|
|
386
|
+
map: schema.record(schema.string(), schema.unknown()).optional().describe('validate / set: a full map document.'),
|
|
387
|
+
force: schema.boolean().optional().describe('set: store a map that has errors.'),
|
|
388
|
+
}, { readOnly: false }, async (args, extra) => {
|
|
389
|
+
const { project_id, ...body } = args;
|
|
390
|
+
return api(`gamekit-projects/${enc(project_id)}/moba-map`, body, 'POST', extra?.signal);
|
|
391
|
+
});
|
|
392
|
+
tool('gripforge_terrain_map_use', 'Play a terrain studio map in a game', 'Make a map of the terrain studio the world of a game that has the world.terrain kit. Pass map (a terrain studio map id, lvl_…) or scene (a scene id, with terrain when the scene has several terrain nodes). The map is baked once into a file (heights, ground roles, lakes and rivers, vegetation, roads, spawn and objective points) stored in the Library and bound to the terrain_map slot; the project data keeps its reference in terrain_maps. The same file is delivered to Godot, Unity and Unreal with the kit. Buildings and other non-vegetation props, and terrain holes, are not carried yet (counted in map.notCarried). Without a bound map world.terrain keeps generating its world from the seed. 0 credits.', {
|
|
393
|
+
project_id: projectId,
|
|
394
|
+
map: schema.string().optional().describe('Terrain studio map id (lvl_…).'),
|
|
395
|
+
scene: schema.string().optional().describe('Scene id whose terrain node becomes the world (instead of map).'),
|
|
396
|
+
terrain: schema.string().optional().describe('Terrain node id in the scene (default: its first terrain).'),
|
|
397
|
+
}, { readOnly: false }, async (args, extra) => {
|
|
398
|
+
const { project_id, ...body } = args;
|
|
399
|
+
return api(`gamekit-projects/${enc(project_id)}/terrain-map`, body, 'PUT', extra?.signal);
|
|
400
|
+
});
|
|
298
401
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export const HERO_ABILITY_TOOL_NAMES = ['gripforge_abilities_generate'];
|
|
3
|
+
/**
|
|
4
|
+
* A hero's abilities ready in Character Studio: kit (role, basic attack, passive, Q/W/E/R), the
|
|
5
|
+
* Kimodo clip of each cast on the character, and the gripforge.abilities/1 pack bound to them.
|
|
6
|
+
* Hosted and local MCP share the same REST workflow: stage=plan (free), then stage=build.
|
|
7
|
+
*/
|
|
8
|
+
export function registerHeroAbilityTools(register, options, schema = z) {
|
|
9
|
+
const shape = {
|
|
10
|
+
stage: schema.enum(['plan', 'build']).optional().describe('plan (default, free): the kit, its abilities and clips, and the price. build: queue the GPU job.'),
|
|
11
|
+
prompt: schema.string().min(8).max(600).optional().describe('The hero in one sentence when there is no concept, e.g. "a frost archer who slows enemies, ultimate freezes an area". Its kit is written at plan.'),
|
|
12
|
+
hero_concept_id: schema.string().optional().describe('A hero concept with an ability kit (gripforge_game_concepts with hero_kits).'),
|
|
13
|
+
kit: schema.record(schema.string(), schema.unknown()).optional().describe('The kit returned by plan: pass it to build to keep exactly what was shown.'),
|
|
14
|
+
name: schema.string().min(2).max(80).optional(),
|
|
15
|
+
character_id: schema.string().optional().describe('Rigged Library character or enemy the clips and abilities go on. Default: the UAL mannequin.'),
|
|
16
|
+
workspace_id: schema.string().max(100).optional(),
|
|
17
|
+
idempotency_key: schema.string().regex(/^[a-zA-Z0-9_.:-]{8,160}$/).optional(),
|
|
18
|
+
};
|
|
19
|
+
register(HERO_ABILITY_TOOL_NAMES[0], {
|
|
20
|
+
title: 'Generate hero abilities',
|
|
21
|
+
description: 'A hero\'s abilities ready to play in Character Studio: the kit (role, lane, basic attack, passive + Q/W/E/R with type, range, cooldown, cost, effect, VFX brief, cast animation) from a hero concept or written from one sentence; the basic attack and Q/W/E/R casts animated with NVIDIA Kimodo on the rigged character; and the gripforge.abilities/1 pack (targeting, damage/heal, timings) bound to those clips and stored on the result. Call stage=plan first (free) and show the abilities and price (1 credit per clip, 5 for a kit); then stage=build with the returned kit. Poll gripforge_generation_read with job_id; result.studio_url opens the Abilities tab. Numbers are starting values, not a balance pass; VFX lines are briefs for gripforge_vfx_generate.',
|
|
22
|
+
inputSchema: shape,
|
|
23
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
24
|
+
}, async (args, extra) => {
|
|
25
|
+
const parsed = schema.object(shape).strict().safeParse(args);
|
|
26
|
+
if (!parsed.success)
|
|
27
|
+
return { isError: true, content: [{ type: 'text', text: parsed.error.message }] };
|
|
28
|
+
const key = options.getApiKey();
|
|
29
|
+
if (!key)
|
|
30
|
+
return { isError: true, content: [{ type: 'text', text: 'GripForge API key required.' }] };
|
|
31
|
+
const { workspace_id, ...body } = parsed.data;
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(options.apiUrl.replace(/\/$/, '') + '/api/v1/abilities/generate', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'content-type': 'application/json', 'x-gripforge-client': 'mcp', 'x-api-key': key, ...(typeof workspace_id === 'string' ? { 'x-workspace-id': workspace_id } : {}) },
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
signal: AbortSignal.any([AbortSignal.timeout(120_000), ...(extra?.signal ? [extra.signal] : [])]),
|
|
38
|
+
});
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
if (typeof data.status_url === 'string' && data.status_url.startsWith('/'))
|
|
41
|
+
data.status_url = new URL(data.status_url, options.apiUrl).href;
|
|
42
|
+
return { ...(!response.ok ? { isError: true } : {}), structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Ability request failed.' }] };
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|