@waica/cli 0.3.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/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/cli.js +184 -0
- package/dist/editor/assets/abap-08VXUWAP.js +1 -0
- package/dist/editor/assets/apex-BWPQTe0t.js +1 -0
- package/dist/editor/assets/azcli-Bc_sGQ0U.js +1 -0
- package/dist/editor/assets/bat-i0X4ZdIN.js +1 -0
- package/dist/editor/assets/bicep-B5-_aFwp.js +2 -0
- package/dist/editor/assets/cameligo-DMUM7wLl.js +1 -0
- package/dist/editor/assets/clojure-Cm7r79vr.js +1 -0
- package/dist/editor/assets/codicon-DCmgc-ay.ttf +0 -0
- package/dist/editor/assets/coffee-Ba7i2nA0.js +1 -0
- package/dist/editor/assets/cpp-C7h46wYY.js +1 -0
- package/dist/editor/assets/csharp-BKxtCVv1.js +1 -0
- package/dist/editor/assets/csp-bTuwJoIa.js +1 -0
- package/dist/editor/assets/css-DIMkf-bt.js +3 -0
- package/dist/editor/assets/css.worker-Fwwr_HdT.js +88 -0
- package/dist/editor/assets/cssMode-CQE-WdwL.js +4 -0
- package/dist/editor/assets/cypher-CVaqCwHa.js +1 -0
- package/dist/editor/assets/dart-onAF5SnQ.js +1 -0
- package/dist/editor/assets/dockerfile-DZFCIeNp.js +1 -0
- package/dist/editor/assets/ecl-D05T4iGw.js +1 -0
- package/dist/editor/assets/editor.worker-DsBDrWS5.js +12 -0
- package/dist/editor/assets/elixir-6RTg0lbw.js +1 -0
- package/dist/editor/assets/flow9-C5_-GSwl.js +1 -0
- package/dist/editor/assets/freemarker2-CYgdVabn.js +3 -0
- package/dist/editor/assets/fsharp-C8Ef5oNN.js +1 -0
- package/dist/editor/assets/go-C-y9NEjX.js +1 -0
- package/dist/editor/assets/graphql-fmXr3nnJ.js +1 -0
- package/dist/editor/assets/handlebars-DdeORCJD.js +1 -0
- package/dist/editor/assets/hcl-CpzslTdj.js +1 -0
- package/dist/editor/assets/html-DTIAGV84.js +1 -0
- package/dist/editor/assets/html.worker-B3Ztmmz2.js +495 -0
- package/dist/editor/assets/htmlMode-CRV3iBih.js +4 -0
- package/dist/editor/assets/index-Btzc6zxr.css +1 -0
- package/dist/editor/assets/index-Dhkqk31v.js +11016 -0
- package/dist/editor/assets/ini-sBoK_t0W.js +1 -0
- package/dist/editor/assets/java-BEtHBSE6.js +1 -0
- package/dist/editor/assets/javascript-dxBW86cl.js +1 -0
- package/dist/editor/assets/json.worker-PWQxM7ZO.js +51 -0
- package/dist/editor/assets/jsonMode-DFA5-FFK.js +10 -0
- package/dist/editor/assets/julia-Bri6UV-V.js +1 -0
- package/dist/editor/assets/kotlin-B0qVs-E9.js +1 -0
- package/dist/editor/assets/less-B9JPFI3C.js +2 -0
- package/dist/editor/assets/lexon-CfSJPG6W.js +1 -0
- package/dist/editor/assets/liquid-CScWaXYV.js +1 -0
- package/dist/editor/assets/lua-CsQS60Ue.js +1 -0
- package/dist/editor/assets/m3-D-oSqn_W.js +1 -0
- package/dist/editor/assets/markdown-Cimd5fb3.js +1 -0
- package/dist/editor/assets/mdx-Bvp84SqA.js +1 -0
- package/dist/editor/assets/mips-CIPQ_RoX.js +1 -0
- package/dist/editor/assets/msdax-DauUninz.js +1 -0
- package/dist/editor/assets/mysql-SOo6toE5.js +1 -0
- package/dist/editor/assets/objective-c-FvmIjYaQ.js +1 -0
- package/dist/editor/assets/pascal-DrH0SRf2.js +1 -0
- package/dist/editor/assets/pascaligo-D-ptJ9y-.js +1 -0
- package/dist/editor/assets/perl-oz_6vUea.js +1 -0
- package/dist/editor/assets/pgsql-DTj74zXo.js +1 -0
- package/dist/editor/assets/php-nr791fC2.js +1 -0
- package/dist/editor/assets/pla-CopQ2nXW.js +1 -0
- package/dist/editor/assets/postiats-43DmfD33.js +1 -0
- package/dist/editor/assets/powerquery-D3hlyOfw.js +1 -0
- package/dist/editor/assets/powershell-DmHpPYUd.js +1 -0
- package/dist/editor/assets/protobuf-C531GsRP.js +2 -0
- package/dist/editor/assets/pug-Z5eAx3Zn.js +1 -0
- package/dist/editor/assets/python-CG-OtvFa.js +1 -0
- package/dist/editor/assets/qsharp-DkqhCAOL.js +1 -0
- package/dist/editor/assets/r-BwWrilGY.js +1 -0
- package/dist/editor/assets/razor-r-iUEZPj.js +1 -0
- package/dist/editor/assets/redis-ClamHrr6.js +1 -0
- package/dist/editor/assets/redshift-DT7zqm-g.js +1 -0
- package/dist/editor/assets/restructuredtext-BYgofb2h.js +1 -0
- package/dist/editor/assets/ruby-DezsRK8O.js +1 -0
- package/dist/editor/assets/rust-DdL9SqIa.js +1 -0
- package/dist/editor/assets/sb-CcwsVR0C.js +1 -0
- package/dist/editor/assets/scala-DHpiXF5c.js +1 -0
- package/dist/editor/assets/scheme-BeGwcela.js +1 -0
- package/dist/editor/assets/scss-gp-XZpBa.js +3 -0
- package/dist/editor/assets/shell-CC2rA5mh.js +1 -0
- package/dist/editor/assets/solidity-BEEn4gHE.js +1 -0
- package/dist/editor/assets/sophia-CRfGWb83.js +1 -0
- package/dist/editor/assets/sparql-D_Lu-MrJ.js +1 -0
- package/dist/editor/assets/sql-NEE52Syq.js +1 -0
- package/dist/editor/assets/st-DbInun42.js +1 -0
- package/dist/editor/assets/swift-Bxkupp3x.js +1 -0
- package/dist/editor/assets/systemverilog-Bz4Y3fRF.js +1 -0
- package/dist/editor/assets/tcl-DISqw1ZD.js +1 -0
- package/dist/editor/assets/ts.worker-0mEyHVru.js +51339 -0
- package/dist/editor/assets/tsMode-Bth1rJwM.js +11 -0
- package/dist/editor/assets/twig-De2hgUGE.js +1 -0
- package/dist/editor/assets/typescript-tNRuHsoe.js +1 -0
- package/dist/editor/assets/typespec-B8J7ngcE.js +1 -0
- package/dist/editor/assets/vb-DV3o63ZY.js +1 -0
- package/dist/editor/assets/wgsl-DpFanUEy.js +298 -0
- package/dist/editor/assets/xml-BCx9e6CK.js +1 -0
- package/dist/editor/assets/yaml-Bb-peGP_.js +1 -0
- package/dist/editor/index.html +14 -0
- package/dist/mcp/archetypes.js +103 -0
- package/dist/mcp/cli.js +6 -0
- package/dist/mcp/create-project.js +151 -0
- package/dist/mcp/introspection.js +201 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/assets/waica-coin.png +0 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/assets/waica-dog.png +0 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/assets/waica-slime.png +0 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/art.d.ts +10 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/art.js +13 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/bundle.d.ts +3 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/bundle.js +10 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/controls.d.ts +5 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/controls.js +12 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/index.d.ts +38 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/index.js +34 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/manifest.d.ts +15 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/manifest.js +26 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/prefabs.d.ts +7 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/prefabs.js +97 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry-data.d.ts +5 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry-data.js +45 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry.d.ts +6 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry.js +15 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/scene-default.d.ts +40 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/scene-default.js +93 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/ui.d.ts +10 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/ui.js +24 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +30 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.d.ts +59 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.js +127 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.d.ts +25 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.js +27 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.d.ts +29 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.js +42 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/index.d.ts +9 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/index.js +9 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.d.ts +16 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.js +19 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/npc.d.ts +4 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/npc.js +14 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.d.ts +43 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.js +68 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.d.ts +124 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.js +146 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/player-identity.d.ts +3 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/player-identity.js +5 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/player-states.d.ts +18 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/player-states.js +95 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.d.ts +23 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.js +27 -0
- package/dist/mcp/node_modules/@waica/behaviors/package.json +24 -0
- package/dist/mcp/node_modules/@waica/engine/dist/aabb.d.ts +2 -0
- package/dist/mcp/node_modules/@waica/engine/dist/aabb.js +4 -0
- package/dist/mcp/node_modules/@waica/engine/dist/animation/clip-player.d.ts +20 -0
- package/dist/mcp/node_modules/@waica/engine/dist/animation/clip-player.js +24 -0
- package/dist/mcp/node_modules/@waica/engine/dist/animation/contract.d.ts +21 -0
- package/dist/mcp/node_modules/@waica/engine/dist/animation/contract.js +23 -0
- package/dist/mcp/node_modules/@waica/engine/dist/animation/sheet.d.ts +65 -0
- package/dist/mcp/node_modules/@waica/engine/dist/animation/sheet.js +44 -0
- package/dist/mcp/node_modules/@waica/engine/dist/archetype.d.ts +37 -0
- package/dist/mcp/node_modules/@waica/engine/dist/archetype.js +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/camera.d.ts +71 -0
- package/dist/mcp/node_modules/@waica/engine/dist/camera.js +59 -0
- package/dist/mcp/node_modules/@waica/engine/dist/collision-shape.d.ts +26 -0
- package/dist/mcp/node_modules/@waica/engine/dist/collision-shape.js +136 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component-registry.d.ts +16 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component-registry.js +44 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +58 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component.js +11 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +80 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +210 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/dynamic-body.d.ts +68 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/dynamic-body.js +189 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/hitbox.d.ts +25 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/hitbox.js +21 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/solid.d.ts +32 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/solid.js +45 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.d.ts +51 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.js +112 -0
- package/dist/mcp/node_modules/@waica/engine/dist/entity.d.ts +22 -0
- package/dist/mcp/node_modules/@waica/engine/dist/entity.js +52 -0
- package/dist/mcp/node_modules/@waica/engine/dist/events.d.ts +8 -0
- package/dist/mcp/node_modules/@waica/engine/dist/events.js +20 -0
- package/dist/mcp/node_modules/@waica/engine/dist/game.d.ts +95 -0
- package/dist/mcp/node_modules/@waica/engine/dist/game.js +263 -0
- package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +39 -0
- package/dist/mcp/node_modules/@waica/engine/dist/index.js +26 -0
- package/dist/mcp/node_modules/@waica/engine/dist/input.d.ts +40 -0
- package/dist/mcp/node_modules/@waica/engine/dist/input.js +84 -0
- package/dist/mcp/node_modules/@waica/engine/dist/scene.d.ts +80 -0
- package/dist/mcp/node_modules/@waica/engine/dist/scene.js +93 -0
- package/dist/mcp/node_modules/@waica/engine/dist/solid-axis.d.ts +21 -0
- package/dist/mcp/node_modules/@waica/engine/dist/solid-axis.js +77 -0
- package/dist/mcp/node_modules/@waica/engine/dist/state/hooks.d.ts +90 -0
- package/dist/mcp/node_modules/@waica/engine/dist/state/hooks.js +81 -0
- package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.d.ts +83 -0
- package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.js +173 -0
- package/dist/mcp/node_modules/@waica/engine/dist/stats.d.ts +27 -0
- package/dist/mcp/node_modules/@waica/engine/dist/stats.js +54 -0
- package/dist/mcp/node_modules/@waica/engine/dist/ui.d.ts +47 -0
- package/dist/mcp/node_modules/@waica/engine/dist/ui.js +175 -0
- package/dist/mcp/node_modules/@waica/engine/package.json +25 -0
- package/dist/mcp/package-resolver.js +240 -0
- package/dist/mcp/project-path.js +86 -0
- package/dist/mcp/scaffolds.js +153 -0
- package/dist/mcp/server.js +217 -0
- package/dist/mcp/stdio.js +13 -0
- package/dist/mcp/template/README.md +22 -0
- package/dist/mcp/template/_gitignore +5 -0
- package/dist/mcp/template/index.html +25 -0
- package/dist/mcp/template/package.json.tpl +20 -0
- package/dist/mcp/template/public/waica.params.json +1 -0
- package/dist/mcp/template/src/art/waica-coin.png +0 -0
- package/dist/mcp/template/src/art/waica-dog.png +0 -0
- package/dist/mcp/template/src/art/waica-slime.png +0 -0
- package/dist/mcp/template/src/characters/player.character.json +133 -0
- package/dist/mcp/template/src/characters/slime.character.json +61 -0
- package/dist/mcp/template/src/controls.json +8 -0
- package/dist/mcp/template/src/game.json +10 -0
- package/dist/mcp/template/src/main.ts +105 -0
- package/dist/mcp/template/src/objects/coin.object.json +41 -0
- package/dist/mcp/template/src/scenes/main.scene.json +217 -0
- package/dist/mcp/template/src/stats.json +7 -0
- package/dist/mcp/template/src/tiles/block.tile.json +21 -0
- package/dist/mcp/template/src/tiles/decor.tile.json +14 -0
- package/dist/mcp/template/src/tiles/platform.tile.json +21 -0
- package/dist/mcp/template/src/ui/coin-counter.html +12 -0
- package/dist/mcp/template/tsconfig.json +15 -0
- package/dist/mcp/template/vite.config.ts +3 -0
- package/dist/mcp/validation.js +323 -0
- package/dist/mcp/workspace-loader.js +14 -0
- package/dist/mcp/workspace-runtime.js +109 -0
- package/dist/server.js +210 -0
- package/package.json +38 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { access, readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { workspacePackageRoot } from './workspace-runtime.js';
|
|
6
|
+
/** A declared package that is absent from both the project and MCP bundle. */
|
|
7
|
+
export class PackageUnavailableError extends Error {
|
|
8
|
+
packageName;
|
|
9
|
+
constructor(packageName) {
|
|
10
|
+
super(`Package ${packageName} is not installed and is not part of the MCP bundle.`);
|
|
11
|
+
this.packageName = packageName;
|
|
12
|
+
this.name = 'PackageUnavailableError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const BUNDLED_SPECIFIERS = new Set([
|
|
16
|
+
'@waica/engine',
|
|
17
|
+
'@waica/behaviors',
|
|
18
|
+
'@waica/archetype-platformer/manifest',
|
|
19
|
+
]);
|
|
20
|
+
const requireBundled = createRequire(import.meta.url);
|
|
21
|
+
const BUILT_ENTRY_BY_SPECIFIER = {
|
|
22
|
+
'@waica/engine': 'dist/index.js',
|
|
23
|
+
'@waica/behaviors': 'dist/index.js',
|
|
24
|
+
'@waica/archetype-platformer/manifest': 'dist/manifest.js',
|
|
25
|
+
};
|
|
26
|
+
function specifierFor(packageName, subpath) {
|
|
27
|
+
if (!subpath || subpath === '.')
|
|
28
|
+
return packageName;
|
|
29
|
+
return `${packageName}/${subpath.replace(/^\.\//, '')}`;
|
|
30
|
+
}
|
|
31
|
+
async function projectPackageJsonUsable(projectPath) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(await readFile(path.join(projectPath, 'package.json'), 'utf8'));
|
|
34
|
+
return !!parsed && typeof parsed === 'object' && !Array.isArray(parsed);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === 'ENOENT')
|
|
38
|
+
return true;
|
|
39
|
+
if (error instanceof SyntaxError)
|
|
40
|
+
return false;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function packageVersion(packageRoot) {
|
|
45
|
+
const manifest = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
46
|
+
if (typeof manifest.version !== 'string') {
|
|
47
|
+
throw new Error(`package at ${packageRoot} has no string version`);
|
|
48
|
+
}
|
|
49
|
+
return manifest.version;
|
|
50
|
+
}
|
|
51
|
+
async function packageRootFromEntry(entry, packageName) {
|
|
52
|
+
let current = path.dirname(entry);
|
|
53
|
+
while (true) {
|
|
54
|
+
try {
|
|
55
|
+
const manifest = JSON.parse(await readFile(path.join(current, 'package.json'), 'utf8'));
|
|
56
|
+
if (manifest.name === packageName)
|
|
57
|
+
return current;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.code !== 'ENOENT')
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const parent = path.dirname(current);
|
|
64
|
+
if (parent === current)
|
|
65
|
+
throw new Error(`could not locate package root for ${packageName}`);
|
|
66
|
+
current = parent;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function bundledPackageRoot(packageName, specifier) {
|
|
70
|
+
// In a built checkout the workspace copy is the bundle: the CLI's vendored
|
|
71
|
+
// directory is a copy of it, and preferring the original keeps a
|
|
72
|
+
// workspace-linked project recognisable as project-owned.
|
|
73
|
+
const workspaceRoot = await workspacePackageRoot(packageName);
|
|
74
|
+
if (workspaceRoot !== undefined)
|
|
75
|
+
return workspaceRoot;
|
|
76
|
+
return packageRootFromEntry(requireBundled.resolve(specifier), packageName);
|
|
77
|
+
}
|
|
78
|
+
async function installedPackageRoot(projectPath, projectRequire, packageName, specifier) {
|
|
79
|
+
// Follow Node's ancestor node_modules search so hoisted installs count. A
|
|
80
|
+
// physical candidate distinguishes an absent package from a present but
|
|
81
|
+
// unloadable one; a successful resolution must point back to that candidate.
|
|
82
|
+
for (let current = projectPath;; current = path.dirname(current)) {
|
|
83
|
+
const candidate = path.join(current, 'node_modules', ...packageName.split('/'));
|
|
84
|
+
if (await exists(path.join(candidate, 'package.json'))) {
|
|
85
|
+
let entry;
|
|
86
|
+
try {
|
|
87
|
+
entry = projectRequire.resolve(specifier);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return candidate;
|
|
91
|
+
}
|
|
92
|
+
const resolvedRoot = await packageRootFromEntry(entry, packageName);
|
|
93
|
+
if (!(await samePackage(candidate, resolvedRoot))) {
|
|
94
|
+
throw new Error(`${specifier} resolved outside its installed package root ${candidate}: ${resolvedRoot}`);
|
|
95
|
+
}
|
|
96
|
+
return candidate;
|
|
97
|
+
}
|
|
98
|
+
const parent = path.dirname(current);
|
|
99
|
+
if (parent === current)
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A built checkout and a published package both carry loadable dist files.
|
|
105
|
+
* Vitest runs buildless, so its literal-import fallback still consumes the
|
|
106
|
+
* workspace source through Vite without changing production resolution.
|
|
107
|
+
*/
|
|
108
|
+
async function loadBundledModule(specifier, packageRoot) {
|
|
109
|
+
const builtRelative = BUILT_ENTRY_BY_SPECIFIER[specifier];
|
|
110
|
+
if (builtRelative) {
|
|
111
|
+
const builtEntry = path.join(packageRoot, builtRelative);
|
|
112
|
+
if (await exists(builtEntry))
|
|
113
|
+
return import(pathToFileURL(builtEntry).href);
|
|
114
|
+
}
|
|
115
|
+
switch (specifier) {
|
|
116
|
+
case '@waica/engine':
|
|
117
|
+
return import('@waica/engine');
|
|
118
|
+
case '@waica/behaviors':
|
|
119
|
+
return import('@waica/behaviors');
|
|
120
|
+
case '@waica/archetype-platformer/manifest':
|
|
121
|
+
return import('@waica/archetype-platformer/manifest');
|
|
122
|
+
default:
|
|
123
|
+
throw new PackageUnavailableError(specifier);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function causeText(error) {
|
|
127
|
+
if (!(error instanceof Error))
|
|
128
|
+
return String(error);
|
|
129
|
+
const code = error.code;
|
|
130
|
+
return `${code ? `${code}: ` : ''}${error.message}`;
|
|
131
|
+
}
|
|
132
|
+
async function samePackage(a, b) {
|
|
133
|
+
try {
|
|
134
|
+
return (await realpath(a)) === (await realpath(b));
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolves runtime answers from a game project's real Node installation.
|
|
142
|
+
* Resolution is anchored with createRequire so hoisted installs count as
|
|
143
|
+
* project-owned. A present package that Node cannot load is surfaced and is
|
|
144
|
+
* never silently replaced with another version.
|
|
145
|
+
*/
|
|
146
|
+
export class PackageResolver {
|
|
147
|
+
projectPath;
|
|
148
|
+
projectRequire;
|
|
149
|
+
projectManifestUsable;
|
|
150
|
+
constructor(projectPath) {
|
|
151
|
+
this.projectPath = projectPath;
|
|
152
|
+
if (projectPath) {
|
|
153
|
+
this.projectRequire = createRequire(path.join(projectPath, 'package.json'));
|
|
154
|
+
this.projectManifestUsable = projectPackageJsonUsable(projectPath);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async load(packageName, subpath) {
|
|
158
|
+
const specifier = specifierFor(packageName, subpath);
|
|
159
|
+
const projectRoot = this.projectPath &&
|
|
160
|
+
this.projectRequire &&
|
|
161
|
+
(await this.projectManifestUsable)
|
|
162
|
+
? await installedPackageRoot(this.projectPath, this.projectRequire, packageName, specifier)
|
|
163
|
+
: undefined;
|
|
164
|
+
if (projectRoot && this.projectRequire) {
|
|
165
|
+
let version = '(unknown version)';
|
|
166
|
+
try {
|
|
167
|
+
version = await packageVersion(projectRoot);
|
|
168
|
+
let loaded;
|
|
169
|
+
try {
|
|
170
|
+
loaded = this.projectRequire(specifier);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
// The repository's pre-publish workspace links intentionally export
|
|
174
|
+
// TS source to bundlers. Once the checkout is built, the CLI's
|
|
175
|
+
// workspace runtime hook makes the same physical package loadable
|
|
176
|
+
// from dist without weakening errors for arbitrary broken installs.
|
|
177
|
+
const bundledRoot = BUNDLED_SPECIFIERS.has(specifier)
|
|
178
|
+
? await bundledPackageRoot(packageName, specifier)
|
|
179
|
+
: undefined;
|
|
180
|
+
if (!bundledRoot || !(await samePackage(projectRoot, bundledRoot)))
|
|
181
|
+
throw error;
|
|
182
|
+
loaded = (await loadBundledModule(specifier, bundledRoot));
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
module: loaded,
|
|
186
|
+
packageRoot: projectRoot,
|
|
187
|
+
provenance: { package: packageName, version, source: 'project' },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
throw new Error(`Cannot load ${packageName} ${version} from the project: ${causeText(error)}`, { cause: error });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!BUNDLED_SPECIFIERS.has(specifier))
|
|
195
|
+
throw new PackageUnavailableError(packageName);
|
|
196
|
+
try {
|
|
197
|
+
const packageRoot = await bundledPackageRoot(packageName, specifier);
|
|
198
|
+
const version = await packageVersion(packageRoot);
|
|
199
|
+
return {
|
|
200
|
+
module: (await loadBundledModule(specifier, packageRoot)),
|
|
201
|
+
packageRoot,
|
|
202
|
+
provenance: { package: packageName, version, source: 'bundled' },
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
throw new Error(`Cannot load bundled ${packageName}: ${causeText(error)}`, { cause: error });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function exists(target) {
|
|
211
|
+
try {
|
|
212
|
+
await access(target);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (error.code === 'ENOENT')
|
|
217
|
+
return false;
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** Stable, de-duplicated rows in first-resolution order. */
|
|
222
|
+
export function provenanceRows(loads) {
|
|
223
|
+
const rows = new Map();
|
|
224
|
+
for (const loaded of loads) {
|
|
225
|
+
if (!rows.has(loaded.provenance.package)) {
|
|
226
|
+
rows.set(loaded.provenance.package, loaded.provenance);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return [...rows.values()];
|
|
230
|
+
}
|
|
231
|
+
/** Makes per-package fallback visible whenever an answer mixes sources. */
|
|
232
|
+
export function mixedSourceWarnings(rows) {
|
|
233
|
+
if (new Set(rows.map((row) => row.source)).size < 2)
|
|
234
|
+
return [];
|
|
235
|
+
const describe = (source) => {
|
|
236
|
+
const names = rows.filter((row) => row.source === source).map((row) => row.package);
|
|
237
|
+
return `${names.join(', ')} from ${source === 'project' ? 'the project' : 'the MCP bundle'}`;
|
|
238
|
+
};
|
|
239
|
+
return [`Mixed @waica package sources: ${describe('project')}; ${describe('bundled')}.`];
|
|
240
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { access, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export class WaicaToolError extends Error {
|
|
4
|
+
body;
|
|
5
|
+
constructor(body) {
|
|
6
|
+
super(body.message);
|
|
7
|
+
this.body = body;
|
|
8
|
+
this.name = 'WaicaToolError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export const ABSOLUTE_PATH_MESSAGE = "project_path must be absolute because a stdio server's working directory belongs to the agent host, not the game project.";
|
|
12
|
+
export function assertAbsoluteProjectPath(projectPath) {
|
|
13
|
+
if (!path.isAbsolute(projectPath)) {
|
|
14
|
+
throw new WaicaToolError({
|
|
15
|
+
code: 'absolute-project-path-required',
|
|
16
|
+
message: ABSOLUTE_PATH_MESSAGE,
|
|
17
|
+
projectPath,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function exists(target) {
|
|
22
|
+
try {
|
|
23
|
+
await access(target);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT')
|
|
28
|
+
return false;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Enforces the common file-project boundary used by every non-create tool. */
|
|
33
|
+
export async function requireWaicaProject(projectPath) {
|
|
34
|
+
assertAbsoluteProjectPath(projectPath);
|
|
35
|
+
let info;
|
|
36
|
+
try {
|
|
37
|
+
info = await stat(projectPath);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code === 'ENOENT') {
|
|
41
|
+
throw cannotOperate(projectPath, 'missing-path', `Project path does not exist: ${projectPath}`);
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
if (!info.isDirectory()) {
|
|
46
|
+
throw cannotOperate(projectPath, 'not-directory', `Project path is not a directory: ${projectPath}`);
|
|
47
|
+
}
|
|
48
|
+
const gameMarker = await exists(path.join(projectPath, 'src/game.json'));
|
|
49
|
+
const sceneMarker = await exists(path.join(projectPath, 'src/scenes/main.scene.json'));
|
|
50
|
+
if (!gameMarker && !sceneMarker) {
|
|
51
|
+
throw cannotOperate(projectPath, 'not-waica-project', 'Not a Waica project: expected src/game.json or src/scenes/main.scene.json.');
|
|
52
|
+
}
|
|
53
|
+
const notes = [];
|
|
54
|
+
if (!gameMarker)
|
|
55
|
+
notes.push('Project marker src/game.json is missing; main.scene.json was accepted.');
|
|
56
|
+
if (!sceneMarker) {
|
|
57
|
+
notes.push('Project marker src/scenes/main.scene.json is missing; src/game.json was accepted.');
|
|
58
|
+
}
|
|
59
|
+
return { notes };
|
|
60
|
+
}
|
|
61
|
+
export function cannotOperate(projectPath, reason, message) {
|
|
62
|
+
return new WaicaToolError({
|
|
63
|
+
code: 'cannot-operate',
|
|
64
|
+
message,
|
|
65
|
+
projectPath,
|
|
66
|
+
reason,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export async function directFiles(directory, suffix) {
|
|
70
|
+
try {
|
|
71
|
+
return (await readdir(directory, { withFileTypes: true }))
|
|
72
|
+
.filter((entry) => entry.isFile() && (!suffix || entry.name.endsWith(suffix)))
|
|
73
|
+
.map((entry) => entry.name)
|
|
74
|
+
.sort();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error.code === 'ENOENT')
|
|
78
|
+
return [];
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function assertSafeSlug(value, label) {
|
|
83
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value)) {
|
|
84
|
+
throw new Error(`${label} must use letters, numbers, hyphens or underscores and cannot contain a path.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { assertSafeSlug } from './project-path.js';
|
|
4
|
+
function nameWords(name) {
|
|
5
|
+
return name
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
8
|
+
.split(/[^A-Za-z0-9]+/)
|
|
9
|
+
.filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
export function componentClassName(name) {
|
|
12
|
+
const words = nameWords(name);
|
|
13
|
+
const value = words.map((word) => word[0]?.toUpperCase() + word.slice(1)).join('');
|
|
14
|
+
if (!/^[A-Za-z][A-Za-z0-9]*$/.test(value)) {
|
|
15
|
+
throw new Error('Component names must start with a letter and use letters or numbers.');
|
|
16
|
+
}
|
|
17
|
+
if (value === 'Component') {
|
|
18
|
+
throw new Error('"Component" is the engine base class — pick another name.');
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function componentFilePath(name) {
|
|
23
|
+
const words = nameWords(name);
|
|
24
|
+
componentClassName(name);
|
|
25
|
+
return `src/components/${words.map((word) => word.toLowerCase()).join('-')}.ts`;
|
|
26
|
+
}
|
|
27
|
+
function componentTemplate(name) {
|
|
28
|
+
const Class = componentClassName(name);
|
|
29
|
+
return `import { Component } from '@waica/engine'
|
|
30
|
+
|
|
31
|
+
// Project-owned behavior — loaded by the shipped game and editor Play.
|
|
32
|
+
export class ${Class} extends Component {
|
|
33
|
+
static override componentName = '${Class}'
|
|
34
|
+
static override params = {
|
|
35
|
+
speed: { label: 'Speed', min: 0, max: 30, step: 0.5 },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
speed = 5
|
|
39
|
+
|
|
40
|
+
override onUpdate(dt: number): void {
|
|
41
|
+
// Add this component's per-frame behavior here.
|
|
42
|
+
void dt
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
function roleTemplate(role) {
|
|
48
|
+
return `import { defineRole } from '@waica/engine'
|
|
49
|
+
|
|
50
|
+
// The "${role}" role — generated by waica.
|
|
51
|
+
// A role is a character's whole behavior package: a plain-language
|
|
52
|
+
// description for the editor, the driver component its states move,
|
|
53
|
+
// the starter state graph, and the state code itself.
|
|
54
|
+
defineRole('${role}', {
|
|
55
|
+
description: 'Describe what a ${role} does — shown when picking roles.',
|
|
56
|
+
// driver: 'PlatformerMotor', // uncomment if the states move a driver component
|
|
57
|
+
graph: {
|
|
58
|
+
initial: 'idle',
|
|
59
|
+
states: { idle: { transitions: [] } },
|
|
60
|
+
},
|
|
61
|
+
states: {
|
|
62
|
+
idle: {
|
|
63
|
+
onUpdate({ entity, game, fsm }, dt) {
|
|
64
|
+
// Runs every frame while a "${role}" is in its "idle" state.
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
function stateTemplate(role, state) {
|
|
72
|
+
if (role === 'player') {
|
|
73
|
+
return `import { defineStates } from '@waica/engine'
|
|
74
|
+
import { PlatformerMotor } from '@waica/behaviors'
|
|
75
|
+
|
|
76
|
+
// Code for the "${state}" state — generated by waica.
|
|
77
|
+
// When it starts, when it ends and which clip it plays live in the
|
|
78
|
+
// prefab's State Machine data. While in it, the body keeps the role's
|
|
79
|
+
// stock update (running, gravity, collisions, land/fall signals), so
|
|
80
|
+
// this file only says what makes "${state}" special.
|
|
81
|
+
defineStates('${role}', {
|
|
82
|
+
${state}: {
|
|
83
|
+
onEnter({ entity }) {
|
|
84
|
+
const motor = entity.get(PlatformerMotor)
|
|
85
|
+
if (!motor) return
|
|
86
|
+
// One burst in the facing direction — replace with your state's idea.
|
|
87
|
+
motor.vx = motor.facing * 30
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
return `import { defineStates } from '@waica/engine'
|
|
94
|
+
|
|
95
|
+
// Code for the "${state}" state — generated by waica.
|
|
96
|
+
// When it starts and when it ends live in the prefab's State Machine
|
|
97
|
+
// data; this file is only the behavior. Hooks receive { entity, game, fsm }.
|
|
98
|
+
defineStates('${role}', {
|
|
99
|
+
${state}: {
|
|
100
|
+
onEnter({ entity }) {
|
|
101
|
+
console.log(\`\${entity.name} entered "${state}"\`)
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
const UI_TEMPLATE = `<style>
|
|
108
|
+
.my-ui {
|
|
109
|
+
position: absolute;
|
|
110
|
+
top: 12px;
|
|
111
|
+
left: 12px;
|
|
112
|
+
font: 600 20px system-ui, sans-serif;
|
|
113
|
+
color: #fff;
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
116
|
+
<div class="my-ui">✨ {{points}}</div>
|
|
117
|
+
`;
|
|
118
|
+
async function writeOnce(projectPath, relative, contents) {
|
|
119
|
+
const target = path.join(projectPath, relative);
|
|
120
|
+
try {
|
|
121
|
+
await access(target);
|
|
122
|
+
return { path: relative, created: false, reason: 'exists' };
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error.code !== 'ENOENT')
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
129
|
+
await writeFile(target, contents);
|
|
130
|
+
return { path: relative, created: true };
|
|
131
|
+
}
|
|
132
|
+
export async function scaffoldComponent(projectPath, name) {
|
|
133
|
+
const className = componentClassName(name);
|
|
134
|
+
return { ...(await writeOnce(projectPath, componentFilePath(name), componentTemplate(name))), className };
|
|
135
|
+
}
|
|
136
|
+
export async function scaffoldRole(projectPath, role) {
|
|
137
|
+
assertSafeSlug(role, 'role');
|
|
138
|
+
return writeOnce(projectPath, `src/roles/${role}.ts`, roleTemplate(role));
|
|
139
|
+
}
|
|
140
|
+
export async function scaffoldState(projectPath, role, state) {
|
|
141
|
+
assertSafeSlug(role, 'role');
|
|
142
|
+
assertSafeSlug(state, 'state');
|
|
143
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(state)) {
|
|
144
|
+
throw new Error('state must be a valid TypeScript identifier because the scaffold uses it as an object key.');
|
|
145
|
+
}
|
|
146
|
+
// Role-agnostic by contract: the editor and shipped runtime both glob the
|
|
147
|
+
// flat src/states/*.ts convention, and the file self-registers its role.
|
|
148
|
+
return writeOnce(projectPath, `src/states/${state}.ts`, stateTemplate(role, state));
|
|
149
|
+
}
|
|
150
|
+
export async function scaffoldUi(projectPath, name) {
|
|
151
|
+
assertSafeSlug(name, 'name');
|
|
152
|
+
return writeOnce(projectPath, `src/ui/${name}.html`, UI_TEMPLATE);
|
|
153
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { createProject } from './create-project.js';
|
|
4
|
+
import { describeArchetype, listComponents, projectSummary, } from './introspection.js';
|
|
5
|
+
import { ABSOLUTE_PATH_MESSAGE, WaicaToolError, assertAbsoluteProjectPath, requireWaicaProject, } from './project-path.js';
|
|
6
|
+
import { scaffoldComponent, scaffoldRole, scaffoldState, scaffoldUi, } from './scaffolds.js';
|
|
7
|
+
import { validateProject } from './validation.js';
|
|
8
|
+
const PROJECT_PATH = {
|
|
9
|
+
type: 'string',
|
|
10
|
+
description: 'Absolute path to the user game project.',
|
|
11
|
+
};
|
|
12
|
+
function schema(properties, required = []) {
|
|
13
|
+
return {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: { project_path: PROJECT_PATH, ...properties },
|
|
16
|
+
required: ['project_path', ...required],
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export const TOOLS = [
|
|
21
|
+
{
|
|
22
|
+
name: 'create_project',
|
|
23
|
+
description: 'Create a blank or playable Waica project in a new or empty directory.',
|
|
24
|
+
inputSchema: schema({
|
|
25
|
+
start: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
enum: ['demo', 'blank'],
|
|
28
|
+
default: 'demo',
|
|
29
|
+
description: 'demo includes archetype content; blank creates only the chassis.',
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'list_components',
|
|
36
|
+
description: 'List installed archetype component metadata and textual project-owned code paths.',
|
|
37
|
+
inputSchema: schema({}),
|
|
38
|
+
annotations: { openWorldHint: false },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'describe_archetype',
|
|
42
|
+
description: 'Describe the active or requested installed archetype manifest.',
|
|
43
|
+
inputSchema: schema({
|
|
44
|
+
archetype: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'Optional archetype manifest id; defaults to src/game.json.',
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
annotations: { openWorldHint: false },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'project_summary',
|
|
53
|
+
description: 'Summarize scenes, prefabs, code, UI, stats and controls from project files.',
|
|
54
|
+
inputSchema: schema({}),
|
|
55
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'validate_project',
|
|
59
|
+
description: 'Validate every project scene, prefab and configuration file with machine findings.',
|
|
60
|
+
inputSchema: schema({}),
|
|
61
|
+
annotations: { openWorldHint: false },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'scaffold_component',
|
|
65
|
+
description: 'Create the editor-compatible starter for a project-owned component.',
|
|
66
|
+
inputSchema: schema({ name: { type: 'string', minLength: 1 } }, ['name']),
|
|
67
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'scaffold_role',
|
|
71
|
+
description: 'Create the editor-compatible starter for a custom character role.',
|
|
72
|
+
inputSchema: schema({ role: { type: 'string', minLength: 1 } }, ['role']),
|
|
73
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'scaffold_state',
|
|
77
|
+
description: 'Create the editor-compatible state-code starter for a role and state.',
|
|
78
|
+
inputSchema: schema({
|
|
79
|
+
role: { type: 'string', minLength: 1 },
|
|
80
|
+
state: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
minLength: 1,
|
|
83
|
+
pattern: '^[A-Za-z][A-Za-z0-9_]*$',
|
|
84
|
+
description: 'TypeScript identifier used as the generated state object key.',
|
|
85
|
+
},
|
|
86
|
+
}, ['role', 'state']),
|
|
87
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'scaffold_ui',
|
|
91
|
+
description: 'Create the editor-compatible starter HTML for a UI piece.',
|
|
92
|
+
inputSchema: schema({ name: { type: 'string', minLength: 1 } }, ['name']),
|
|
93
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
function requiredString(args, name, projectPath = '') {
|
|
97
|
+
const value = args[name];
|
|
98
|
+
if (typeof value === 'string' && value.length > 0)
|
|
99
|
+
return value;
|
|
100
|
+
throw new WaicaToolError({
|
|
101
|
+
code: 'invalid-input',
|
|
102
|
+
message: `${name} must be a non-empty string.`,
|
|
103
|
+
projectPath,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async function execute(name, args) {
|
|
107
|
+
const projectPath = requiredString(args, 'project_path');
|
|
108
|
+
// Keep this at the dispatch boundary so every tool has byte-identical
|
|
109
|
+
// stdio-cwd semantics, including create_project.
|
|
110
|
+
assertAbsoluteProjectPath(projectPath);
|
|
111
|
+
switch (name) {
|
|
112
|
+
case 'create_project': {
|
|
113
|
+
const start = args.start === undefined ? 'demo' : requiredString(args, 'start', projectPath);
|
|
114
|
+
if (start !== 'demo' && start !== 'blank') {
|
|
115
|
+
throw new WaicaToolError({
|
|
116
|
+
code: 'invalid-input',
|
|
117
|
+
message: 'start must be "demo" or "blank".',
|
|
118
|
+
projectPath,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return { ...(await createProject(projectPath, start)) };
|
|
122
|
+
}
|
|
123
|
+
case 'list_components':
|
|
124
|
+
return listComponents(projectPath);
|
|
125
|
+
case 'describe_archetype':
|
|
126
|
+
return describeArchetype(projectPath, args.archetype === undefined ? undefined : requiredString(args, 'archetype', projectPath));
|
|
127
|
+
case 'project_summary':
|
|
128
|
+
return projectSummary(projectPath);
|
|
129
|
+
case 'validate_project':
|
|
130
|
+
return validateProject(projectPath);
|
|
131
|
+
case 'scaffold_component': {
|
|
132
|
+
const check = await requireWaicaProject(projectPath);
|
|
133
|
+
return {
|
|
134
|
+
...(await scaffoldComponent(projectPath, requiredString(args, 'name', projectPath))),
|
|
135
|
+
notes: check.notes,
|
|
136
|
+
provenance: [],
|
|
137
|
+
warnings: [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
case 'scaffold_role': {
|
|
141
|
+
const check = await requireWaicaProject(projectPath);
|
|
142
|
+
return {
|
|
143
|
+
...(await scaffoldRole(projectPath, requiredString(args, 'role', projectPath))),
|
|
144
|
+
notes: check.notes,
|
|
145
|
+
provenance: [],
|
|
146
|
+
warnings: [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
case 'scaffold_state': {
|
|
150
|
+
const check = await requireWaicaProject(projectPath);
|
|
151
|
+
return {
|
|
152
|
+
...(await scaffoldState(projectPath, requiredString(args, 'role', projectPath), requiredString(args, 'state', projectPath))),
|
|
153
|
+
notes: check.notes,
|
|
154
|
+
provenance: [],
|
|
155
|
+
warnings: [],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
case 'scaffold_ui': {
|
|
159
|
+
const check = await requireWaicaProject(projectPath);
|
|
160
|
+
return {
|
|
161
|
+
...(await scaffoldUi(projectPath, requiredString(args, 'name', projectPath))),
|
|
162
|
+
notes: check.notes,
|
|
163
|
+
provenance: [],
|
|
164
|
+
warnings: [],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
default:
|
|
168
|
+
throw new WaicaToolError({
|
|
169
|
+
code: 'unknown-tool',
|
|
170
|
+
message: `Unknown tool "${name}".`,
|
|
171
|
+
projectPath,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function jsonSafe(value) {
|
|
176
|
+
return JSON.parse(JSON.stringify(value));
|
|
177
|
+
}
|
|
178
|
+
function result(payload, isError = false) {
|
|
179
|
+
const safe = jsonSafe(payload);
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: 'text', text: JSON.stringify(safe, null, 2) }],
|
|
182
|
+
structuredContent: safe,
|
|
183
|
+
...(isError ? { isError: true } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function errorResult(error, args) {
|
|
187
|
+
const projectPath = typeof args.project_path === 'string' ? args.project_path : '';
|
|
188
|
+
if (error instanceof WaicaToolError) {
|
|
189
|
+
return result({ error: error.body, provenance: [] }, true);
|
|
190
|
+
}
|
|
191
|
+
return result({
|
|
192
|
+
error: {
|
|
193
|
+
code: 'tool-error',
|
|
194
|
+
message: error instanceof Error ? error.message : String(error),
|
|
195
|
+
projectPath,
|
|
196
|
+
},
|
|
197
|
+
provenance: [],
|
|
198
|
+
}, true);
|
|
199
|
+
}
|
|
200
|
+
export function createWaicaMcpServer() {
|
|
201
|
+
const server = new Server({ name: '@waica/mcp', version: '0.1.0' }, {
|
|
202
|
+
capabilities: { tools: {} },
|
|
203
|
+
instructions: 'Operate on user Waica projects through absolute project_path values. Keep editing JSON and TypeScript with the host file tools.',
|
|
204
|
+
});
|
|
205
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
206
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
207
|
+
const args = (request.params.arguments ?? {});
|
|
208
|
+
try {
|
|
209
|
+
return result(await execute(request.params.name, args));
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
return errorResult(error, args);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
return server;
|
|
216
|
+
}
|
|
217
|
+
export { ABSOLUTE_PATH_MESSAGE };
|