@waica/cli 0.4.2 → 0.6.0

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.
Files changed (85) hide show
  1. package/README.md +42 -37
  2. package/dist/editor/assets/{cssMode-BO8FrG9g.js → cssMode-DfLN2qUF.js} +1 -1
  3. package/dist/editor/assets/{freemarker2-C6rCz24J.js → freemarker2-BM-wWcXR.js} +1 -1
  4. package/dist/editor/assets/{handlebars-MZdu9Qw_.js → handlebars-C68SV5M9.js} +1 -1
  5. package/dist/editor/assets/{html-BrjX1TJ1.js → html-dQgLbIki.js} +1 -1
  6. package/dist/editor/assets/{htmlMode-cEvpMwiW.js → htmlMode-Dx91fUdd.js} +1 -1
  7. package/dist/editor/assets/{index-eTEBznqW.js → index-592ABIaz.js} +7154 -3186
  8. package/dist/editor/assets/{index-Btzc6zxr.css → index-Cy5pmBhN.css} +1 -1
  9. package/dist/editor/assets/{javascript-DHUgp9_L.js → javascript-BmsSq5ka.js} +1 -1
  10. package/dist/editor/assets/{jsonMode-D0PmIuaM.js → jsonMode-CWoPtX2M.js} +1 -1
  11. package/dist/editor/assets/{liquid-UM67NCct.js → liquid-DwnJlNJZ.js} +1 -1
  12. package/dist/editor/assets/{mdx-D3xXLqrP.js → mdx-DaLkvlR1.js} +1 -1
  13. package/dist/editor/assets/{python-BhLrcAva.js → python-Cs5X5zFp.js} +1 -1
  14. package/dist/editor/assets/{razor-DcG373mQ.js → razor-PTwWoRk8.js} +1 -1
  15. package/dist/editor/assets/{tsMode-ByTYaf45.js → tsMode-BqUQdgsR.js} +1 -1
  16. package/dist/editor/assets/{typescript-DfJlvcTN.js → typescript-BsJ7r2RJ.js} +1 -1
  17. package/dist/editor/assets/{xml-22dL0oCq.js → xml-DyyoPey4.js} +1 -1
  18. package/dist/editor/assets/{yaml-mgCu6yV6.js → yaml-Cjqtlp29.js} +1 -1
  19. package/dist/editor/index.html +2 -2
  20. package/dist/mcp/component-metadata.js +23 -0
  21. package/dist/mcp/introspection.js +4 -14
  22. package/dist/mcp/node_modules/@waica/archetype-platformer/dist/prefabs.js +7 -1
  23. package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry-data.js +3 -1
  24. package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +3 -3
  25. package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.d.ts +1 -0
  26. package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.js +1 -0
  27. package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.d.ts +1 -0
  28. package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.js +1 -1
  29. package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.d.ts +42 -1
  30. package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.js +49 -3
  31. package/dist/mcp/node_modules/@waica/behaviors/dist/health.d.ts +82 -0
  32. package/dist/mcp/node_modules/@waica/behaviors/dist/health.js +147 -0
  33. package/dist/mcp/node_modules/@waica/behaviors/dist/index.d.ts +2 -0
  34. package/dist/mcp/node_modules/@waica/behaviors/dist/index.js +2 -0
  35. package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.d.ts +1 -0
  36. package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.js +1 -0
  37. package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.d.ts +30 -0
  38. package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.js +38 -0
  39. package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.d.ts +1 -0
  40. package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.js +1 -0
  41. package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.d.ts +1 -0
  42. package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.js +10 -0
  43. package/dist/mcp/node_modules/@waica/behaviors/dist/player-states.js +26 -0
  44. package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.d.ts +5 -13
  45. package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.js +5 -11
  46. package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
  47. package/dist/mcp/node_modules/@waica/engine/README.md +85 -0
  48. package/dist/mcp/node_modules/@waica/engine/dist/authoring-defaults.d.ts +11 -0
  49. package/dist/mcp/node_modules/@waica/engine/dist/authoring-defaults.js +51 -0
  50. package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.d.ts +39 -0
  51. package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.js +157 -0
  52. package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +15 -1
  53. package/dist/mcp/node_modules/@waica/engine/dist/component.js +2 -0
  54. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +2 -0
  55. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +11 -0
  56. package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.d.ts +1 -0
  57. package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.js +1 -0
  58. package/dist/mcp/node_modules/@waica/engine/dist/game.d.ts +7 -0
  59. package/dist/mcp/node_modules/@waica/engine/dist/game.js +79 -1
  60. package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +8 -1
  61. package/dist/mcp/node_modules/@waica/engine/dist/index.js +4 -0
  62. package/dist/mcp/node_modules/@waica/engine/dist/input.d.ts +10 -0
  63. package/dist/mcp/node_modules/@waica/engine/dist/input.js +40 -2
  64. package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.d.ts +69 -0
  65. package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.js +105 -0
  66. package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.d.ts +71 -0
  67. package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.js +279 -0
  68. package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.d.ts +2 -1
  69. package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.js +7 -1
  70. package/dist/mcp/node_modules/@waica/engine/package.json +3 -2
  71. package/dist/mcp/package-resolver.js +10 -2
  72. package/dist/mcp/project-component-loader.js +553 -0
  73. package/dist/mcp/project-component-runner.js +237 -0
  74. package/dist/mcp/runtime-browser.js +336 -0
  75. package/dist/mcp/runtime-dev-server.js +273 -0
  76. package/dist/mcp/runtime-preflight.js +249 -0
  77. package/dist/mcp/runtime-service.js +8 -0
  78. package/dist/mcp/runtime-session-manager.js +265 -0
  79. package/dist/mcp/scaffolds.js +126 -6
  80. package/dist/mcp/server.js +330 -9
  81. package/dist/mcp/template/src/characters/player.character.json +29 -2
  82. package/dist/mcp/template/src/characters/slime.character.json +6 -0
  83. package/dist/mcp/template/src/main.ts +6 -1
  84. package/dist/mcp/validation.js +240 -24
  85. package/package.json +3 -2
@@ -6,7 +6,10 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
6
6
  import { createProject } from './create-project.js';
7
7
  import { describeArchetype, listComponents, projectSummary, } from './introspection.js';
8
8
  import { ABSOLUTE_PATH_MESSAGE, WaicaToolError, assertAbsoluteProjectPath, requireWaicaProject, } from './project-path.js';
9
- import { scaffoldComponent, scaffoldRole, scaffoldState, scaffoldUi, } from './scaffolds.js';
9
+ import { scaffoldComponent, scaffoldPrefab, scaffoldRole, scaffoldState, scaffoldUi, } from './scaffolds.js';
10
+ import { RuntimeToolError, } from './runtime-service.js';
11
+ import { createDefaultRuntimeSessionManager } from './runtime-session-manager.js';
12
+ import { ProjectComponentLoader } from './project-component-loader.js';
10
13
  import { validateProject } from './validation.js';
11
14
  const PROJECT_PATH = {
12
15
  type: 'string',
@@ -69,6 +72,29 @@ export const TOOLS = [
69
72
  inputSchema: schema({ name: { type: 'string', minLength: 1 } }, ['name']),
70
73
  annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
71
74
  },
75
+ {
76
+ name: 'scaffold_prefab',
77
+ description: 'Create the editor-compatible starter prefab for a character, object or tile.',
78
+ inputSchema: schema({
79
+ name: { type: 'string', minLength: 1, description: 'Prefab file name, without the type suffix.' },
80
+ type: {
81
+ type: 'string',
82
+ enum: ['character', 'object', 'tile'],
83
+ description: 'Prefab category; it decides the directory and the file suffix.',
84
+ },
85
+ role: {
86
+ type: 'string',
87
+ minLength: 1,
88
+ description: 'Character role from the active archetype (characters only); defaults to player.',
89
+ },
90
+ identity: {
91
+ type: 'string',
92
+ enum: ['player', 'enemy', 'npc', 'custom'],
93
+ description: 'What the character is to the game (characters only); adds its starter components.',
94
+ },
95
+ }, ['name', 'type']),
96
+ annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
97
+ },
72
98
  {
73
99
  name: 'scaffold_role',
74
100
  description: 'Create the editor-compatible starter for a custom character role.',
@@ -95,7 +121,92 @@ export const TOOLS = [
95
121
  inputSchema: schema({ name: { type: 'string', minLength: 1 } }, ['name']),
96
122
  annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
97
123
  },
124
+ {
125
+ name: 'start_project',
126
+ description: 'Start or reuse a browser-backed Run Session for a trusted Waica Project.',
127
+ inputSchema: schema({
128
+ browser_executable_path: { type: 'string', minLength: 1 },
129
+ headless: { type: 'boolean', default: true },
130
+ viewport: {
131
+ type: 'object',
132
+ properties: {
133
+ width: { type: 'integer', minimum: 1 },
134
+ height: { type: 'integer', minimum: 1 },
135
+ },
136
+ required: ['width', 'height'],
137
+ additionalProperties: false,
138
+ },
139
+ timeout_ms: { type: 'integer', minimum: 1_000, maximum: 120_000, default: 30_000 },
140
+ }),
141
+ annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
142
+ },
143
+ {
144
+ name: 'stop_project',
145
+ description: 'Stop a Project Run Session and all browser and process resources it owns.',
146
+ inputSchema: schema({}),
147
+ annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
148
+ },
149
+ {
150
+ name: 'inspect_runtime',
151
+ description: 'Read a filtered Runtime Snapshot from a running Project.',
152
+ inputSchema: schema({
153
+ entity_ids: { type: 'array', items: { type: 'string' } },
154
+ entity_names: { type: 'array', items: { type: 'string' } },
155
+ component_types: { type: 'array', items: { type: 'string' } },
156
+ }),
157
+ annotations: { readOnlyHint: true, openWorldHint: false },
158
+ },
159
+ {
160
+ name: 'control_runtime',
161
+ description: 'Inject a semantic action or change deterministic frame control for a Run Session.',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: {
165
+ project_path: PROJECT_PATH,
166
+ operation: {
167
+ type: 'string',
168
+ enum: ['press', 'hold', 'release', 'pause', 'resume', 'step'],
169
+ },
170
+ action: { type: 'string', minLength: 1 },
171
+ dt: { type: 'number', exclusiveMinimum: 0, maximum: 0.1 },
172
+ frames: { type: 'integer', minimum: 1, maximum: 600 },
173
+ },
174
+ required: ['project_path', 'operation'],
175
+ additionalProperties: false,
176
+ oneOf: [
177
+ {
178
+ properties: { operation: { enum: ['press', 'hold', 'release'] } },
179
+ required: ['action'],
180
+ not: { anyOf: [{ required: ['dt'] }, { required: ['frames'] }] },
181
+ },
182
+ {
183
+ properties: { operation: { enum: ['pause', 'resume'] } },
184
+ not: {
185
+ anyOf: [{ required: ['action'] }, { required: ['dt'] }, { required: ['frames'] }],
186
+ },
187
+ },
188
+ {
189
+ properties: { operation: { const: 'step' } },
190
+ not: { anyOf: [{ required: ['action'] }] },
191
+ },
192
+ ],
193
+ },
194
+ annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: false },
195
+ },
196
+ {
197
+ name: 'capture_screenshot',
198
+ description: 'Capture the composited Waica Game surface from a running Project.',
199
+ inputSchema: schema({}),
200
+ annotations: { readOnlyHint: true, openWorldHint: false },
201
+ },
98
202
  ];
203
+ const RUNTIME_TOOL_NAMES = new Set([
204
+ 'start_project',
205
+ 'stop_project',
206
+ 'inspect_runtime',
207
+ 'control_runtime',
208
+ 'capture_screenshot',
209
+ ]);
99
210
  function requiredString(args, name, projectPath = '') {
100
211
  const value = args[name];
101
212
  if (typeof value === 'string' && value.length > 0)
@@ -106,11 +217,131 @@ function requiredString(args, name, projectPath = '') {
106
217
  projectPath,
107
218
  });
108
219
  }
109
- async function execute(name, args) {
220
+ function invalidRuntimeInput(name, projectPath, message) {
221
+ throw new RuntimeToolError({
222
+ code: 'runtime-operation-failed',
223
+ stage: name === 'control_runtime' ? 'control' : 'project',
224
+ message,
225
+ projectPath,
226
+ });
227
+ }
228
+ function assertOnlyRuntimeFields(name, args, allowed, projectPath) {
229
+ const extras = Object.keys(args).filter((key) => !allowed.includes(key));
230
+ if (extras.length > 0) {
231
+ invalidRuntimeInput(name, projectPath, `Unexpected properties: ${extras.sort().join(', ')}.`);
232
+ }
233
+ }
234
+ function assertStringArray(name, args, field, projectPath) {
235
+ const value = args[field];
236
+ if (value !== undefined && (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string'))) {
237
+ invalidRuntimeInput(name, projectPath, `${field} must be an array of strings.`);
238
+ }
239
+ }
240
+ function validateRuntimeArguments(name, args, projectPath) {
241
+ switch (name) {
242
+ case 'start_project': {
243
+ assertOnlyRuntimeFields(name, args, ['project_path', 'browser_executable_path', 'headless', 'viewport', 'timeout_ms'], projectPath);
244
+ if (args.browser_executable_path !== undefined &&
245
+ (typeof args.browser_executable_path !== 'string' || args.browser_executable_path.length === 0)) {
246
+ invalidRuntimeInput(name, projectPath, 'browser_executable_path must be a nonempty string.');
247
+ }
248
+ if (args.headless !== undefined && typeof args.headless !== 'boolean') {
249
+ invalidRuntimeInput(name, projectPath, 'headless must be a boolean.');
250
+ }
251
+ if (args.timeout_ms !== undefined) {
252
+ const timeout = args.timeout_ms;
253
+ if (typeof timeout !== 'number' || !Number.isInteger(timeout) || timeout < 1_000 || timeout > 120_000) {
254
+ invalidRuntimeInput(name, projectPath, 'timeout_ms must be an integer from 1,000 through 120,000.');
255
+ }
256
+ }
257
+ if (args.viewport !== undefined) {
258
+ if (!args.viewport || typeof args.viewport !== 'object' || Array.isArray(args.viewport)) {
259
+ invalidRuntimeInput(name, projectPath, 'viewport must contain width and height.');
260
+ }
261
+ const viewport = args.viewport;
262
+ const keys = Object.keys(viewport);
263
+ if (keys.some((key) => key !== 'width' && key !== 'height') ||
264
+ keys.length !== 2 ||
265
+ typeof viewport.width !== 'number' ||
266
+ typeof viewport.height !== 'number' ||
267
+ !Number.isInteger(viewport.width) ||
268
+ !Number.isInteger(viewport.height) ||
269
+ viewport.width <= 0 ||
270
+ viewport.height <= 0 ||
271
+ viewport.width * viewport.height > 1_000_000) {
272
+ invalidRuntimeInput(name, projectPath, 'viewport width and height must be positive integers totaling at most 1,000,000 pixels.');
273
+ }
274
+ }
275
+ return;
276
+ }
277
+ case 'stop_project':
278
+ case 'capture_screenshot':
279
+ assertOnlyRuntimeFields(name, args, ['project_path'], projectPath);
280
+ return;
281
+ case 'inspect_runtime':
282
+ assertOnlyRuntimeFields(name, args, ['project_path', 'entity_ids', 'entity_names', 'component_types'], projectPath);
283
+ assertStringArray(name, args, 'entity_ids', projectPath);
284
+ assertStringArray(name, args, 'entity_names', projectPath);
285
+ assertStringArray(name, args, 'component_types', projectPath);
286
+ return;
287
+ case 'control_runtime': {
288
+ assertOnlyRuntimeFields(name, args, ['project_path', 'operation', 'action', 'dt', 'frames'], projectPath);
289
+ const operation = args.operation;
290
+ if (!['press', 'hold', 'release', 'pause', 'resume', 'step'].includes(String(operation))) {
291
+ invalidRuntimeInput(name, projectPath, 'operation is not a supported runtime control operation.');
292
+ }
293
+ if (operation === 'press' || operation === 'hold' || operation === 'release') {
294
+ if (typeof args.action !== 'string' || args.action.length === 0) {
295
+ invalidRuntimeInput(name, projectPath, `${operation} requires a nonempty action.`);
296
+ }
297
+ if (args.dt !== undefined || args.frames !== undefined) {
298
+ invalidRuntimeInput(name, projectPath, `${operation} does not accept dt or frames.`);
299
+ }
300
+ }
301
+ else if (operation === 'pause' || operation === 'resume') {
302
+ if (args.action !== undefined || args.dt !== undefined || args.frames !== undefined) {
303
+ invalidRuntimeInput(name, projectPath, `${operation} accepts no additional fields.`);
304
+ }
305
+ }
306
+ else {
307
+ if (args.action !== undefined)
308
+ invalidRuntimeInput(name, projectPath, 'step does not accept action.');
309
+ if (args.dt !== undefined &&
310
+ (typeof args.dt !== 'number' || !Number.isFinite(args.dt) || args.dt <= 0 || args.dt > 0.1)) {
311
+ invalidRuntimeInput(name, projectPath, 'dt must be finite and greater than 0 and at most 0.1.');
312
+ }
313
+ if (args.frames !== undefined &&
314
+ (typeof args.frames !== 'number' || !Number.isInteger(args.frames) || args.frames < 1 || args.frames > 600)) {
315
+ invalidRuntimeInput(name, projectPath, 'frames must be an integer from 1 through 600.');
316
+ }
317
+ }
318
+ return;
319
+ }
320
+ }
321
+ }
322
+ async function execute(name, args, runtime, signal, componentLoader) {
323
+ const isRuntimeTool = RUNTIME_TOOL_NAMES.has(name);
324
+ const rawProjectPath = args.project_path;
325
+ if (isRuntimeTool && (typeof rawProjectPath !== 'string' || rawProjectPath.length === 0)) {
326
+ invalidRuntimeInput(name, '', 'project_path must be a nonempty absolute path.');
327
+ }
110
328
  const projectPath = requiredString(args, 'project_path');
111
329
  // Keep this at the dispatch boundary so every tool has byte-identical
112
330
  // stdio-cwd semantics, including create_project.
113
- assertAbsoluteProjectPath(projectPath);
331
+ if (isRuntimeTool) {
332
+ if (!path.isAbsolute(projectPath)) {
333
+ throw new RuntimeToolError({
334
+ code: 'runtime-prerequisite-missing',
335
+ stage: 'project',
336
+ message: ABSOLUTE_PATH_MESSAGE,
337
+ projectPath,
338
+ });
339
+ }
340
+ validateRuntimeArguments(name, args, projectPath);
341
+ }
342
+ else {
343
+ assertAbsoluteProjectPath(projectPath);
344
+ }
114
345
  switch (name) {
115
346
  case 'create_project': {
116
347
  const start = args.start === undefined ? 'demo' : requiredString(args, 'start', projectPath);
@@ -130,7 +361,7 @@ async function execute(name, args) {
130
361
  case 'project_summary':
131
362
  return projectSummary(projectPath);
132
363
  case 'validate_project':
133
- return validateProject(projectPath);
364
+ return validateProject(projectPath, { signal, componentLoader });
134
365
  case 'scaffold_component': {
135
366
  const check = await requireWaicaProject(projectPath);
136
367
  return {
@@ -140,6 +371,15 @@ async function execute(name, args) {
140
371
  warnings: [],
141
372
  };
142
373
  }
374
+ case 'scaffold_prefab': {
375
+ const check = await requireWaicaProject(projectPath);
376
+ return {
377
+ ...(await scaffoldPrefab(projectPath, requiredString(args, 'name', projectPath), requiredString(args, 'type', projectPath), args.role === undefined ? undefined : requiredString(args, 'role', projectPath), args.identity === undefined ? undefined : requiredString(args, 'identity', projectPath))),
378
+ notes: check.notes,
379
+ provenance: [],
380
+ warnings: [],
381
+ };
382
+ }
143
383
  case 'scaffold_role': {
144
384
  const check = await requireWaicaProject(projectPath);
145
385
  return {
@@ -167,6 +407,39 @@ async function execute(name, args) {
167
407
  warnings: [],
168
408
  };
169
409
  }
410
+ case 'start_project':
411
+ return runtime.start({
412
+ projectPath,
413
+ ...(typeof args.browser_executable_path === 'string'
414
+ ? { browserExecutablePath: args.browser_executable_path }
415
+ : {}),
416
+ ...(typeof args.headless === 'boolean' ? { headless: args.headless } : {}),
417
+ ...(args.viewport && typeof args.viewport === 'object'
418
+ ? { viewport: args.viewport }
419
+ : {}),
420
+ ...(typeof args.timeout_ms === 'number' ? { timeoutMs: args.timeout_ms } : {}),
421
+ });
422
+ case 'stop_project':
423
+ return runtime.stop(projectPath);
424
+ case 'inspect_runtime':
425
+ return runtime.inspect({
426
+ projectPath,
427
+ ...(Array.isArray(args.entity_ids) ? { entityIds: args.entity_ids } : {}),
428
+ ...(Array.isArray(args.entity_names) ? { entityNames: args.entity_names } : {}),
429
+ ...(Array.isArray(args.component_types)
430
+ ? { componentTypes: args.component_types }
431
+ : {}),
432
+ });
433
+ case 'control_runtime':
434
+ return runtime.control({
435
+ projectPath,
436
+ operation: requiredString(args, 'operation', projectPath),
437
+ ...(typeof args.action === 'string' ? { action: args.action } : {}),
438
+ ...(typeof args.dt === 'number' ? { dt: args.dt } : {}),
439
+ ...(typeof args.frames === 'number' ? { frames: args.frames } : {}),
440
+ });
441
+ case 'capture_screenshot':
442
+ return runtime.captureScreenshot(projectPath);
170
443
  default:
171
444
  throw new WaicaToolError({
172
445
  code: 'unknown-tool',
@@ -186,11 +459,39 @@ function result(payload, isError = false) {
186
459
  ...(isError ? { isError: true } : {}),
187
460
  };
188
461
  }
189
- function errorResult(error, args) {
462
+ function screenshotResult(screenshot) {
463
+ const metadata = jsonSafe(screenshot.metadata);
464
+ return {
465
+ content: [
466
+ { type: 'text', text: JSON.stringify(metadata, null, 2) },
467
+ { type: 'image', mimeType: 'image/png', data: screenshot.data },
468
+ ],
469
+ structuredContent: metadata,
470
+ };
471
+ }
472
+ function errorResult(error, args, toolName) {
190
473
  const projectPath = typeof args.project_path === 'string' ? args.project_path : '';
474
+ if (error instanceof RuntimeToolError) {
475
+ return result({ error: error.body }, true);
476
+ }
191
477
  if (error instanceof WaicaToolError) {
192
478
  return result({ error: error.body, provenance: [] }, true);
193
479
  }
480
+ if (RUNTIME_TOOL_NAMES.has(toolName)) {
481
+ const stage = toolName === 'control_runtime'
482
+ ? 'control'
483
+ : toolName === 'stop_project'
484
+ ? 'cleanup'
485
+ : 'game';
486
+ return result({
487
+ error: {
488
+ code: 'runtime-operation-failed',
489
+ stage,
490
+ message: error instanceof Error ? error.message : String(error),
491
+ projectPath,
492
+ },
493
+ }, true);
494
+ }
194
495
  return result({
195
496
  error: {
196
497
  code: 'tool-error',
@@ -224,7 +525,9 @@ function shippedVersion() {
224
525
  directory = parent;
225
526
  }
226
527
  }
227
- export function createWaicaMcpServer() {
528
+ export function createWaicaMcpServer(options = {}) {
529
+ const runtime = options.runtime ?? createDefaultRuntimeSessionManager();
530
+ const componentLoader = options.componentLoader ?? new ProjectComponentLoader();
228
531
  const server = new Server(
229
532
  // The name stays fixed: it identifies the server to the host, not the
230
533
  // package that happens to carry it.
@@ -233,15 +536,33 @@ export function createWaicaMcpServer() {
233
536
  instructions: 'Operate on user Waica projects through absolute project_path values. Keep editing JSON and TypeScript with the host file tools.',
234
537
  });
235
538
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
236
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
539
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
237
540
  const args = (request.params.arguments ?? {});
238
541
  try {
239
- return result(await execute(request.params.name, args));
542
+ const executed = await execute(request.params.name, args, runtime, extra.signal, componentLoader);
543
+ return request.params.name === 'capture_screenshot'
544
+ ? screenshotResult(executed)
545
+ : result(executed);
240
546
  }
241
547
  catch (error) {
242
- return errorResult(error, args);
548
+ if (extra.signal.aborted)
549
+ throw (extra.signal.reason ?? error);
550
+ return errorResult(error, args, request.params.name);
243
551
  }
244
552
  });
553
+ let cleanup;
554
+ const cleanupResources = () => {
555
+ cleanup ??= Promise.all([runtime.close(), componentLoader.close()]).then(() => undefined);
556
+ return cleanup;
557
+ };
558
+ server.onclose = () => {
559
+ void cleanupResources();
560
+ };
561
+ const closeProtocol = server.close.bind(server);
562
+ server.close = async () => {
563
+ await cleanupResources();
564
+ await closeProtocol();
565
+ };
245
566
  return server;
246
567
  }
247
568
  export { ABSOLUTE_PATH_MESSAGE };
@@ -112,6 +112,23 @@
112
112
  "to": "idle"
113
113
  }
114
114
  ]
115
+ },
116
+ "dead": {
117
+ "clip": "idle",
118
+ "transitions": [
119
+ {
120
+ "on": "timer:0.8",
121
+ "to": "idle"
122
+ }
123
+ ]
124
+ },
125
+ "*": {
126
+ "transitions": [
127
+ {
128
+ "on": "signal:death",
129
+ "to": "dead"
130
+ }
131
+ ]
115
132
  }
116
133
  }
117
134
  }
@@ -124,9 +141,19 @@
124
141
  }
125
142
  },
126
143
  {
127
- "type": "Respawnable",
144
+ "type": "Respawnable"
145
+ },
146
+ {
147
+ "type": "Health",
148
+ "props": {
149
+ "max": 3,
150
+ "invulnerability": 1
151
+ }
152
+ },
153
+ {
154
+ "type": "OutOfBounds",
128
155
  "props": {
129
- "killY": -12
156
+ "minY": -12
130
157
  }
131
158
  }
132
159
  ]
@@ -56,6 +56,12 @@
56
56
  "stompable": true,
57
57
  "bounce": 10
58
58
  }
59
+ },
60
+ {
61
+ "type": "Health",
62
+ "props": {
63
+ "max": 1
64
+ }
59
65
  }
60
66
  ]
61
67
  }
@@ -14,7 +14,12 @@ import settings from './game.json'
14
14
 
15
15
  // Your components, roles and state code extend the installed archetype
16
16
  // baseline when main() imports them.
17
- const projectCode = import.meta.glob(['./components/*.ts', './roles/*.ts', './states/*.ts'])
17
+ const projectCode = import.meta.glob([
18
+ './components/*.ts',
19
+ './roles/*.ts',
20
+ './states/*.ts',
21
+ '!**/*.test.ts',
22
+ ])
18
23
 
19
24
  // Your prefabs ARE these files (saved by the editor, or hand-edited — they're
20
25
  // just JSON). Nothing else: delete one and the game stops knowing about it.