@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.
@@ -4,12 +4,23 @@ export function registerSceneTools(register, options, schema = z) {
4
4
  const identifier = schema.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,159}$/);
5
5
  const record = schema.record(schema.string(), schema.unknown());
6
6
  const revision = schema.number().int().positive();
7
+ const imageRef = schema.object({ assetId: identifier, revisionId: identifier, fileRole: identifier.optional() });
8
+ const mapWizard = schema.object({
9
+ style: schema.enum(['realistic', 'stylized', 'lowpoly', 'handpainted']).optional(),
10
+ world: schema.enum(['open_world', 'closed_arena', 'dungeon', 'linear', 'battle_map']).optional(),
11
+ terrain: schema.enum(['auto', 'plains', 'desert_canyon', 'forest', 'snow', 'island', 'volcanic']).optional(),
12
+ size: schema.enum(['small', 'medium', 'large']).optional(),
13
+ 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.'),
14
+ boundary: schema.enum(['fixed', 'infinite']).optional(), spawn: schema.enum(['single', 'teams']).optional(),
15
+ teams: schema.union([schema.literal(2), schema.literal(4)]).optional(),
16
+ boss: schema.boolean().optional(), safeZone: schema.boolean().optional(), extraction: schema.boolean().optional(),
17
+ }).passthrough();
7
18
  const workspace = { workspace_id: schema.string().max(100).optional().describe('Workspace authorized by the current key. Returned scene links open this workspace.') };
8
19
  async function call(path, args, method, signal) {
9
20
  const key = options.getApiKey();
10
21
  if (!key)
11
22
  return { isError: true, content: [{ type: 'text', text: 'GripForge API key required.' }] };
12
- const { workspace_id, id: _id, version: _version, include_preview, ...body } = args;
23
+ const { workspace_id, id: _id, version: _version, include_preview, preview_view, ...body } = args;
13
24
  try {
14
25
  const response = await fetch(`${options.apiUrl.replace(/\/$/, '')}/api/v1/${path}`, {
15
26
  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 +34,47 @@ export function registerSceneTools(register, options, schema = z) {
23
34
  if (result && typeof result === 'object' && typeof result.studio_url === 'string')
24
35
  result.studio_url = new URL(String(result.studio_url), options.apiUrl).href;
25
36
  const content = [{ type: 'text', text: JSON.stringify(data) }];
26
- const preview = data.result?.preview_url;
27
- if (response.ok && include_preview === true && typeof preview === 'string' && preview.startsWith('/api/v1/generation-jobs/')) {
28
- const image = await fetch(new URL(preview, 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] : [])]) });
29
- if (!image.ok || !image.headers.get('content-type')?.startsWith('image/png'))
30
- throw Error('The job preview could not be read. The completed job remains available.');
31
- const bytes = await image.arrayBuffer();
32
- if (bytes.byteLength > 12 * 1024 * 1024)
33
- throw Error('Preview exceeds the image budget.');
34
- content.push({ type: 'image', mimeType: 'image/png', data: Buffer.from(bytes).toString('base64') });
37
+ const result = data.result;
38
+ if (response.ok && include_preview === true) {
39
+ 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 }] : [];
40
+ if (typeof preview_view === 'string') {
41
+ previews = previews.filter(view => view.view === preview_view);
42
+ if (!previews.length)
43
+ throw Error('This preview view is not available yet. The job remains available.');
44
+ }
45
+ const images = await Promise.all(previews.map(async (preview) => {
46
+ const path = preview.preview_url ?? ('image_url' in preview ? preview.image_url : undefined);
47
+ if (typeof path !== 'string' || !/^\/api\/v1\/(?:generation-jobs|scene-assets)\//.test(path))
48
+ throw Error('Invalid job preview URL.');
49
+ 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] : [])]) });
50
+ if (!image.ok || !image.headers.get('content-type')?.startsWith('image/png'))
51
+ throw Error('The job preview could not be read. The completed job remains available.');
52
+ const bytes = await image.arrayBuffer();
53
+ if (bytes.byteLength > 12 * 1024 * 1024)
54
+ throw Error('Preview exceeds the image budget.');
55
+ return { label: preview.label ?? preview.view.replaceAll('_', ' '), data: Buffer.from(bytes).toString('base64') };
56
+ }));
57
+ for (const image of images) {
58
+ if (images.length > 1)
59
+ content.push({ type: 'text', text: image.label });
60
+ content.push({ type: 'image', mimeType: 'image/png', data: image.data });
61
+ }
35
62
  }
63
+ for (const result of [data, data.result, job?.result])
64
+ if (result && typeof result === 'object') {
65
+ const row = result;
66
+ for (const key of ['studio_url', 'review_url', 'file_url', 'source_url', 'concept_url', 'status_url'])
67
+ if (typeof row[key] === 'string' && row[key].startsWith('/'))
68
+ row[key] = new URL(row[key], options.apiUrl).href;
69
+ if (Array.isArray(row.concept_views))
70
+ for (const view of row.concept_views)
71
+ if (view && typeof view === 'object') {
72
+ for (const key of ['image_url', 'preview_url', 'studio_url'])
73
+ if (typeof view[key] === 'string' && view[key].startsWith('/'))
74
+ view[key] = new URL(view[key], options.apiUrl).href;
75
+ }
76
+ }
77
+ content[0] = { type: 'text', text: JSON.stringify(data) };
36
78
  return { ...(!response.ok ? { isError: true } : {}), structuredContent: data, content };
37
79
  }
38
80
  catch (error) {
@@ -40,6 +82,15 @@ export function registerSceneTools(register, options, schema = z) {
40
82
  }
41
83
  }
42
84
  const tool = (name, title, description, inputSchema, readOnly, callback) => register(name, { title, description, inputSchema: { ...inputSchema, ...workspace }, annotations: { readOnlyHint: readOnly, destructiveHint: false, idempotentHint: readOnly, openWorldHint: false } }, callback);
85
+ tool('gripforge_vehicle_doors', 'Open or close vehicle doors in the Studio', 'Save an independent door pose on one prepared vehicle instance in a saved scene. doors maps Door_FL/FR/RL/RR to 0 (closed)..1 (open). Unspecified doors retain their state; null clears the manual pose. Requires exact expectedRevision and named hinges/portable clips from prepare_vehicle. Pauses the global animation for manual control. Preserves the source asset, transforms, other instances and validated versions. Returns the actual Studio link. No generation charge.', { id: identifier, expectedRevision: revision, nodeId: identifier, doors: schema.record(schema.string().regex(/^Door_[FR][LR]$/), schema.number().min(0).max(1)).nullable() }, false, (args, extra) => call(`scenes/${args.id}/vehicle-doors`, args, 'POST', extra?.signal));
86
+ tool('gripforge_prepare_vehicle', 'Prepare segmented vehicle hinges in Blender', 'Durable Blender preparation of an owned static segmented GLB. Map each Door_FL/FR/RL/RR or Wheel_FL/FR/RL/RR to exact mesh node names from the segmentation report. Keeps geometry placement, UVs and PBR; normalizes +Z forward/metres using yaw and length; adds hinge origins, seat/entry sockets and portable GLB open/close/preview clips. Optional hinge coordinates are in normalized vehicle metres. thickness adds an inward door shell, not a full cabin. Optional cuts provide strictly convex door outlines [forwardZ,heightY] and depth [near,far] measured from the centre on that side, in normalized metres. Blender cuts those explicit profiles while interpolating UVs. Does not infer outlines or guarantee clearance. Returns job_id, new private work GLB, Blender source and animated Studio review link. 0 credits; storage applies. Inspect before replacement.', { source: imageRef, parts: schema.record(schema.string(), schema.array(schema.string().min(1).max(160)).min(1).max(64)).optional(), cuts: schema.array(schema.object({ role: schema.enum(['Door_FL', 'Door_FR', 'Door_RL', 'Door_RR']), outline: schema.array(schema.tuple([schema.number(), schema.number()])).min(3).max(12), depth: schema.tuple([schema.number(), schema.number()]) })).max(4).optional(), yaw: schema.number().min(-180).max(180).optional(), length: schema.number().min(2.8).max(7).optional(), openAngle: schema.number().min(20).max(85).optional(), thickness: schema.number().min(0).max(.04).optional(), hinges: schema.record(schema.string(), schema.tuple([schema.number(), schema.number(), schema.number()])).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('vehicles/prepare', args, 'POST', extra?.signal));
87
+ tool('gripforge_vehicle_schema', 'Modular vehicle manufacturing contract', 'Read the concept-first vehicle recipe, canonical parts, hinge convention, limits, quotes and revision workflow.', {}, true, (args, extra) => call('vehicles', args, 'GET', extra?.signal));
88
+ tool('gripforge_generate_vehicle', 'Generate a vehicle from reviewed multi-view references', 'Read vehicle_schema first. Recommended recipe.workflow=body_wheels follows concept → references (five wheel-less body views + wheel) → geometry (stop/review) → topology (stop/review) → build (PBR + wheel + rig). Each geometry review uses scene_review with the returned assetRef and must be approved before the next paid step. Rigging preserves UVs and corner normals, GLB/FBX/Blend and measured Chaos setup. Top view is review-only. parts.body.geometry accepts unfinished Hunyuan/Blender GLB; parts.body.topology pins the reviewed reduced GLB; parts.<role>.source reuses finished meshes. Legacy modular_doors remains available for articulated doors. stage=plan quotes each stage without spending; paid stages require its budget. Durable job; poll generation_read. Work draft, real-render review and target-engine driving test required; no automatic publication/replacement.', { stage: schema.enum(['plan', 'concept', 'references', 'geometry', 'topology', 'build']).default('plan'), recipe: record, budget: schema.object({ usd: schema.number().nonnegative().optional(), credits: schema.number().int().nonnegative().optional() }).optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('vehicles', args, 'POST', extra?.signal));
89
+ 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));
90
+ 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));
91
+ 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));
92
+ 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));
93
+ 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
94
  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
95
  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
96
  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 +103,32 @@ export function registerSceneTools(register, options, schema = z) {
52
103
  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
104
  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
105
  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('gripforge_map_generate', 'Generate an editable map', 'Queue persistent map generation into the common Scene Engine. wizard: style realistic|stylized|lowpoly|handpainted; world open_world|closed_arena|dungeon|linear|battle_map; terrain plains|desert_canyon|forest|snow|island|volcanic; size small|medium|large; boundary fixed|infinite (open edge, finite terrain); spawn single|teams; teams 2|4. Result contains independent terrain, regions, paths, spawn zones and asset instances. Work is not automatically validated. Poll generation_read.', { prompt: schema.string().min(3).max(4000), wizard: record, seed: schema.number().int().optional(), idempotency_key: schema.string().min(8).max(160).optional() }, false, (args, extra) => call('generation-jobs/map', args, 'POST', extra?.signal));
106
+ tool('gripforge_map_schema', 'Map concept and generation workflow', 'Read the concept-first map workflow, immutable image reference format, costs and an example. Concepts, map work scenes and external engine delivery are separate steps.', {}, true, (args, extra) => call('generation-jobs/map', args, 'GET', extra?.signal));
107
+ tool('gripforge_map_generate', 'Create map concept views or an editable map', 'Queue a persistent map job. stage=concept creates a private master overview plus optional views and stops. views: overview,top_down,entrance,objective. All additional views are conditioned on the SAME pinned master; they are illustrations, not exact 3D renders. reference is an environment screenshot for a NEW layout. To add views to an existing concept, pass source_concept instead; the master is kept unchanged and is free. One credit per new image (full new 4-view set = 4); default is overview only. Poll generation_read with include_preview=true to see the views. After the user selects the concept, submit stage=build with the returned concept and a new idempotency key. Build costs 1 credit and is the default for existing callers; legacy concept_item accepts a Library image ID, while concept pins its exact version. wizard: style realistic|stylized|lowpoly|handpainted; world open_world|closed_arena|dungeon|linear|battle_map; terrain auto|plains|desert_canyon|forest|snow|island|volcanic; size small|medium|large; boundary fixed|infinite (open edge, finite terrain); spawn single|teams; teams 2|4; boss,safeZone,extraction booleans. sources selects catalog/workspace/community props. Build uses the common Scene Engine with independent terrain, regions, paths, zones and asset instances. Cancel/retry keeps successful images and billing. No automatic visual validation, promotion or Unreal import.', {
108
+ prompt: schema.string().min(3).max(4000), wizard: mapWizard, stage: schema.enum(['concept', 'build']).optional(),
109
+ reference: imageRef.optional().describe('Pinned source-environment screenshot for stage=concept only. Upload to Library and pin first.'),
110
+ source_concept: imageRef.optional().describe('Existing immutable master to add views to, stage=concept only. Mutually exclusive with reference; never regenerate this master.'),
111
+ 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.'),
112
+ concept: imageRef.optional().describe('Exact chosen concept revision for stage=build. Mutually exclusive with concept_item.'),
113
+ concept_item: identifier.optional().describe('Legacy Library concept image ID. Prefer an immutable concept reference.'),
114
+ 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.'),
115
+ sources: schema.array(schema.enum(['catalog', 'workspace', 'community'])).min(1).max(3).optional(),
116
+ seed: schema.number().int().min(-2147483647).max(2147483647).optional(), idempotency_key: schema.string().min(8).max(160).optional(),
117
+ }, false, (args, extra) => call('generation-jobs/map', args, 'POST', extra?.signal));
56
118
  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
119
  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
120
  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
121
  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
122
  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));
123
+ 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));
124
+ 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
125
  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
126
  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
127
  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
128
  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
129
  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
130
  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));
131
+ 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
132
  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
133
  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
134
  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 +136,7 @@ export function registerSceneTools(register, options, schema = z) {
72
136
  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
137
  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
138
  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 an actual rendered VFX image when available. Success does not mean visual approval or promotion.', { id: identifier, include_preview: schema.boolean().optional() }, true, (args, extra) => call(`generation-jobs/${args.id}`, args, 'GET', extra?.signal));
139
+ 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
140
  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
141
  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
142
  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));
@@ -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
  }