@gripforgeai/mcp 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -2
- package/dist/ability-tools.js +21 -0
- package/dist/architecture-tools.js +83 -0
- package/dist/asset-production-tools.js +101 -0
- package/dist/creature-rig-tools.js +41 -0
- package/dist/game-concept-tools.js +56 -0
- package/dist/game-creation-tools.js +45 -0
- package/dist/gamekit-deliver-local.js +5 -7
- package/dist/gamekit-tools.js +133 -24
- package/dist/hero-ability-tools.js +48 -0
- package/dist/joystick-kit/godot.js +146 -0
- package/dist/joystick-kit/index.js +32 -0
- package/dist/joystick-kit/threejs.js +93 -0
- package/dist/joystick-kit/unity.js +182 -0
- package/dist/joystick-kit/unreal.js +231 -0
- package/dist/joystick-tools.js +26 -0
- package/dist/look.js +6 -0
- package/dist/map-unreal-export-local.js +285 -0
- package/dist/map-unreal-import-local.js +421 -0
- package/dist/map-unreal-local.js +75 -0
- package/dist/motion-tools.js +50 -0
- package/dist/projectile-tools.js +28 -0
- package/dist/scene-tools.js +79 -13
- package/dist/server-tools.js +3 -3
- package/dist/server.js +139 -13
- package/dist/shield-vfx-tools.js +29 -0
- package/dist/studio-tools-tools.js +106 -0
- package/dist/unreal-export-package.js +212 -0
- package/dist/vfx-project-tools.js +2 -1
- package/dist/weapon-motion-tools.js +34 -0
- package/package.json +6 -1
- package/runtime/unreal-scene-export.py +341 -0
- package/runtime/unreal-scene-import.py +622 -0
- package/runtime/unreal-scene-upload.mjs +7 -0
- package/runtime/unreal_scene_math.py +96 -0
- package/runtime/unreal_spawn_review.py +135 -0
package/dist/scene-tools.js
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { z } from 'zod/v4';
|
|
2
|
+
import { LOOK_DESCRIPTION, LOOK_VALUES } from './look.js';
|
|
2
3
|
/** UI and agents call the same scene commands, access checks and revision store. */
|
|
3
4
|
export function registerSceneTools(register, options, schema = z) {
|
|
4
5
|
const identifier = schema.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,159}$/);
|
|
5
6
|
const record = schema.record(schema.string(), schema.unknown());
|
|
6
7
|
const revision = schema.number().int().positive();
|
|
8
|
+
const imageRef = schema.object({ assetId: identifier, revisionId: identifier, fileRole: identifier.optional() });
|
|
9
|
+
const mapWizard = schema.object({
|
|
10
|
+
style: schema.enum(['realistic', 'stylized', 'lowpoly', 'handpainted']).optional().describe('Legacy wording kept in the intent; it does not change the rendering. Use look.'),
|
|
11
|
+
look: schema.enum(LOOK_VALUES).optional().describe(`${LOOK_DESCRIPTION} Map: the theme gives the biome, the look the materials (LevelSpec.look).`),
|
|
12
|
+
world: schema.enum(['open_world', 'closed_arena', 'dungeon', 'linear', 'battle_map']).optional(),
|
|
13
|
+
terrain: schema.enum(['auto', 'plains', 'desert_canyon', 'forest', 'snow', 'island', 'volcanic']).optional(),
|
|
14
|
+
size: schema.enum(['small', 'medium', 'large']).optional(),
|
|
15
|
+
dimensions: schema.object({ width: schema.number().min(100).max(5000), depth: schema.number().min(100).max(5000) }).optional().describe('Explicit ground footprint in metres. Overrides size presets and world multipliers. Example: {width:500,depth:500}. Four-team maps require a square.'),
|
|
16
|
+
boundary: schema.enum(['fixed', 'infinite']).optional(), spawn: schema.enum(['single', 'teams']).optional(),
|
|
17
|
+
teams: schema.union([schema.literal(2), schema.literal(4)]).optional(),
|
|
18
|
+
boss: schema.boolean().optional(), safeZone: schema.boolean().optional(), extraction: schema.boolean().optional(),
|
|
19
|
+
}).passthrough();
|
|
7
20
|
const workspace = { workspace_id: schema.string().max(100).optional().describe('Workspace authorized by the current key. Returned scene links open this workspace.') };
|
|
8
21
|
async function call(path, args, method, signal) {
|
|
9
22
|
const key = options.getApiKey();
|
|
10
23
|
if (!key)
|
|
11
24
|
return { isError: true, content: [{ type: 'text', text: 'GripForge API key required.' }] };
|
|
12
|
-
const { workspace_id, id: _id, version: _version, include_preview, ...body } = args;
|
|
25
|
+
const { workspace_id, id: _id, version: _version, include_preview, preview_view, ...body } = args;
|
|
13
26
|
try {
|
|
14
27
|
const response = await fetch(`${options.apiUrl.replace(/\/$/, '')}/api/v1/${path}`, {
|
|
15
28
|
method, headers: { 'content-type': 'application/json', 'x-api-key': key, 'x-gripforge-client': 'mcp', ...(typeof workspace_id === 'string' ? { 'x-workspace-id': workspace_id } : {}) },
|
|
@@ -23,16 +36,47 @@ export function registerSceneTools(register, options, schema = z) {
|
|
|
23
36
|
if (result && typeof result === 'object' && typeof result.studio_url === 'string')
|
|
24
37
|
result.studio_url = new URL(String(result.studio_url), options.apiUrl).href;
|
|
25
38
|
const content = [{ type: 'text', text: JSON.stringify(data) }];
|
|
26
|
-
const
|
|
27
|
-
if (response.ok && include_preview === true
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
const result = data.result;
|
|
40
|
+
if (response.ok && include_preview === true) {
|
|
41
|
+
let previews = Array.isArray(result?.concept_views) ? result.concept_views.slice(0, 4) : result?.preview_url ? [{ view: 'overview', label: 'Preview', preview_url: result.preview_url }] : [];
|
|
42
|
+
if (typeof preview_view === 'string') {
|
|
43
|
+
previews = previews.filter(view => view.view === preview_view);
|
|
44
|
+
if (!previews.length)
|
|
45
|
+
throw Error('This preview view is not available yet. The job remains available.');
|
|
46
|
+
}
|
|
47
|
+
const images = await Promise.all(previews.map(async (preview) => {
|
|
48
|
+
const path = preview.preview_url ?? ('image_url' in preview ? preview.image_url : undefined);
|
|
49
|
+
if (typeof path !== 'string' || !/^\/api\/v1\/(?:generation-jobs|scene-assets)\//.test(path))
|
|
50
|
+
throw Error('Invalid job preview URL.');
|
|
51
|
+
const image = await fetch(new URL(path, options.apiUrl), { headers: { 'x-api-key': key, ...(typeof workspace_id === 'string' ? { 'x-workspace-id': workspace_id } : {}) }, signal: AbortSignal.any([AbortSignal.timeout(30_000), ...(signal ? [signal] : [])]) });
|
|
52
|
+
if (!image.ok || !image.headers.get('content-type')?.startsWith('image/png'))
|
|
53
|
+
throw Error('The job preview could not be read. The completed job remains available.');
|
|
54
|
+
const bytes = await image.arrayBuffer();
|
|
55
|
+
if (bytes.byteLength > 12 * 1024 * 1024)
|
|
56
|
+
throw Error('Preview exceeds the image budget.');
|
|
57
|
+
return { label: preview.label ?? preview.view.replaceAll('_', ' '), data: Buffer.from(bytes).toString('base64') };
|
|
58
|
+
}));
|
|
59
|
+
for (const image of images) {
|
|
60
|
+
if (images.length > 1)
|
|
61
|
+
content.push({ type: 'text', text: image.label });
|
|
62
|
+
content.push({ type: 'image', mimeType: 'image/png', data: image.data });
|
|
63
|
+
}
|
|
35
64
|
}
|
|
65
|
+
for (const result of [data, data.result, job?.result])
|
|
66
|
+
if (result && typeof result === 'object') {
|
|
67
|
+
const row = result;
|
|
68
|
+
for (const key of ['studio_url', 'review_url', 'file_url', 'source_url', 'concept_url', 'status_url'])
|
|
69
|
+
if (typeof row[key] === 'string' && row[key].startsWith('/'))
|
|
70
|
+
row[key] = new URL(row[key], options.apiUrl).href;
|
|
71
|
+
if (Array.isArray(row.concept_views))
|
|
72
|
+
for (const view of row.concept_views)
|
|
73
|
+
if (view && typeof view === 'object') {
|
|
74
|
+
for (const key of ['image_url', 'preview_url', 'studio_url'])
|
|
75
|
+
if (typeof view[key] === 'string' && view[key].startsWith('/'))
|
|
76
|
+
view[key] = new URL(view[key], options.apiUrl).href;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
content[0] = { type: 'text', text: JSON.stringify(data) };
|
|
36
80
|
return { ...(!response.ok ? { isError: true } : {}), structuredContent: data, content };
|
|
37
81
|
}
|
|
38
82
|
catch (error) {
|
|
@@ -40,6 +84,15 @@ export function registerSceneTools(register, options, schema = z) {
|
|
|
40
84
|
}
|
|
41
85
|
}
|
|
42
86
|
const tool = (name, title, description, inputSchema, readOnly, callback) => register(name, { title, description, inputSchema: { ...inputSchema, ...workspace }, annotations: { readOnlyHint: readOnly, destructiveHint: false, idempotentHint: readOnly, openWorldHint: false } }, callback);
|
|
87
|
+
tool('gripforge_vehicle_doors', 'Open or close vehicle doors in the Studio', 'Save an independent door pose on one prepared vehicle instance in a saved scene. doors maps Door_FL/FR/RL/RR to 0 (closed)..1 (open). Unspecified doors retain their state; null clears the manual pose. Requires exact expectedRevision and named hinges/portable clips from prepare_vehicle. Pauses the global animation for manual control. Preserves the source asset, transforms, other instances and validated versions. Returns the actual Studio link. No generation charge.', { id: identifier, expectedRevision: revision, nodeId: identifier, doors: schema.record(schema.string().regex(/^Door_[FR][LR]$/), schema.number().min(0).max(1)).nullable() }, false, (args, extra) => call(`scenes/${args.id}/vehicle-doors`, args, 'POST', extra?.signal));
|
|
88
|
+
tool('gripforge_prepare_vehicle', 'Prepare segmented vehicle hinges in Blender', 'Durable Blender preparation of an owned static segmented GLB. Map each Door_FL/FR/RL/RR or Wheel_FL/FR/RL/RR to exact mesh node names from the segmentation report. Keeps geometry placement, UVs and PBR; normalizes +Z forward/metres using yaw and length; adds hinge origins, seat/entry sockets and portable GLB open/close/preview clips. Optional hinge coordinates are in normalized vehicle metres. thickness adds an inward door shell, not a full cabin. Optional cuts provide strictly convex door outlines [forwardZ,heightY] and depth [near,far] measured from the centre on that side, in normalized metres. cuts[].meshes restricts each cut to exact exterior/glazing meshes so seats and dashboard cannot be cut. removeMeshes discards explicitly named obsolete trim in the work copy only. Optional clearanceDepthM clears scoped inner walls behind the door skin; trim adds measured body frames, seals and moulded cards below cardTopY. framePoints follows outline in normalized [x,y,z]. Optional trim.fitToSurface projects frames, seals and the inner card onto only the explicitly selected opaque door skin, preserving the supplied outline. attachments adds scoped [min,max] [x,y,z] volumes for mirrors extending beyond the door profile. Blender cuts those explicit profiles while interpolating UVs. Does not infer outlines or guarantee clearance. Returns job_id, new private work GLB, Blender source and animated Studio review link. 0 credits; storage applies. Inspect before replacement.', { source: imageRef, parts: schema.record(schema.string(), schema.array(schema.string().min(1).max(160)).min(1).max(64)).optional(), cuts: schema.array(schema.object({ role: schema.enum(['Door_FL', 'Door_FR', 'Door_RL', 'Door_RR']), outline: schema.array(schema.tuple([schema.number(), schema.number()])).min(3).max(12), depth: schema.tuple([schema.number(), schema.number()]), meshes: schema.array(schema.string().min(1).max(160)).min(1).max(128).optional(), clearanceDepthM: schema.number().min(0).max(3).optional(), attachments: schema.array(schema.object({ bounds: schema.tuple([schema.tuple([schema.number(), schema.number(), schema.number()]), schema.tuple([schema.number(), schema.number(), schema.number()])]), meshes: schema.array(schema.string().min(1).max(160)).min(1).max(128) })).max(4).optional(), trim: schema.object({ framePoints: schema.array(schema.tuple([schema.number(), schema.number(), schema.number()])).min(3).max(12), frameRadiusM: schema.number().min(.003).max(.04).optional(), sealRadiusM: schema.number().min(.002).max(.02).optional(), cardTopY: schema.number().optional(), cardInsetM: schema.number().min(.01).max(.12).optional(), fitToSurface: schema.boolean().optional(), cardColor: schema.tuple([schema.number().min(0).max(1), schema.number().min(0).max(1), schema.number().min(0).max(1)]).optional() }).optional() })).max(4).optional(), removeMeshes: schema.array(schema.string().min(1).max(160)).max(128).optional(), yaw: schema.number().min(-180).max(180).optional(), length: schema.number().min(2.8).max(7).optional(), openAngle: schema.number().min(20).max(85).optional(), thickness: schema.number().min(0).max(.04).optional(), hinges: schema.record(schema.string(), schema.tuple([schema.number(), schema.number(), schema.number()])).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('vehicles/prepare', args, 'POST', extra?.signal));
|
|
89
|
+
tool('gripforge_vehicle_schema', 'Modular vehicle manufacturing contract', 'Read the concept-first vehicle recipe, canonical parts, hinge convention, limits, quotes and revision workflow.', {}, true, (args, extra) => call('vehicles', args, 'GET', extra?.signal));
|
|
90
|
+
tool('gripforge_generate_vehicle', 'Generate a vehicle from reviewed multi-view references', 'Read vehicle_schema first. Recommended recipe.workflow=body_wheels follows concept → references (five wheel-less body views + wheel) → geometry (stop/review) → topology (stop/review) → build (PBR + wheel + rig). Each geometry review uses scene_review with the returned assetRef and must be approved before the next paid step. Rigging preserves UVs and corner normals, GLB/FBX/Blend and measured Chaos setup. Top view is review-only. parts.body.geometry accepts unfinished Hunyuan/Blender GLB; parts.body.topology pins the reviewed reduced GLB; parts.<role>.source reuses finished meshes. Legacy modular_doors remains available for articulated doors. stage=plan quotes each stage without spending; paid stages require its budget. Durable job; poll generation_read. Work draft, real-render review and target-engine driving test required; no automatic publication/replacement.', { stage: schema.enum(['plan', 'concept', 'references', 'geometry', 'topology', 'build']).default('plan'), recipe: record, budget: schema.object({ usd: schema.number().nonnegative().optional(), credits: schema.number().int().nonnegative().optional() }).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('vehicles', args, 'POST', extra?.signal));
|
|
91
|
+
tool('gripforge_vehicle_segment', 'Separate an existing vehicle with Tripo', 'Quote then queue one durable Tripo v2 semantic segmentation of an owned immutable GLB, including Meshy or Blender sources. stage=plan is free; stage=build requires quoted budget. 40 Tripo provider credits, distinct from the GripForge quote. Returns job_id; inspect parts and texture preservation in generation_read. No automatic completion, retexture, articulation, publication or replacement. A segmented output still needs hinge preparation.', { stage: schema.enum(['plan', 'build']).default('plan'), source: imageRef, granularity: schema.enum(['simple', 'balanced', 'detailed']).optional(), budget: schema.object({ usd: schema.number().nonnegative().optional(), credits: schema.number().int().nonnegative().optional() }).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('vehicles/segment', args, 'POST', extra?.signal));
|
|
92
|
+
tool('gripforge_map_import_unreal_schema', 'Unreal to Studio import contract', 'Read the native export → binary upload → persistent map import workflow, limits and explicit conversion caveats. Native extraction requires the local MCP and an installed Unreal editor. Hosted MCP/API consume the resulting portable SceneDocument; they cannot read your local .umap/.uasset files.', {}, true, (args, extra) => call('maps/import/unreal', args, 'GET', extra?.signal));
|
|
93
|
+
tool('gripforge_map_import_unreal', 'Import a portable Unreal scene', 'Queue a persistent import of schema gripforge.unreal-studio-import.v1. Upload files via local gripforge_map_unreal_upload_local or POST /api/v1/maps/import/unreal/assets first. document is a common SceneDocument with workspace-owned immutable assets, separate props, static foliage patches and editable terrain. source={engine:unreal,level:/Game/...,sha256,warnings:[]}. Returns job_id; poll generation_read for studio_url. Always a NEW private work scene; no overwrite, automatic validation or Community publication. 0 credits; storage quota applies.', { schema: schema.literal('gripforge.unreal-studio-import.v1'), document: record, source: schema.object({ engine: schema.literal('unreal'), level: schema.string().regex(/^\/Game(?:\/[\w-]+)+$/), sha256: schema.string().regex(/^[a-f0-9]{64}$/), warnings: schema.array(schema.string().max(2000)).max(100) }), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('maps/import/unreal', args, 'POST', extra?.signal));
|
|
94
|
+
tool('gripforge_flexible_weapon_schema', 'Flexible weapon manufacturing recipes', 'Read the bounded recipe and concept-first workflow for Meshy + Blender flexible weapons, costs, pinned component reuse and source animation requirements.', {}, true, (args, extra) => call('flexible-weapons', args, 'GET', extra?.signal));
|
|
95
|
+
tool('gripforge_generate_flexible_weapon', 'Generate an articulated flexible weapon', 'Persistent concept → isolated handle/segment references → Meshy PBR components → FlexibleChain XPBD → Blender assembly/rig → actual GripForge renderer review. Read flexible_weapon_schema first. recipe stage=concept returns an image before 3D; build can reuse that concept and owned pinned components. Up to 12 existing source clips or sampled motions; character choreography is not generated by this tool. Returns job_id immediately; poll generation_read for weapon GLB, Blender source, physics reports, separate character/weapon review scene and Studio links. Max 23 credits when all art is new; reused components are free. Cancel/retry retains successful provider and fabrication steps. Always private work: no automatic replacement, promotion or Community publication.', { recipe: record, idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('flexible-weapons', args, 'POST', extra?.signal));
|
|
43
96
|
tool('gripforge_flexible_chain_schema', 'Flexible chain recipes and examples', 'Read the bounded recipe schema for the shared FlexibleChain solver. Presets: whip, rope, tail. Input can follow an exact named mount in a pinned GLB animation or sampled world-space motion. Includes units, collision capsules, limits and a runnable example.', {}, true, (args, extra) => call('flexible-chain', args, 'GET', extra?.signal));
|
|
44
97
|
tool('gripforge_flexible_chain', 'Simulate and bake a flexible chain', 'Queue a persistent 240 Hz XPBD simulation using GripForge Scene Engine. Read flexible_chain_schema first. recipe version=1: preset whip|rope|tail, duration<=10s, fps=30|60, physics {length,segments,damping,bendCompliance,gravity,radius,floor,iterations}; choose motion [{time,position,direction,capsules}] OR source {asset:{assetId,revisionId},clip,mount_node,offset,axis,colliders}. Source assets are immutable and workspace-scoped. Returns a job ID; poll generation_read for a new draft skinned-chain GLB, sampled joints, recipe, physics metrics and Character Studio link. This is a neutral physics preview, not a Meshy weapon redesign or automatic rebind of the source weapon. No runtime world collision or seamless loop guarantee. Cancel/retry keeps completed steps. Does not replace or visually validate an existing asset. 0 credits; storage quota applies.', { recipe: record, idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('flexible-chain', args, 'POST', extra?.signal));
|
|
45
98
|
tool('gripforge_armor_generate', 'Generate a modular armor kit', 'Queue a durable armor-kit job: one shared concept, isolated Imagine references, Meshy PBR models, bounded Blender interior preparation and paired GLB exports, then actual GripForge wearer review. Types: helmet, chest, belt, pauldron, bracer, glove, thigh, greave, boot, upperarm, gorget, tasset, undersuit. coverage=full adds a durable anatomical decomposition of the concept, rejects missing planned roles before mesh generation and records per-instance coverage fits. A piece count alone never proves body coverage. The optional undersuit is a locally manufactured adaptive textile which retains each wearer’s skin weights. Meshy pieces cost 11 credits per canonical type; the textile has no image/mesh charge. A new concept costs 1 credit; mirrors are included. concept_item must be an owned Library image. source_kit + explicit pieces regenerates only those types into a NEW work kit and pins unchanged pieces. Closing the call/page does not cancel. Poll gripforge_generation_read. Success is a work version, never automatic visual approval. The result includes a Grip Studio link, review scene, concepts, source models and Blender source.', {
|
|
@@ -52,19 +105,32 @@ export function registerSceneTools(register, options, schema = z) {
|
|
|
52
105
|
tool('gripforge_armor_read', 'Read a modular armor kit', 'Read an owned kit manifest, semantic slots and immutable piece revisions. Equipment and instances remain independent. Returns the Grip Studio link. To correct selected types, call gripforge_armor_generate with source_kit and pieces.', { id: identifier }, true, (args, extra) => call(`armor-kits/${args.id}`, args, 'GET', extra?.signal));
|
|
53
106
|
tool('gripforge_armor_repair', 'Repair unfinished armor pieces', 'Fork a failed/cancelled armor job into a new work kit. Preserve completed pieces and the exact shared concept; recreate only unfinished types by default. Optional pieces chooses types explicitly. Use prompt for targeted corrections. Original checkpoints and artifacts remain intact. Costs 11 credits per regenerated canonical type (plus a concept credit only if none survived). Use generation_retry instead for a transient provider or worker error to reuse the exact same steps without another charge.', { source_job: identifier, prompt: schema.string().min(3).max(2000).optional(), pieces: schema.array(schema.enum(['helmet', 'chest', 'belt', 'pauldron', 'bracer', 'glove', 'thigh', 'greave', 'boot', 'upperarm', 'gorget', 'tasset', 'undersuit'])).min(1).max(13).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('armor-kits', args, 'POST', extra?.signal));
|
|
54
107
|
tool('gripforge_map_import', 'Migrate a saved terrain map', 'Queue a one-time materialization of an owned legacy terrain map into the shared Scene Engine. Preserve the saved spec, seed, resolution, family replacements, placed assets and removed props. Completed jobs return a canonical Studio link; source edits during migration cause a conflict rather than data loss.', { id: identifier }, false, (args, extra) => call('generation-jobs/map/import', { ...args, legacyMapId: args.id }, 'POST', extra?.signal));
|
|
55
|
-
tool('
|
|
108
|
+
tool('gripforge_map_schema', 'Map concept and generation workflow', 'Read the concept-first map workflow, immutable image reference format, costs and an example. Concepts, map work scenes and external engine delivery are separate steps.', {}, true, (args, extra) => call('generation-jobs/map', args, 'GET', extra?.signal));
|
|
109
|
+
tool('gripforge_map_generate', 'Create map concept views or an editable map', 'Queue a persistent map job. stage=concept creates a private master overview plus optional views and stops. views: overview,top_down,entrance,objective. All additional views are conditioned on the SAME pinned master; they are illustrations, not exact 3D renders. reference is an environment screenshot for a NEW layout. To add views to an existing concept, pass source_concept instead; the master is kept unchanged and is free. One credit per new image (full new 4-view set = 4); default is overview only. Poll generation_read with include_preview=true to see the views. After the user selects the concept, submit stage=build with the returned concept and a new idempotency key. Build costs 1 credit and is the default for existing callers; legacy concept_item accepts a Library image ID, while concept pins its exact version. wizard: look stylized|painted|toon|anime|realistic|lowpoly|pixel (rendering, separate from style; omit for the theme default); style realistic|stylized|lowpoly|handpainted (legacy wording); world open_world|closed_arena|dungeon|linear|battle_map; terrain auto|plains|desert_canyon|forest|snow|island|volcanic; size small|medium|large; boundary fixed|infinite (open edge, finite terrain); spawn single|teams; teams 2|4; boss,safeZone,extraction booleans. sources selects catalog/workspace/community props. Build uses the common Scene Engine with independent terrain, regions, paths, zones and asset instances. Cancel/retry keeps successful images and billing. No automatic visual validation, promotion or Unreal import.', {
|
|
110
|
+
prompt: schema.string().min(3).max(4000), wizard: mapWizard, stage: schema.enum(['concept', 'build']).optional(),
|
|
111
|
+
reference: imageRef.optional().describe('Pinned source-environment screenshot for stage=concept only. Upload to Library and pin first.'),
|
|
112
|
+
source_concept: imageRef.optional().describe('Existing immutable master to add views to, stage=concept only. Mutually exclusive with reference; never regenerate this master.'),
|
|
113
|
+
views: schema.array(schema.enum(['overview', 'top_down', 'entrance', 'objective'])).min(1).max(4).optional().describe('Unique requested views, stage=concept only. The master overview is always included; omitting views keeps overview only.'),
|
|
114
|
+
concept: imageRef.optional().describe('Exact chosen concept revision for stage=build. Mutually exclusive with concept_item.'),
|
|
115
|
+
concept_item: identifier.optional().describe('Legacy Library concept image ID. Prefer an immutable concept reference.'),
|
|
116
|
+
structure: record.optional().describe('Optional explicit gripforge.map-structures.v1 plan for stage=build: terraces with polygon/elevation and separate ramp/bridge connections. Read gripforge_map_schema for the contract. Preserves canyon voids; validates bridge clearance, endpoints and slopes. Uses durable per-piece assets and the common SceneDocument. Dimensions must match wizard.dimensions. Solo maps only; result is a structural work version requiring visual review.'),
|
|
117
|
+
sources: schema.array(schema.enum(['catalog', 'workspace', 'community'])).min(1).max(3).optional(),
|
|
118
|
+
seed: schema.number().int().min(-2147483647).max(2147483647).optional(), idempotency_key: schema.string().min(8).max(160).optional(),
|
|
119
|
+
}, false, (args, extra) => call('generation-jobs/map', args, 'POST', extra?.signal));
|
|
56
120
|
tool('gripforge_scene_modify', 'Modify a selection with AI', 'Queue a scoped edit of saved scene nodes or a region. The server derives the allowed scope and preserves locks, gameplay zones, outside geometry and the last validated revision. Blender may fabricate missing assets. The result is a proposal requiring explicit application, never a silently replaced map.', { id: identifier, expectedRevision: revision, nodeIds: schema.array(identifier).min(1).max(100), prompt: schema.string().min(3).max(4000), blendDistance: schema.number().positive().max(1000).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call(`scenes/${args.id}/modify`, args, 'POST', extra?.signal));
|
|
57
121
|
tool('gripforge_scene_proposals', 'List proposed scene edits', 'List persistent proposals and their base revision, status and descriptions.', { id: identifier }, true, (args, extra) => call(`scenes/${args.id}/proposals`, args, 'GET', extra?.signal));
|
|
58
122
|
tool('gripforge_scene_proposal_read', 'Read a proposed scene edit', 'Read the original scene, proposed document and scoped commands. Use the Studio link to inspect actual rendering before applying.', { id: identifier, proposal: identifier }, true, (args, extra) => call(`scenes/${args.id}/proposals/${args.proposal}`, args, 'GET', extra?.signal));
|
|
59
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));
|
|
60
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
|
+
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_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));
|
|
61
127
|
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));
|
|
62
128
|
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));
|
|
63
129
|
tool('gripforge_scene_create', 'Create a workspace scene', 'Create a draft scene. Pin Library assets first and use returned assetId/revisionId references. The response contains a GripForge Studio link. Exporting a VFX definition must not include this test scene.', { document: record }, false, (args, extra) => call('scenes', args, 'POST', extra?.signal));
|
|
64
130
|
tool('gripforge_scene_write', 'Save scene work revision', 'Save a complete SceneDocument. expectedRevision must equal the latest work revision. A 409 is a real conflict: reread before changing it. Does not change the validated revision or source Library assets.', { id: identifier, document: record, expectedRevision: schema.number().int().positive() }, false, (args, extra) => call(`scenes/${encodeURIComponent(String(args.id))}`, args, 'PUT', extra?.signal));
|
|
65
131
|
tool('gripforge_scene_patch', 'Edit selected scene instances', 'Apply insert/remove/transform/replace/update commands atomically. Environment updates can configure shared 3D cloud layers (density, altitude, wind, colour and quality); see gripforge_scene_schema. Replace preserves id, transform, role, materials and attachments, and rejects incompatible named slots. Locked objects cannot be changed. Scope constrains regional edits; read the schema first.', { id: identifier, baseRevision: schema.number().int().positive(), commands: schema.array(record).min(1).max(20000), scope: record.optional() }, false, (args, extra) => call(`scenes/${encodeURIComponent(String(args.id))}`, args, 'PATCH', extra?.signal));
|
|
66
132
|
tool('gripforge_scene_versions', 'Scene versions and visual reviews', 'Read work/current pointers, immutable revision history and trusted visual review evidence. A rejected or unavailable review does not change the validated version.', { id: identifier }, true, (args, extra) => call(`scenes/${args.id}/versions`, args, 'GET', extra?.signal));
|
|
67
|
-
tool('gripforge_scene_review', 'Request an actual GripForge visual review', 'Queue a persistent render and independent visual review of the exact saved work revision. Poll generation_read. This does not promote or replace the validated version.', { id: identifier, expectedRevision: schema.number().int().positive(), prompt: schema.string().max(4000).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call(`scenes/${args.id}/review`, args, 'POST', extra?.signal));
|
|
133
|
+
tool('gripforge_scene_review', 'Request an actual GripForge visual review', 'Queue a persistent render and independent visual review of the exact saved work revision. Poll generation_read. Optional assetRef records the review on the exact isolated asset revision as well; required for vehicle geometry/topology gates. This does not promote or replace the validated version.', { id: identifier, expectedRevision: schema.number().int().positive(), assetRef: imageRef.optional(), prompt: schema.string().max(4000).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call(`scenes/${args.id}/review`, args, 'POST', extra?.signal));
|
|
68
134
|
tool('gripforge_scene_promote', 'Set the visually approved scene version', 'Explicitly promote the latest work revision only if its trusted visual review matches the exact content, dependencies and current renderer. expectedCurrent must match the existing validated pointer or null. No client-supplied approval report is accepted.', { id: identifier, revision: schema.number().int().positive(), expectedCurrent: schema.number().int().positive().nullable() }, false, (args, extra) => call(`scenes/${args.id}/promote`, args, 'POST', extra?.signal));
|
|
69
135
|
tool('gripforge_scene_asset_pin', 'Pin an immutable Library revision', 'Archive the owned Library resource bytes and metadata for use in scenes. Returns assetId/revisionId, capabilities, material/animation names and sockets. Repeating for identical content reuses the archive; it consumes workspace storage only once. Prefers the validated version for insertion; choose version=work to test the draft. Does not approve the asset visually.', { assetId: identifier, version: schema.enum(['current', 'work']).default('current') }, false, (args, extra) => call('scene-assets', args, 'POST', extra?.signal));
|
|
70
136
|
tool('gripforge_scene_asset_versions', 'Read asset version and review', 'Read immutable asset work/current pointers and the trusted review of work. Does not create a version.', { assetId: identifier }, true, (args, extra) => call(`scene-assets/${args.assetId}/versions`, args, 'GET', extra?.signal));
|
|
@@ -72,7 +138,7 @@ export function registerSceneTools(register, options, schema = z) {
|
|
|
72
138
|
tool('gripforge_scene_asset_promote', 'Set the visually approved asset version', 'Promote only when the exact work revision has a matching trusted review for the current renderer. Existing scene instances keep their pinned revision.', { assetId: identifier, revisionId: identifier, expectedCurrent: identifier.nullable() }, false, (args, extra) => call(`scene-assets/${args.assetId}/${args.revisionId}/promote`, args, 'POST', extra?.signal));
|
|
73
139
|
tool('gripforge_fps_animation', 'Manufacture first-person weapon animations', 'Queue a persistent Blender job for paired FPS arms and an articulated pistol: idle, draw, fire, tactical reload and empty reload. Recipe: {version:1,preset:"pistol",name:"Pistol FPS",tempo:1}; tempo 0.7–1.3. Returns job_id immediately; poll gripforge_generation_read for the private Library asset, .blend source, event markers and Character Studio FPS link. Generated variants are work revisions, never automatically validated. Use gripforge_generation_cancel/retry; completed steps survive closing the page.', { recipe: record, idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('fps-animations', args, 'POST', extra?.signal));
|
|
74
140
|
tool('gripforge_generation_list', 'List persistent generation jobs', 'List workspace jobs. Jobs and completed steps survive closing the studio or restarting the web server and worker.', {}, true, (args, extra) => call('generation-jobs', args, 'GET', extra?.signal));
|
|
75
|
-
tool('gripforge_generation_read', 'Read generation progress and result', 'Poll a job returned by a generation tool. Includes durable steps, progress, failure/retry state and the resulting work revision with a Studio link. include_preview returns
|
|
141
|
+
tool('gripforge_generation_read', 'Read generation progress and result', 'Poll a job returned by a generation tool. Includes durable steps, progress, failure/retry state and the resulting work revision with a Studio link. include_preview returns saved map/building concept images (at most 4). Set preview_view to return one chosen view. Concepts are generated illustrations, not verified 3D renders. Success does not mean visual approval or promotion.', { id: identifier, include_preview: schema.boolean().optional(), preview_view: schema.enum(['overview', 'top_down', 'entrance', 'objective', 'front_right', 'front_left', 'rear_right', 'rear_left']).optional() }, true, (args, extra) => call(`generation-jobs/${args.id}`, args, 'GET', extra?.signal));
|
|
76
142
|
tool('gripforge_generation_cancel', 'Cancel generation', 'Request worker cancellation while retaining completed steps and artifacts. Closing a page alone does not cancel a job.', { id: identifier }, false, (args, extra) => call(`generation-jobs/${args.id}`, { ...args, action: 'cancel' }, 'POST', extra?.signal));
|
|
77
143
|
tool('gripforge_generation_retry', 'Retry interrupted generation', 'Retry a failed or cancelled job, reusing completed steps. An unconfirmed interrupted provider call may be executed again when explicitly retried.', { id: identifier }, false, (args, extra) => call(`generation-jobs/${args.id}`, { ...args, action: 'retry' }, 'POST', extra?.signal));
|
|
78
144
|
tool('gripforge_blender_fabricate', 'Fabricate editable assets with Blender', 'Queue a bounded Blender recipe, never arbitrary Python. Saves GLB and .blend in the workspace as an unvalidated work version with a shared Scene Studio link. Read gripforge_scene_schema for recipe fields. Supports original primitive/custom meshes, PBR colors and keyed transforms. Poll gripforge_generation_read; final visual approval must use GripForge rendering, not Blender screenshots.', { recipe: record, idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('generation-jobs/blender', args, 'POST', extra?.signal));
|
package/dist/server-tools.js
CHANGED
|
@@ -112,7 +112,7 @@ export function registerServerTools(register, options, schema = z) {
|
|
|
112
112
|
}, { readOnly: false }, (args, extra) => api('servers', args, 'POST', extra?.signal));
|
|
113
113
|
tool('gripforge_server_deploy', 'Deploy a game server', 'Create a Docker instance for this server and start it. Local demo uses a real container (python heartbeat unless a custom image is set). Mutating.', { id: serverId }, { readOnly: false }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/deploy`, args, 'POST', extra?.signal));
|
|
114
114
|
tool('gripforge_server_start', 'Start a game server', 'Start the existing instance. Mutating.', { id: serverId }, { readOnly: false }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/start`, args, 'POST', extra?.signal));
|
|
115
|
-
tool('gripforge_server_stop', 'Stop a game server', 'Stop the running instance. Mutating.', { id: serverId }, { readOnly: false }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/stop`, args, 'POST', extra?.signal));
|
|
115
|
+
tool('gripforge_server_stop', 'Stop a game server', 'Stop the running instance. Mutating.', { id: serverId }, { readOnly: false, destructive: true }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/stop`, args, 'POST', extra?.signal));
|
|
116
116
|
tool('gripforge_server_restart', 'Restart a game server', 'Restart the running instance. Mutating.', { id: serverId }, { readOnly: false }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/restart`, args, 'POST', extra?.signal));
|
|
117
117
|
tool('gripforge_server_scale', 'Scale max players', 'Update desired max players. Does not resize cloud hardware (Docker provider has no node pool). Mutating.', { id: serverId, maxPlayers: schema.number().int().min(1).max(200) }, { readOnly: false }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/scale`, args, 'POST', extra?.signal));
|
|
118
118
|
tool('gripforge_server_delete', 'Delete a game server', 'HIGH IMPACT. Stops the instance and deletes the server. Requires confirm=true. Owner only.', { id: serverId, confirm: schema.literal(true).describe('Must be true. Production-destructive.') }, { readOnly: false, destructive: true }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}`, args, 'DELETE', extra?.signal, { confirm: true }));
|
|
@@ -127,7 +127,7 @@ export function registerServerTools(register, options, schema = z) {
|
|
|
127
127
|
tool('gripforge_build_rollback', 'Rollback a build', 'Not implemented. Returns 501. Mutating / production-sensitive.', { id: buildId }, { readOnly: false, destructive: true }, (args, extra) => api(`game-builds/${encodeURIComponent(String(args.id))}/rollback`, args, 'POST', extra?.signal));
|
|
128
128
|
tool('gripforge_players_list', 'List connected players', 'Sessions reported by the Unity SDK. Empty until the SDK calls player.joined.', { id: serverId }, { readOnly: true }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/players`, args, 'GET', extra?.signal));
|
|
129
129
|
tool('gripforge_player_get', 'Get a player session', 'Read one player session on a server.', { id: serverId, player_id: playerId }, { readOnly: true }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/players/${encodeURIComponent(String(args.player_id))}`, args, 'GET', extra?.signal));
|
|
130
|
-
tool('gripforge_player_kick', 'Kick a player', 'Requires the Unity SDK. Currently returns 501. Mutating.', { id: serverId, player_id: playerId }, { readOnly: false }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/players/${encodeURIComponent(String(args.player_id))}/kick`, args, 'POST', extra?.signal));
|
|
130
|
+
tool('gripforge_player_kick', 'Kick a player', 'Requires the Unity SDK. Currently returns 501. Mutating.', { id: serverId, player_id: playerId }, { readOnly: false, destructive: true }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/players/${encodeURIComponent(String(args.player_id))}/kick`, args, 'POST', extra?.signal));
|
|
131
131
|
tool('gripforge_player_ban', 'Ban a player', 'HIGH IMPACT. Requires the Unity SDK. Currently returns 501. Owner only.', { id: serverId, player_id: playerId }, { readOnly: false, destructive: true }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/players/${encodeURIComponent(String(args.player_id))}/ban`, args, 'POST', extra?.signal));
|
|
132
132
|
tool('gripforge_game_broadcast', 'Broadcast to a match', 'Requires the Unity SDK. Currently returns 501. Never runs a shell on the host.', { id: serverId, message: schema.string().max(500).optional() }, { readOnly: false }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/broadcast`, args, 'POST', extra?.signal));
|
|
133
133
|
tool('gripforge_world_list', 'List worlds', 'Worlds stored for the workspace or a server’s project. Empty until worlds are implemented.', { id: serverId.optional() }, { readOnly: true }, (args, extra) => args.id
|
|
@@ -139,5 +139,5 @@ export function registerServerTools(register, options, schema = z) {
|
|
|
139
139
|
tool('gripforge_match_list', 'List matches', 'Matches recorded for a server (SDK or API).', { id: serverId }, { readOnly: true }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/matches`, args, 'GET', extra?.signal));
|
|
140
140
|
tool('gripforge_match_get', 'Get a match', 'Read one match record.', { id: serverId, match_id: matchId }, { readOnly: true }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/matches/${encodeURIComponent(String(args.match_id))}`, args, 'GET', extra?.signal));
|
|
141
141
|
tool('gripforge_match_create', 'Create a match record', 'Records match.started. Does not spawn a new process. Mutating.', { id: serverId, map: schema.string().max(120).optional() }, { readOnly: false }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/matches`, args, 'POST', extra?.signal));
|
|
142
|
-
tool('gripforge_match_stop', 'Stop a match', 'Marks the match ended. Mutating.', { id: serverId, match_id: matchId }, { readOnly: false }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/matches/${encodeURIComponent(String(args.match_id))}`, args, 'POST', extra?.signal));
|
|
142
|
+
tool('gripforge_match_stop', 'Stop a match', 'Marks the match ended. Mutating.', { id: serverId, match_id: matchId }, { readOnly: false, destructive: true }, (args, extra) => api(`servers/${encodeURIComponent(String(args.id))}/matches/${encodeURIComponent(String(args.match_id))}`, args, 'POST', extra?.signal));
|
|
143
143
|
}
|
package/dist/server.js
CHANGED
|
@@ -16,7 +16,22 @@ import { registerVfxProjectTools } from './vfx-project-tools.js';
|
|
|
16
16
|
import { registerSceneTools } from './scene-tools.js';
|
|
17
17
|
import { registerServerTools } from './server-tools.js';
|
|
18
18
|
import { registerGameKitTools } from './gamekit-tools.js';
|
|
19
|
+
import { registerJoystickTools } from './joystick-tools.js';
|
|
20
|
+
import { registerAbilityTools } from './ability-tools.js';
|
|
21
|
+
import { registerAssetProductionTools } from './asset-production-tools.js';
|
|
22
|
+
import { registerCreatureRigTools } from './creature-rig-tools.js';
|
|
23
|
+
import { registerWeaponMotionTools } from './weapon-motion-tools.js';
|
|
24
|
+
import { registerShieldVfxTools } from './shield-vfx-tools.js';
|
|
25
|
+
import { registerProjectileTools } from './projectile-tools.js';
|
|
26
|
+
import { registerArchitectureTools } from './architecture-tools.js';
|
|
27
|
+
import { registerGameConceptTools } from './game-concept-tools.js';
|
|
28
|
+
import { registerMotionTools } from './motion-tools.js';
|
|
29
|
+
import { registerHeroAbilityTools } from './hero-ability-tools.js';
|
|
30
|
+
import { registerGameCreationTools } from './game-creation-tools.js';
|
|
19
31
|
import { registerGameKitLocalTools } from './gamekit-deliver-local.js';
|
|
32
|
+
import { registerMapUnrealLocalTools } from './map-unreal-local.js';
|
|
33
|
+
import { registerMapUnrealImportLocalTools } from './map-unreal-import-local.js';
|
|
34
|
+
import { registerMapUnrealExportLocalTools } from './map-unreal-export-local.js';
|
|
20
35
|
const API_URL = process.env.GRIPFORGE_API_URL ?? 'https://gripforge.ai';
|
|
21
36
|
const API_KEY = process.env.GRIPFORGE_API_KEY;
|
|
22
37
|
const MCP_SELF = '0.1.9';
|
|
@@ -27,6 +42,21 @@ registerSceneTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKe
|
|
|
27
42
|
registerServerTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
28
43
|
registerGameKitTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
29
44
|
registerGameKitLocalTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY, userAgent: `gripforge-mcp/${MCP_SELF}` });
|
|
45
|
+
registerMapUnrealLocalTools(server.registerTool.bind(server));
|
|
46
|
+
registerMapUnrealImportLocalTools(server.registerTool.bind(server));
|
|
47
|
+
registerMapUnrealExportLocalTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
48
|
+
registerJoystickTools(server.registerTool.bind(server));
|
|
49
|
+
registerAbilityTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
50
|
+
registerCreatureRigTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
51
|
+
registerWeaponMotionTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
52
|
+
registerShieldVfxTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
53
|
+
registerProjectileTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
54
|
+
registerArchitectureTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
55
|
+
registerGameConceptTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
56
|
+
registerMotionTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
57
|
+
registerHeroAbilityTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
58
|
+
registerGameCreationTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
59
|
+
registerAssetProductionTools(server.registerTool.bind(server), { apiUrl: API_URL, getApiKey: () => API_KEY });
|
|
30
60
|
const err = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
31
61
|
server.tool('gripforge_attach', 'Attach a prop (sword, shield, gun, staff/scythe…) onto a rigged character. ' +
|
|
32
62
|
'Finds the hand bone across naming schemes, scales to body height, seats the grip, ' +
|
|
@@ -37,7 +67,9 @@ server.tool('gripforge_attach', 'Attach a prop (sword, shield, gun, staff/scythe
|
|
|
37
67
|
'GripForge puts that point in the palm and skips all grip heuristics. ' +
|
|
38
68
|
'Hand closing: rigs WITH finger bones get bind.gripPose rotations (applied by the snippets); ' +
|
|
39
69
|
'rigs WITHOUT finger bones (mitten hands) need export_glb: true + out_dir to receive ' +
|
|
40
|
-
'attached.glb with the fist baked in — the JSON bind alone cannot close a mitten hand.'
|
|
70
|
+
'attached.glb with the fist baked in — the JSON bind alone cannot close a mitten hand. ' +
|
|
71
|
+
'Attachment confidence is not visual validation. Inspect bind.review and preview the exported GLB ' +
|
|
72
|
+
'in the target renderer at idle and during attacks; do not call a needs_review result validated.', {
|
|
41
73
|
character_path: z.string().optional().describe('Absolute path to the rigged character (.glb .gltf .fbx .obj)'),
|
|
42
74
|
prop_path: z.string().optional().describe('Absolute path to the prop mesh'),
|
|
43
75
|
character_id: z.string().optional().describe('Library id of a character (lib_…)'),
|
|
@@ -218,7 +250,8 @@ server.tool('gripforge_attach', 'Attach a prop (sword, shield, gun, staff/scythe
|
|
|
218
250
|
JSON.stringify({ bind: data.bind, confidence: data.confidence, exports: exportsObj, wrote, glbFile, library }, null, 2) +
|
|
219
251
|
(credits ? `\ncredits: ${credits.remaining}/${credits.limit} remaining (${credits.plan})` : '') +
|
|
220
252
|
(glbNotes.length ? `\nglb notes:\n- ${glbNotes.join('\n- ')}` : '') +
|
|
221
|
-
mittenHint
|
|
253
|
+
mittenHint +
|
|
254
|
+
'\nVisual review required: verify palm contact, wrist continuity and weapon/body clearance in the target renderer. Bone parenting alone does not validate the grip.',
|
|
222
255
|
},
|
|
223
256
|
],
|
|
224
257
|
};
|
|
@@ -450,6 +483,7 @@ server.tool('gripforge_generate_character', 'CREATE a Mixamo T-pose auto-rigged
|
|
|
450
483
|
'Without an image, this is text-to-3D only. ' +
|
|
451
484
|
'If the look may already be in the locker, call gripforge_style_kit FIRST. ' +
|
|
452
485
|
'For knives/guns use gripforge_generate_weapon. Costs 10 credits. Returns job_id immediately; poll gripforge_generation_read. Closing this call does not cancel it.', {
|
|
486
|
+
idempotency_key: z.string().regex(/^[a-zA-Z0-9_.:-]{8,160}$/).optional().describe('Stable asset request key; reuse after an uncertain response.'),
|
|
453
487
|
prompt: z.string().min(3).max(600).describe('Description. "Devil May Cry like enemy" → tagged devil-may-cry, kind=enemy.'),
|
|
454
488
|
provider: z.enum(['tripo', 'meshy']).optional().describe('Default tripo. Forced meshy when concept_item / path / file_url is set.'),
|
|
455
489
|
name: z.string().max(160).optional().describe('Library item name (default: the prompt)'),
|
|
@@ -460,7 +494,7 @@ server.tool('gripforge_generate_character', 'CREATE a Mixamo T-pose auto-rigged
|
|
|
460
494
|
path: z.string().optional().describe('Absolute path to a local concept or T-pose image (PNG/JPG/WebP). Image-to-3D.'),
|
|
461
495
|
file_url: z.string().optional().describe('https URL of a concept or T-pose image. Image-to-3D.'),
|
|
462
496
|
out_dir: z.string().optional().describe('After the job completes, gripforge_library_pull into this folder.'),
|
|
463
|
-
}, async ({ prompt, provider, name, kind, polycount, underwear, concept_item, path, file_url, out_dir }) => {
|
|
497
|
+
}, async ({ idempotency_key, prompt, provider, name, kind, polycount, underwear, concept_item, path, file_url, out_dir }) => {
|
|
464
498
|
if (!API_KEY)
|
|
465
499
|
return err('GRIPFORGE_API_KEY missing.');
|
|
466
500
|
let image_base64;
|
|
@@ -488,6 +522,7 @@ server.tool('gripforge_generate_character', 'CREATE a Mixamo T-pose auto-rigged
|
|
|
488
522
|
method: 'POST',
|
|
489
523
|
headers: { ...apiHeaders(), 'content-type': 'application/json' },
|
|
490
524
|
body: JSON.stringify({
|
|
525
|
+
idempotency_key,
|
|
491
526
|
prompt,
|
|
492
527
|
provider: image_base64 || concept_item ? 'meshy' : provider,
|
|
493
528
|
name,
|
|
@@ -517,19 +552,21 @@ server.tool('gripforge_generate_character', 'CREATE a Mixamo T-pose auto-rigged
|
|
|
517
552
|
server.tool('gripforge_generate_weapon', 'CREATE a game-ready weapon GLB (knife, gun, sword) and save it to Library as kind=weapon. ' +
|
|
518
553
|
'Default Meshy, no T-pose, no rig. polycount default 4000. style=melee|gun for attach. 10 credits. ' +
|
|
519
554
|
'Do NOT write one-off Meshy scripts.', {
|
|
555
|
+
idempotency_key: z.string().regex(/^[a-zA-Z0-9_.:-]{8,160}$/).optional().describe('Stable asset request key; reuse after an uncertain response.'),
|
|
520
556
|
prompt: z.string().min(3).max(600).describe('e.g. CS2 default CT combat knife, silver blade, black grip'),
|
|
521
557
|
name: z.string().max(160).optional(),
|
|
522
558
|
polycount: z.number().optional().describe('Target triangles (default 4000)'),
|
|
523
559
|
style: z.enum(['melee', 'gun', 'shield', 'staff']).optional().describe('Grip style for attach (inferred if omitted)'),
|
|
524
560
|
provider: z.enum(['meshy', 'tripo']).optional().describe('Default meshy'),
|
|
525
561
|
out_dir: z.string().optional().describe('After the durable job completes, use gripforge_library_pull with this out_dir.'),
|
|
526
|
-
}, async ({ prompt, name, polycount, style, provider, out_dir }) => {
|
|
562
|
+
}, async ({ idempotency_key, prompt, name, polycount, style, provider, out_dir }) => {
|
|
527
563
|
if (!API_KEY)
|
|
528
564
|
return err('GRIPFORGE_API_KEY missing.');
|
|
529
565
|
const res = await fetch(`${API_URL}/api/v1/generate-character`, {
|
|
530
566
|
method: 'POST',
|
|
531
567
|
headers: { ...apiHeaders(), 'content-type': 'application/json' },
|
|
532
568
|
body: JSON.stringify({
|
|
569
|
+
idempotency_key,
|
|
533
570
|
prompt,
|
|
534
571
|
name,
|
|
535
572
|
kind: 'weapon',
|
|
@@ -554,24 +591,32 @@ server.tool('gripforge_generate_weapon', 'CREATE a game-ready weapon GLB (knife,
|
|
|
554
591
|
}
|
|
555
592
|
return { content: [{ type: 'text', text: JSON.stringify({ ...data, wrote, ...(out_dir && !wrote.length ? { next: 'Poll gripforge_generation_read, then gripforge_library_pull with the completed asset id and this out_dir.', requested_out_dir: out_dir } : {}) }, null, 2) }] };
|
|
556
593
|
});
|
|
557
|
-
server.tool('gripforge_generate_prop', 'CREATE a game-ready prop GLB (kart, crate, banana, barrier…) and save it to Library as kind=prop. Default Meshy, no T-pose, no rig. polycount default 4000. 10 credits. Returns a durable job_id; poll gripforge_generation_read.', {
|
|
594
|
+
server.tool('gripforge_generate_prop', 'CREATE a game-ready prop GLB (kart, crate, banana, barrier…) and save it to Library as kind=prop. Default Meshy, no T-pose, no rig. polycount default 4000. Quoted against the shared subscription, or 10 GripForge credits if it cannot cover the complete operation. Returns a durable job_id; poll gripforge_generation_read.', {
|
|
595
|
+
idempotency_key: z.string().regex(/^[a-zA-Z0-9_.:-]{8,160}$/).optional().describe('Stable asset request key; reuse after an uncertain response.'),
|
|
558
596
|
prompt: z.string().min(3).max(600).describe('e.g. compact orange open-cockpit racing kart, isolated'),
|
|
559
597
|
name: z.string().max(160).optional(),
|
|
560
598
|
polycount: z.number().optional().describe('Target triangles (default 4000)'),
|
|
561
599
|
provider: z.enum(['meshy', 'tripo']).optional().describe('Default meshy'),
|
|
600
|
+
concept_item: z.string().min(1).max(160).optional().describe('Owned concept image in this workspace; forces Meshy image-to-3D.'),
|
|
601
|
+
concept_items: z.array(z.string().min(1).max(160)).min(2).max(4).optional().describe('2–4 distinct owned views of the SAME prop for Meshy multi-image-to-3D. Do not combine with concept_item. Geometry standard/2k only.'),
|
|
602
|
+
meshQuality: z.object({ geometry: z.enum(['standard', '2k', '4k']), texture: z.enum(['2k', '4k', '8k']) }).optional().describe('Meshy geometry and PBR texture quality. Requested texture resolution is retained through optimization.'),
|
|
603
|
+
optimize: z.boolean().optional().describe('Default true; false retains the provider mesh for close-up review.'),
|
|
562
604
|
out_dir: z.string().optional().describe('After the durable job completes, use gripforge_library_pull with this out_dir.'),
|
|
563
|
-
|
|
605
|
+
subtype: z.enum(['vehicle', 'none']).optional().describe('vehicle: made as the Vehicles prop sub-kind — no grip style, nose on +Z, the four wheels split with hub pivots so it drives. Then finished like a game car: paint, glass, chrome, grille and lamp materials, smoothed body, modelled wheels. Detected from the prompt (car, truck, van, kart…) when omitted; none opts out.'),
|
|
606
|
+
}, async ({ idempotency_key, prompt, name, polycount, provider, concept_item, concept_items, meshQuality, optimize, out_dir, subtype }) => {
|
|
564
607
|
if (!API_KEY)
|
|
565
608
|
return err('GRIPFORGE_API_KEY missing.');
|
|
566
609
|
const res = await fetch(`${API_URL}/api/v1/generate-character`, {
|
|
567
610
|
method: 'POST',
|
|
568
611
|
headers: { ...apiHeaders(), 'content-type': 'application/json' },
|
|
569
612
|
body: JSON.stringify({
|
|
613
|
+
idempotency_key,
|
|
570
614
|
prompt,
|
|
571
615
|
name,
|
|
572
616
|
kind: 'prop',
|
|
617
|
+
subtype,
|
|
573
618
|
polycount: polycount ?? 4000,
|
|
574
|
-
provider: provider ?? 'meshy',
|
|
619
|
+
provider: provider ?? 'meshy', concept_item, concept_items, meshQuality, optimize,
|
|
575
620
|
}),
|
|
576
621
|
});
|
|
577
622
|
const data = (await res.json().catch(() => ({})));
|
|
@@ -829,11 +874,13 @@ server.tool('gripforge_concept_correct', 'Turn a character concept image into a
|
|
|
829
874
|
return err(String(data.error ?? res.status));
|
|
830
875
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
831
876
|
});
|
|
832
|
-
server.tool('gripforge_library_list', 'List the GripForge Library locker (characters, enemies, weapons, props, textures, HUD, buttons, binds). Filter by kind and/or game style.', {
|
|
877
|
+
server.tool('gripforge_library_list', 'List the GripForge Library locker (characters, enemies, weapons, props, textures, HUD, buttons, binds). Filter by kind and/or game style. Every item carries `rank` {score/100, grade A–D, reasons}: fitness for a GripForge game; sort=rank lists the best fit first.', {
|
|
833
878
|
kind: KIND.optional().describe('Filter by kind'),
|
|
834
879
|
q: z.string().optional().describe('Search name/filename'),
|
|
835
880
|
style: z.string().optional().describe('Game look (devil-may-cry, dmc, genshin)'),
|
|
836
|
-
|
|
881
|
+
sort: z.enum(['newest', 'rank']).optional().describe('newest (default) or rank: best fit first'),
|
|
882
|
+
target: z.enum(['mobile', 'desktop']).optional().describe('Platform for the rank budgets. Defaults to mobile.'),
|
|
883
|
+
}, async ({ kind, q, style, sort, target }) => {
|
|
837
884
|
if (!API_KEY)
|
|
838
885
|
return err('GRIPFORGE_API_KEY missing.');
|
|
839
886
|
const qs = new URLSearchParams();
|
|
@@ -843,12 +890,34 @@ server.tool('gripforge_library_list', 'List the GripForge Library locker (charac
|
|
|
843
890
|
qs.set('q', q);
|
|
844
891
|
if (style)
|
|
845
892
|
qs.set('style', style);
|
|
893
|
+
if (sort === 'rank')
|
|
894
|
+
qs.set('sort', 'rank');
|
|
895
|
+
if (target)
|
|
896
|
+
qs.set('target', target);
|
|
846
897
|
const res = await fetch(`${API_URL}/api/v1/library?${qs}`, { headers: apiHeaders() });
|
|
847
898
|
const data = await res.json().catch(() => ({}));
|
|
848
899
|
if (!res.ok)
|
|
849
900
|
return err(String(data.error ?? res.status));
|
|
850
901
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
851
902
|
});
|
|
903
|
+
server.tool('gripforge_library_optimize', 'Derive a lighter version of a static model (prop, weapon, equipment, vehicle) for a target: decimated to `triangles`, textures bounded to `texture` px, saved as a NEW Library item linked to its master (meta.lod_of). The master is never changed — generate high once, derive mobile/desktop versions. Wheel pivots and nodes are kept. 0 credits.', {
|
|
904
|
+
id: z.string().describe('Library id of the master model (lib_…)'),
|
|
905
|
+
triangles: z.number().int().min(500).max(200000).describe('Target triangles, e.g. 12000 for a mobile car, 40000 desktop'),
|
|
906
|
+
texture: z.union([z.literal(512), z.literal(1024), z.literal(2048), z.literal(4096)]).optional().describe('Max texture size in px (default 2048)'),
|
|
907
|
+
name: z.string().max(160).optional(),
|
|
908
|
+
}, async ({ id, triangles, texture, name }) => {
|
|
909
|
+
if (!API_KEY)
|
|
910
|
+
return err('GRIPFORGE_API_KEY missing.');
|
|
911
|
+
const res = await fetch(`${API_URL}/api/v1/library/${encodeURIComponent(id)}/optimize`, {
|
|
912
|
+
method: 'POST',
|
|
913
|
+
headers: { ...apiHeaders(), 'content-type': 'application/json' },
|
|
914
|
+
body: JSON.stringify({ triangles, texture, name }),
|
|
915
|
+
});
|
|
916
|
+
const data = await res.json().catch(() => ({}));
|
|
917
|
+
if (!res.ok)
|
|
918
|
+
return err(String(data.error ?? res.status));
|
|
919
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
920
|
+
});
|
|
852
921
|
server.tool('gripforge_library_get', 'Get one Library item (metadata + signed file URL).', { id: z.string().describe('Library id (lib_…)') }, async ({ id }) => {
|
|
853
922
|
if (!API_KEY)
|
|
854
923
|
return err('GRIPFORGE_API_KEY missing.');
|
|
@@ -1110,7 +1179,7 @@ server.tool('gripforge_audio_kit', 'Search locker + Community SFX by description
|
|
|
1110
1179
|
hits,
|
|
1111
1180
|
wrote: [],
|
|
1112
1181
|
hint: wanted
|
|
1113
|
-
? 'Pick by description/use. Locker ids: gripforge_library_pull. Community ids: clone first (
|
|
1182
|
+
? 'Pick by description/use. Locker ids: gripforge_library_pull. Community ids: clone first (free unless the creator sets a price in credits), then pull.'
|
|
1114
1183
|
: 'Pass prompt to filter. Descriptions are on each hit.',
|
|
1115
1184
|
};
|
|
1116
1185
|
if (out_dir) {
|
|
@@ -1343,16 +1412,73 @@ server.tool('gripforge_hitbox', 'Body capsules + weapons[] (one per gf_prop_ wra
|
|
|
1343
1412
|
return err(String(data.error ?? res.status));
|
|
1344
1413
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
1345
1414
|
});
|
|
1346
|
-
server.tool('gripforge_vehicle_wheels', 'Split the 4 wheels out of a fused kart/car GLB so they can spin: Wheel_FL/FR/RL/RR nodes, hub pivots, extras gf_wheel {radius, axle, spin}. Geometric cut (cylinder + plane, seam invariant under rotation, capped).
|
|
1415
|
+
server.tool('gripforge_vehicle_wheels', 'Split the 4 wheels out of a fused kart/car GLB so they can spin: Wheel_FL/FR/RL/RR nodes, hub pivots, extras gf_wheel {radius, radiusMeters, axle, spin}. Geometric cut (cylinder + plane, seam invariant under rotation, capped). normalize=true also puts the nose on +Z, the real size in metres and the wheels on y = 0 (works on a vehicle already split). 0 credits. apply=false → analysis only.', {
|
|
1347
1416
|
id: z.string().describe('Library id of the vehicle GLB (lib_…)'),
|
|
1348
1417
|
apply: z.boolean().optional().describe('default true — rewrite the Library file with the split wheels'),
|
|
1349
|
-
|
|
1418
|
+
normalize: z.boolean().optional().describe('nose on +Z, metres, wheels on the ground'),
|
|
1419
|
+
reverse: z.boolean().optional().describe('the nose came out backwards: turn the vehicle 180° (with normalize)'),
|
|
1420
|
+
length_m: z.number().min(0.5).max(25).optional().describe('bumper-to-bumper length in metres; default: typical for the vehicle named (car 4.4, SUV 4.7, pickup 5.8, van 5.5, kart 1.8…)'),
|
|
1421
|
+
}, async ({ id, apply, normalize, length_m, reverse }) => {
|
|
1350
1422
|
if (!API_KEY)
|
|
1351
1423
|
return err('GRIPFORGE_API_KEY missing.');
|
|
1352
1424
|
const res = await fetch(`${API_URL}/api/v1/vehicle-wheels`, {
|
|
1353
1425
|
method: 'POST',
|
|
1354
1426
|
headers: { ...apiHeaders(), 'content-type': 'application/json' },
|
|
1355
|
-
body: JSON.stringify({ id, apply }),
|
|
1427
|
+
body: JSON.stringify({ id, apply, normalize, length_m, reverse }),
|
|
1428
|
+
});
|
|
1429
|
+
const data = await res.json().catch(() => ({}));
|
|
1430
|
+
if (!res.ok)
|
|
1431
|
+
return err(String(data.error ?? res.status));
|
|
1432
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
1433
|
+
});
|
|
1434
|
+
server.tool('gripforge_vehicle_finish', 'Repair generated automotive surfaces. Preferred: source (immutable static GLB with Body/Wheel_* groups) + repair recipe queues a persistent Blender job; guided glazing/panel regions, paint cleanup, coloured caliper extraction, measured replacement panels/curves with PBR and fitted boundaries, optional rebuilt wheels and rigid wheel rig. features.marks adds surface-fitted pinned PNG logos, relief badges and mesh lettering; wheel markings follow their wheel, named authored marks are replaced in the new work copy. For grilles or trim on a reconstructed recess, set curves.fitSurface.panel to its non-structural panel name to follow the final part. Returns a NEW private work version with GLB/Blend/optional FBX and GripForge review link. Does not promote or overwrite the source. Regions use +Y up/+Z forward metres, bounded displacement; region-only glass stays opaque, authored transmissive panes need an authored cabin. Poll/cancel/retry generation-jobs. 0 provider credits. Legacy id without repair uses one-time material/wheel finish and rewrites its Library file.', {
|
|
1435
|
+
id: z.string().optional().describe('Library id; with repair pins source, without repair uses legacy finish'),
|
|
1436
|
+
source: z.object({ assetId: z.string(), revisionId: z.string(), fileRole: z.string().optional() }).optional().describe('Pinned static source, instead of id'),
|
|
1437
|
+
repair: z.object({
|
|
1438
|
+
regions: z.array(z.object({ name: z.string(), bounds: z.tuple([z.tuple([z.number(), z.number(), z.number()]), z.tuple([z.number(), z.number(), z.number()])]), select: z.enum(['neutral', 'paint', 'all']), surface: z.enum(['glass', 'paint']), fitAxis: z.enum(['x', 'y', 'z']), maxOffsetM: z.number().min(0).max(.05).optional() })).max(16),
|
|
1439
|
+
denoise: z.object({ iterations: z.number().int().min(1).max(80).optional(), maxOffsetM: z.number().min(0).max(.05).optional(), normalIterations: z.number().int().min(0).max(30).optional() }).optional().describe('Bounded Body smoothing across UV seams; pins open boundaries, retains UVs and wheel/caliper transforms; review small details'),
|
|
1440
|
+
cleanPaint: z.boolean().optional(), paintColor: z.tuple([z.number(), z.number(), z.number()]).optional(),
|
|
1441
|
+
calipers: z.object({ color: z.tuple([z.number(), z.number(), z.number()]), tolerance: z.number().min(.05).max(.4).optional() }).optional(),
|
|
1442
|
+
features: z.object({
|
|
1443
|
+
replaceBody: z.boolean().optional().describe('Explicitly replace coachwork in a new work copy using structural panels; preserve wheels and Body calipers.'),
|
|
1444
|
+
panels: z.array(z.object({
|
|
1445
|
+
name: z.string(), points: z.array(z.array(z.tuple([z.number(), z.number(), z.number()])).min(2).max(12)).min(2).max(12),
|
|
1446
|
+
structural: z.boolean().optional().describe('New coachwork shell, required with replaceBody; cannot fit or clip the discarded source.'),
|
|
1447
|
+
normal: z.tuple([z.number(), z.number(), z.number()]).optional(),
|
|
1448
|
+
segmentsU: z.number().int().min(4).max(96).optional(), segmentsV: z.number().int().min(4).max(96).optional(),
|
|
1449
|
+
mirrorX: z.boolean().optional(), material: z.object({ color: z.tuple([z.number(), z.number(), z.number()]).optional(), metallic: z.number().min(0).max(1).optional(), roughness: z.number().min(.02).max(1).optional(), coat: z.number().min(0).max(1).optional(), transmission: z.number().min(0).max(1).optional(), ior: z.number().min(1).max(2.5).optional(), emission: z.number().min(0).max(10).optional() }).optional(),
|
|
1450
|
+
replace: z.object({ axis: z.enum(['x', 'y', 'z']), depthM: z.number().min(.001).max(.5).optional() }).optional(),
|
|
1451
|
+
fitBoundary: z.object({ axis: z.enum(['x', 'y', 'z']), direction: z.union([z.literal(1), z.literal(-1)]).optional(), offsetM: z.number().min(0).max(.01).optional(), fullSurface: z.boolean().optional() }).optional(), sealRadiusM: z.number().min(.0005).max(.03).optional(),
|
|
1452
|
+
})).max(32).optional(),
|
|
1453
|
+
curves: z.array(z.object({
|
|
1454
|
+
name: z.string(), points: z.array(z.tuple([z.number(), z.number(), z.number()])).min(2).max(128),
|
|
1455
|
+
fitSurface: z.object({ axis: z.enum(['x', 'y', 'z']), direction: z.union([z.literal(1), z.literal(-1)]).optional(), offsetM: z.number().min(0).max(.01).optional(), panel: z.string().trim().min(1).max(80).optional() }).optional(),
|
|
1456
|
+
radiusM: z.number().min(.0005).max(.08).optional(), closed: z.boolean().optional(), mirrorX: z.boolean().optional(),
|
|
1457
|
+
material: z.object({ color: z.tuple([z.number(), z.number(), z.number()]).optional(), metallic: z.number().min(0).max(1).optional(), roughness: z.number().min(.02).max(1).optional(), coat: z.number().min(0).max(1).optional(), transmission: z.number().min(0).max(1).optional(), ior: z.number().min(1).max(2.5).optional(), emission: z.number().min(0).max(10).optional() }).optional(),
|
|
1458
|
+
})).max(256).optional(),
|
|
1459
|
+
marks: z.array(z.object({
|
|
1460
|
+
name: z.string().min(1).max(80), kind: z.enum(['decal', 'badge', 'text']), target: z.enum(['Body', 'Wheel_FL', 'Wheel_FR', 'Wheel_RL', 'Wheel_RR']).optional(),
|
|
1461
|
+
receiver: z.string().trim().min(1).max(120).optional(), position: z.tuple([z.number(), z.number(), z.number()]), normal: z.tuple([z.number(), z.number(), z.number()]), up: z.tuple([z.number(), z.number(), z.number()]),
|
|
1462
|
+
widthM: z.number().min(.005).max(.8), heightM: z.number().min(.005).max(.8), offsetM: z.number().min(.0002).max(.01).optional(), depthM: z.number().min(0).max(.01).optional(), maxDistanceM: z.number().min(.001).max(.12).optional(), segments: z.number().int().min(4).max(32).optional(), mirrorX: z.boolean().optional(),
|
|
1463
|
+
outline: z.array(z.tuple([z.number().min(-.5).max(.5), z.number().min(-.5).max(.5)])).min(3).max(64).optional(),
|
|
1464
|
+
artwork: z.object({ assetId: z.string(), revisionId: z.string(), fileRole: z.string().optional() }).optional(), text: z.string().min(1).max(48).optional(),
|
|
1465
|
+
material: z.object({ color: z.tuple([z.number(), z.number(), z.number()]).optional(), metallic: z.number().min(0).max(1).optional(), roughness: z.number().min(.02).max(1).optional(), coat: z.number().min(0).max(1).optional(), transmission: z.number().min(0).max(1).optional(), ior: z.number().min(1).max(2.5).optional(), emission: z.number().min(0).max(10).optional() }).optional(),
|
|
1466
|
+
})).max(32).optional().describe('Surface-fitted logos and relief: decals require pinned PNG artwork; badges add thickness and a convex outline; text makes mesh lettering. Explicit normal/up frame; target wheel marks spin with that wheel. Optional receiver scopes fitting to a mesh in the target. PNGs <=1 MiB/2048px, max 16 distinct sources. No URLs or local paths.'),
|
|
1467
|
+
}).optional().describe('Measured panels, curves, badges and decals; explicit PBR, clipping, symmetry, 250k vertex budget.'),
|
|
1468
|
+
rig: z.object({ wheelRadius: z.number(), wheelWidth: z.number() }).optional(),
|
|
1469
|
+
wheelRebuild: z.object({ radiusM: z.number().min(.1).max(1.5), widthM: z.number().min(.05).max(.8), spokes: z.number().int().min(3).max(12).optional(), segments: z.number().int().min(24).max(128).optional(), trackM: z.number().min(.3).max(6).optional() }).optional().describe('Replace generated wheel meshes with measured tyres, dished spokes and brake rotors. Optional trackM adjusts axle track and matching explicit Body calipers together; omitted preserves wheel pivots.'),
|
|
1470
|
+
}).optional().describe('Guided surface repair; never edits the source'),
|
|
1471
|
+
name: z.string().optional(), idempotency_key: z.string().min(8).max(160).optional(),
|
|
1472
|
+
smooth: z.number().int().min(0).max(60).optional().describe('body smoothing passes (default 15, 0 = none)'),
|
|
1473
|
+
wheels: z.boolean().optional().describe('false keeps the generated wheels'),
|
|
1474
|
+
segments: z.number().int().min(12).max(128).optional().describe('wheel roundness: 64 default, 24 for mobile'),
|
|
1475
|
+
}, async (args) => {
|
|
1476
|
+
if (!API_KEY)
|
|
1477
|
+
return err('GRIPFORGE_API_KEY missing.');
|
|
1478
|
+
const res = await fetch(`${API_URL}/api/v1/vehicle-finish`, {
|
|
1479
|
+
method: 'POST',
|
|
1480
|
+
headers: { ...apiHeaders(), 'content-type': 'application/json' },
|
|
1481
|
+
body: JSON.stringify(args),
|
|
1356
1482
|
});
|
|
1357
1483
|
const data = await res.json().catch(() => ({}));
|
|
1358
1484
|
if (!res.ok)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
const styles = ['fire', 'water', 'water-petal', 'blood', 'cat', 'cosmic', 'cyber', 'cyber-grid', 'holy', 'ice', 'ink', 'light', 'lightning', 'poison', 'rock', 'void', 'wind'];
|
|
3
|
+
export const SHIELD_VFX_TOOL_NAMES = ['gripforge_shield_vfx', 'gripforge_shield_export'];
|
|
4
|
+
export function registerShieldVfxTools(register, options, s = z) {
|
|
5
|
+
const config = s.object({ version: s.literal(1).optional(), style: s.enum(styles).optional(), radius: s.number().min(.15).max(5).optional(), stretch: s.tuple([s.number().min(.25).max(2), s.number().min(.25).max(2), s.number().min(.25).max(2)]).optional(), color: s.string().regex(/^#[a-f\d]{6}$/i).optional(), accent: s.string().regex(/^#[a-f\d]{6}$/i).optional(), intensity: s.number().min(.1).max(3).optional(), opacity: s.number().min(0).max(1).optional(), speed: s.number().min(.1).max(4).optional(), seed: s.number().int().min(0).max(65535).optional(), quality: s.enum(['low', 'high']).optional() }).strict();
|
|
6
|
+
const common = { workspace_id: s.string().max(100).optional(), config: config.optional() };
|
|
7
|
+
const definitions = [{ name: SHIELD_VFX_TOOL_NAMES[0], shape: { ...common, save: s.boolean().optional(), idempotency_key: s.string().min(8).max(160).optional() }, description: 'ADMIN ONLY. Create an original editable 3D elemental shield using the shared shield-vfx module: 17 styles, radius/stretch, colours, intensity, quality and seed. Same shaders used in Studio and native exports. save=true by default queues persistent rendering/review/workspace save; poll generation_read for Studio URL. save=false returns an unsaved project. No commercial reference assets, collision or damage. Recipe compilation needs no AI; saving uses the normal VFX review pipeline. Work is not visually validated automatically.' },
|
|
8
|
+
{ name: SHIELD_VFX_TOOL_NAMES[1], shape: { ...common, engine: s.enum(['threejs', 'godot', 'unity', 'unreal']) }, description: 'ADMIN ONLY. Compile a shield recipe into reusable native source files for Three.js, Godot 4, Unity or Unreal 5, using the same geometry and shader functions. Returns files keyed by relative path plus install instructions. Write ALL files into a dedicated directory; never overwrite an existing modified package without checking. Includes enable/hit/shatter runtime; no mannequin or preview environment. Native builds require target-engine verification; source export is not visual certification.' }];
|
|
9
|
+
for (const d of definitions)
|
|
10
|
+
register(d.name, { title: d.name, description: d.description, inputSchema: d.shape, annotations: { readOnlyHint: d.name.endsWith('export'), destructiveHint: false, idempotentHint: d.name.endsWith('export'), openWorldHint: false } }, async (args, extra) => {
|
|
11
|
+
const key = options.getApiKey();
|
|
12
|
+
if (!key)
|
|
13
|
+
return { isError: true, content: [{ type: 'text', text: 'GripForge API key required.' }] };
|
|
14
|
+
const parsed = s.object(d.shape).strict().safeParse(args);
|
|
15
|
+
if (!parsed.success)
|
|
16
|
+
return { isError: true, content: [{ type: 'text', text: parsed.error.message }] };
|
|
17
|
+
const { workspace_id, ...body } = parsed.data;
|
|
18
|
+
try {
|
|
19
|
+
const r = await fetch(options.apiUrl.replace(/\/$/, '') + '/api/v1/vfx/shields', { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': key, ...(workspace_id ? { 'x-workspace-id': String(workspace_id) } : {}) }, body: JSON.stringify({ ...body, action: d.name.endsWith('export') ? 'export' : body.save === false ? 'preview' : 'create' }), signal: AbortSignal.any([AbortSignal.timeout(90000), ...(extra?.signal ? [extra.signal] : [])]) });
|
|
20
|
+
const data = await r.json();
|
|
21
|
+
if (data.studio_url?.startsWith('/'))
|
|
22
|
+
data.studio_url = new URL(data.studio_url, options.apiUrl).href;
|
|
23
|
+
return { ...(!r.ok ? { isError: true } : {}), structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
return { isError: true, content: [{ type: 'text', text: e.message }] };
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|