@waica/cli 0.5.0 → 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.
- package/README.md +42 -37
- package/dist/editor/assets/{cssMode-2_8FrcPt.js → cssMode-DfLN2qUF.js} +1 -1
- package/dist/editor/assets/{freemarker2-BgSLfjNS.js → freemarker2-BM-wWcXR.js} +1 -1
- package/dist/editor/assets/{handlebars-CZIkUhwZ.js → handlebars-C68SV5M9.js} +1 -1
- package/dist/editor/assets/{html-B66ooLhD.js → html-dQgLbIki.js} +1 -1
- package/dist/editor/assets/{htmlMode-BiIT--Mv.js → htmlMode-Dx91fUdd.js} +1 -1
- package/dist/editor/assets/{index-Br-M9Tdo.js → index-592ABIaz.js} +2787 -406
- package/dist/editor/assets/{index-q7IkLe-P.css → index-Cy5pmBhN.css} +1 -1
- package/dist/editor/assets/{javascript-BZrbbzWQ.js → javascript-BmsSq5ka.js} +1 -1
- package/dist/editor/assets/{jsonMode-CABkbZgt.js → jsonMode-CWoPtX2M.js} +1 -1
- package/dist/editor/assets/{liquid-CT7pyNYe.js → liquid-DwnJlNJZ.js} +1 -1
- package/dist/editor/assets/{mdx-DW7MPqdm.js → mdx-DaLkvlR1.js} +1 -1
- package/dist/editor/assets/{python-CgBwEaNf.js → python-Cs5X5zFp.js} +1 -1
- package/dist/editor/assets/{razor-Dvggr_Q1.js → razor-PTwWoRk8.js} +1 -1
- package/dist/editor/assets/{tsMode-OfEpAJE7.js → tsMode-BqUQdgsR.js} +1 -1
- package/dist/editor/assets/{typescript-BQbhlbBK.js → typescript-BsJ7r2RJ.js} +1 -1
- package/dist/editor/assets/{xml-5qToUR1k.js → xml-DyyoPey4.js} +1 -1
- package/dist/editor/assets/{yaml-CFM1jPNu.js → yaml-Cjqtlp29.js} +1 -1
- package/dist/editor/index.html +2 -2
- package/dist/mcp/introspection.js +2 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +3 -3
- package/dist/mcp/node_modules/@waica/behaviors/dist/health.d.ts +4 -3
- package/dist/mcp/node_modules/@waica/behaviors/dist/health.js +12 -15
- package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.js +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
- package/dist/mcp/node_modules/@waica/engine/README.md +85 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.d.ts +39 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.js +157 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +5 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component.js +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/game.d.ts +7 -0
- package/dist/mcp/node_modules/@waica/engine/dist/game.js +79 -1
- package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +7 -1
- package/dist/mcp/node_modules/@waica/engine/dist/index.js +3 -0
- package/dist/mcp/node_modules/@waica/engine/dist/input.d.ts +10 -0
- package/dist/mcp/node_modules/@waica/engine/dist/input.js +40 -2
- package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.d.ts +69 -0
- package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.js +105 -0
- package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.d.ts +71 -0
- package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.js +279 -0
- package/dist/mcp/node_modules/@waica/engine/package.json +3 -2
- package/dist/mcp/project-component-loader.js +506 -240
- package/dist/mcp/project-component-runner.js +237 -0
- package/dist/mcp/runtime-browser.js +336 -0
- package/dist/mcp/runtime-dev-server.js +273 -0
- package/dist/mcp/runtime-preflight.js +249 -0
- package/dist/mcp/runtime-service.js +8 -0
- package/dist/mcp/runtime-session-manager.js +265 -0
- package/dist/mcp/scaffolds.js +126 -6
- package/dist/mcp/server.js +330 -9
- package/dist/mcp/stdio.js +0 -25
- package/dist/mcp/template/src/main.ts +6 -1
- package/dist/mcp/validation.js +77 -6
- package/package.json +2 -1
- package/dist/mcp/native-import.cjs +0 -3
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/mcp/stdio.js
CHANGED
|
@@ -1,36 +1,11 @@
|
|
|
1
1
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
2
2
|
import { prepareWorkspaceRuntime } from './workspace-runtime.js';
|
|
3
|
-
/**
|
|
4
|
-
* Logs to stderr (never stdout — that's the JSON-RPC transport) instead of
|
|
5
|
-
* letting the failure reach Node's default handling. Exported so the logging
|
|
6
|
-
* itself is unit-testable without touching the global process listeners.
|
|
7
|
-
*/
|
|
8
|
-
export function reportProjectCodeFailure(kind, detail) {
|
|
9
|
-
console.error(`waica-mcp: ${kind} — likely async work scheduled by a project module executed during ` +
|
|
10
|
-
'validate_project (e.g. a floating fetch() or a throwing setTimeout callback). ' +
|
|
11
|
-
'The server keeps running:', detail);
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* validate_project runs project-owned components/roles/states in-process
|
|
15
|
-
* (project-component-loader.ts). That code can schedule async work that
|
|
16
|
-
* rejects or throws *after* module evaluation already returned — Node's
|
|
17
|
-
* default handling for that is to crash the whole process, which would take
|
|
18
|
-
* down this long-lived stdio server and every other in-flight tool call for
|
|
19
|
-
* one module's sloppy code. A subprocess sandbox for project code is a
|
|
20
|
-
* larger, separately tracked follow-up; this keeps the blast radius to a
|
|
21
|
-
* logged line for now.
|
|
22
|
-
*/
|
|
23
|
-
function installProjectCodeGuards() {
|
|
24
|
-
process.on('unhandledRejection', (reason) => reportProjectCodeFailure('unhandledRejection', reason));
|
|
25
|
-
process.on('uncaughtException', (error) => reportProjectCodeFailure('uncaughtException', error));
|
|
26
|
-
}
|
|
27
3
|
/**
|
|
28
4
|
* Connects the MCP server to stdio. Kept separate from the bin wrapper so the
|
|
29
5
|
* waica CLI can start the same server in-process from its bundled copy without
|
|
30
6
|
* importing a module that runs on load.
|
|
31
7
|
*/
|
|
32
8
|
export async function startStdioServer() {
|
|
33
|
-
installProjectCodeGuards();
|
|
34
9
|
await prepareWorkspaceRuntime();
|
|
35
10
|
const { createWaicaMcpServer } = await import('./server.js');
|
|
36
11
|
const server = createWaicaMcpServer();
|
|
@@ -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([
|
|
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.
|
package/dist/mcp/validation.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { resolveComponentUpdateSchedule, } from '@waica/engine';
|
|
1
2
|
import { readFile } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { discoverArchetypes, pickArchetype } from './archetypes.js';
|
|
4
5
|
import { classDefaults, objectRecord } from './component-metadata.js';
|
|
5
6
|
import { PackageResolver, mixedSourceWarnings, provenanceRows, } from './package-resolver.js';
|
|
6
|
-
import { loadProjectComponents } from './project-component-loader.js';
|
|
7
|
+
import { loadProjectComponents, } from './project-component-loader.js';
|
|
7
8
|
import { directFiles, requireWaicaProject } from './project-path.js';
|
|
8
9
|
function add(context, severity, code, message, file, ref) {
|
|
9
10
|
context.findings.push({ severity, code, message, file, ...(ref ? { ref } : {}) });
|
|
@@ -49,7 +50,7 @@ function componentList(value) {
|
|
|
49
50
|
}
|
|
50
51
|
function classMetadata(Class) {
|
|
51
52
|
// A throwing constructor has no observable defaults, matching list_components.
|
|
52
|
-
return { params: Class.params ?? {}, defaults: classDefaults(Class) };
|
|
53
|
+
return { Class, params: Class.params ?? {}, defaults: classDefaults(Class) };
|
|
53
54
|
}
|
|
54
55
|
/** Clip names declared by the sibling AnimatedSprite, or undefined when there is none (no animation contract to check). */
|
|
55
56
|
function siblingClips(siblings) {
|
|
@@ -171,7 +172,7 @@ function validateStateMachines(components, file, ref, context) {
|
|
|
171
172
|
}
|
|
172
173
|
if (typeof on === 'string' && on.startsWith('input:')) {
|
|
173
174
|
const action = on.slice('input:'.length);
|
|
174
|
-
if (!context.bindings
|
|
175
|
+
if (!isBoundAction(context.bindings, action)) {
|
|
175
176
|
add(context, 'warning', 'input-action-unbound', `Input action "${action}" has no bindings.`, file, action);
|
|
176
177
|
}
|
|
177
178
|
}
|
|
@@ -199,12 +200,63 @@ function resolvedEntityComponents(entity, prefab) {
|
|
|
199
200
|
}));
|
|
200
201
|
return [...inherited, ...componentList(entity.components)];
|
|
201
202
|
}
|
|
203
|
+
function componentUpdateIssueKey(issue) {
|
|
204
|
+
switch (issue.code) {
|
|
205
|
+
case 'duplicate-component':
|
|
206
|
+
return `${issue.code}:${issue.componentName}:${issue.count}`;
|
|
207
|
+
case 'invalid-update-constraint':
|
|
208
|
+
return `${issue.code}:${issue.reason}:${issue.declarer}:${issue.target ?? ''}`;
|
|
209
|
+
case 'component-update-cycle':
|
|
210
|
+
return `${issue.code}:${issue.componentNames.join('\0')}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function componentUpdateIssues(components, context) {
|
|
214
|
+
const result = resolveComponentUpdateSchedule(components.map((component) => component.type), context.componentRegistry);
|
|
215
|
+
return result.ok ? [] : [...result.issues];
|
|
216
|
+
}
|
|
217
|
+
function addComponentUpdateFinding(issue, file, ref, context) {
|
|
218
|
+
let findingFile = file;
|
|
219
|
+
let findingRef = ref;
|
|
220
|
+
if (issue.code === 'invalid-update-constraint') {
|
|
221
|
+
const source = context.componentMetadata.get(issue.declarer)?.sourceFile;
|
|
222
|
+
if (source) {
|
|
223
|
+
const key = `${source}:${componentUpdateIssueKey(issue)}`;
|
|
224
|
+
if (context.reportedClassConstraints.has(key))
|
|
225
|
+
return;
|
|
226
|
+
context.reportedClassConstraints.add(key);
|
|
227
|
+
findingFile = source;
|
|
228
|
+
findingRef = issue.declarer;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
add(context, 'error', issue.code, issue.cause, findingFile, findingRef);
|
|
232
|
+
}
|
|
233
|
+
function validateComponentUpdateSchedule(components, file, ref, context, inheritedIssues = new Set()) {
|
|
234
|
+
for (const issue of componentUpdateIssues(components, context)) {
|
|
235
|
+
if (inheritedIssues.has(componentUpdateIssueKey(issue)))
|
|
236
|
+
continue;
|
|
237
|
+
addComponentUpdateFinding(issue, file, ref, context);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function validateComponentClassUpdateContracts(context) {
|
|
241
|
+
for (const componentName of Object.keys(context.componentRegistry).sort()) {
|
|
242
|
+
const metadata = context.componentMetadata.get(componentName);
|
|
243
|
+
const result = resolveComponentUpdateSchedule([componentName], context.componentRegistry);
|
|
244
|
+
if (result.ok)
|
|
245
|
+
continue;
|
|
246
|
+
for (const issue of result.issues) {
|
|
247
|
+
if (issue.code !== 'invalid-update-constraint')
|
|
248
|
+
continue;
|
|
249
|
+
addComponentUpdateFinding(issue, metadata?.sourceFile ?? 'package.json', componentName, context);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
202
253
|
function validatePrefab(prefab, file, ref, context) {
|
|
203
254
|
const components = componentList(prefab.components);
|
|
204
255
|
for (const component of components)
|
|
205
256
|
checkComponent(component, file, ref, context);
|
|
206
257
|
validateParamReferences(components.map((component) => ({ component })), components, file, context);
|
|
207
258
|
validateStateMachines(components, file, ref, context);
|
|
259
|
+
validateComponentUpdateSchedule(components, file, ref, context);
|
|
208
260
|
}
|
|
209
261
|
function validateScene(scene, file, prefabs, uiNames, context) {
|
|
210
262
|
const rawEntities = Array.isArray(scene.entities) ? scene.entities : [];
|
|
@@ -305,6 +357,13 @@ function validateScene(scene, file, prefabs, uiNames, context) {
|
|
|
305
357
|
if (changesStateBehavior) {
|
|
306
358
|
validateStateMachines(effectiveComponents(), file, entityRef, context);
|
|
307
359
|
}
|
|
360
|
+
// A plain prefab instance inherits the prefab-level result already emitted
|
|
361
|
+
// above. Inline components create a new effective composition; report only
|
|
362
|
+
// the issues they introduce, not every inherited issue again.
|
|
363
|
+
if (inline.length > 0) {
|
|
364
|
+
const inheritedIssues = new Set(componentUpdateIssues(componentList(prefab?.components), context).map(componentUpdateIssueKey));
|
|
365
|
+
validateComponentUpdateSchedule(effectiveComponents(), file, entityRef, context, inheritedIssues);
|
|
366
|
+
}
|
|
308
367
|
}
|
|
309
368
|
}
|
|
310
369
|
function statBindings(html) {
|
|
@@ -321,7 +380,8 @@ function statBindings(html) {
|
|
|
321
380
|
}
|
|
322
381
|
return names;
|
|
323
382
|
}
|
|
324
|
-
export async function validateProject(projectPath) {
|
|
383
|
+
export async function validateProject(projectPath, options = {}) {
|
|
384
|
+
options.signal?.throwIfAborted();
|
|
325
385
|
const check = await requireWaicaProject(projectPath);
|
|
326
386
|
const findings = [];
|
|
327
387
|
const fixedPaths = [
|
|
@@ -350,7 +410,9 @@ export async function validateProject(projectPath) {
|
|
|
350
410
|
discoverArchetypes(projectPath, resolver, discoveryWarnings, activeId ? [activeId] : []),
|
|
351
411
|
projectComponentCandidates(projectPath),
|
|
352
412
|
projectRoleStateSources(projectPath),
|
|
353
|
-
|
|
413
|
+
options.componentLoader
|
|
414
|
+
? options.componentLoader.load(projectPath, resolver, { signal: options.signal })
|
|
415
|
+
: loadProjectComponents(projectPath, resolver, { signal: options.signal }),
|
|
354
416
|
]);
|
|
355
417
|
for (const failure of loadedProjectComponents.failures) {
|
|
356
418
|
add({ findings },
|
|
@@ -376,14 +438,20 @@ export async function validateProject(projectPath) {
|
|
|
376
438
|
}
|
|
377
439
|
const stateFiles = new Set((await directFiles(path.join(projectPath, 'src/states'), '.ts')).map((file) => file.slice(0, -'.ts'.length)));
|
|
378
440
|
const declaredStats = new Set(Object.keys(objectRecord(objectRecord(fixed.get('src/stats.json')).stats)));
|
|
379
|
-
const
|
|
441
|
+
const componentRegistry = {
|
|
442
|
+
...manifest.registry.components,
|
|
443
|
+
};
|
|
444
|
+
const componentMetadata = new Map(Object.entries(componentRegistry).map(([name, Class]) => [
|
|
380
445
|
name,
|
|
381
446
|
classMetadata(Class),
|
|
382
447
|
]));
|
|
383
448
|
for (const [name, description] of Object.entries(loadedProjectComponents.components)) {
|
|
449
|
+
componentRegistry[name] = description.Class;
|
|
384
450
|
componentMetadata.set(name, {
|
|
451
|
+
Class: description.Class,
|
|
385
452
|
params: description.params,
|
|
386
453
|
defaults: description.defaults,
|
|
454
|
+
sourceFile: description.file,
|
|
387
455
|
});
|
|
388
456
|
projectComponents.add(name);
|
|
389
457
|
}
|
|
@@ -414,12 +482,15 @@ export async function validateProject(projectPath) {
|
|
|
414
482
|
knownComponents: new Set(Object.keys(manifest.registry.components)),
|
|
415
483
|
projectComponents,
|
|
416
484
|
componentMetadata,
|
|
485
|
+
componentRegistry,
|
|
486
|
+
reportedClassConstraints: new Set(),
|
|
417
487
|
prefabRefs: new Set(prefabs.keys()),
|
|
418
488
|
declaredStats,
|
|
419
489
|
stateFiles,
|
|
420
490
|
roleStateSources,
|
|
421
491
|
bindings,
|
|
422
492
|
};
|
|
493
|
+
validateComponentClassUpdateContracts(context);
|
|
423
494
|
for (const { prefab, relative, ref } of prefabFiles) {
|
|
424
495
|
validatePrefab(prefab, relative, ref, context);
|
|
425
496
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waica/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Waica in one command — `npx @waica/cli` opens the editor, `waica mcp` serves the MCP server",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
30
|
+
"playwright-core": "^1.62.1",
|
|
30
31
|
"three": "^0.185.1"
|
|
31
32
|
}
|
|
32
33
|
}
|