@vgai/sdk 0.5.0 → 0.5.2
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/package.json +2 -2
- package/src/account.ts +5 -4
- package/src/cinematic/capabilities-operations.ts +3 -3
- package/src/cinematic/cue-operations.ts +11 -11
- package/src/cinematic/gsap-operations.ts +5 -5
- package/src/cinematic/index.ts +3 -3
- package/src/cinematic/preview-operations.ts +10 -10
- package/src/cinematic/preview-transport.ts +2 -2
- package/src/cinematic/render-operations.ts +15 -15
- package/src/cinematic/render-transport.ts +2 -2
- package/src/cinematic/theatre-operations.ts +17 -17
- package/src/editor/camera-operations.ts +7 -7
- package/src/editor/console-operations.ts +5 -5
- package/src/editor/hierarchy-operations.ts +5 -5
- package/src/editor/index.ts +3 -3
- package/src/editor/open-operations.ts +10 -10
- package/src/editor/screenshot-operations.ts +5 -5
- package/src/editor/selection-operations.ts +7 -7
- package/src/editor/session-operations.ts +3 -3
- package/src/editor/source-location-operations.ts +6 -6
- package/src/editor/transport.ts +24 -11
- package/src/errors.ts +8 -8
- package/src/generations.ts +12 -1
- package/src/http/http-projection.ts +26 -26
- package/src/index.ts +22 -30
- package/src/mcp/index.ts +2 -2
- package/src/mcp/mcp-projection.ts +25 -25
- package/src/mcp/mcp-server.ts +2 -2
- package/src/operations.ts +5 -5
- package/src/play/control-operations.ts +10 -10
- package/src/play/debug-command-operations.ts +12 -12
- package/src/play/index.ts +3 -3
- package/src/play/input-operations.ts +10 -10
- package/src/play/lifecycle-operations.ts +11 -11
- package/src/play/log-operations.ts +7 -7
- package/src/play/run-ticks-operations.ts +8 -8
- package/src/play/state-operations.ts +9 -9
- package/src/play/status-operations.ts +7 -7
- package/src/play/transport.ts +93 -22
- package/src/project/asset-operations.ts +8 -8
- package/src/project/component-operations.ts +10 -10
- package/src/project/discovery-operations.ts +9 -9
- package/src/project/entity-operations.ts +8 -8
- package/src/project/index.ts +4 -4
- package/src/project/input-map-operations.ts +9 -9
- package/src/project/inspection-operation.ts +3 -3
- package/src/project/manifest-operations.ts +9 -9
- package/src/project/scene-operations.ts +14 -14
- package/src/project/shared.ts +17 -17
- package/src/project-tool-catalog.ts +21 -5
- package/src/registry.ts +39 -39
- package/src/render/render-cinematic.ts +24 -1
- package/src/tools.ts +19 -12
- package/src/types.ts +4 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP projection of the operation registry (B8, §8 B8).
|
|
3
3
|
*
|
|
4
|
-
* The SAME `
|
|
4
|
+
* The SAME `ToolRegistry` that backs the SDK (`dispatch`) and the CLI
|
|
5
5
|
* (`packages/vgai-cli/src/oclif/*`, B6) is projected here as a small HTTP
|
|
6
6
|
* server. Routes are GENERATED from `registry.listOperations()` — one route
|
|
7
7
|
* per operation — never hand-authored per op: adding an operation to the SDK
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
* `tools/list` and is built from the very same `listOperations()` summaries.
|
|
27
27
|
*
|
|
28
28
|
* OUTCOME SHAPE: every response body is the registry's own
|
|
29
|
-
* `
|
|
29
|
+
* `ToolOutcome` — `{ ok: true, data }` or `{ ok: false, error }` with a
|
|
30
30
|
* machine-readable `error.code` — byte-for-byte the shape the SDK returns and
|
|
31
31
|
* the CLI prints under `--json`. The HTTP STATUS is a secondary, best-effort
|
|
32
32
|
* signal derived from that code (`httpStatusForOutcome`); the authoritative,
|
|
33
33
|
* cross-projection-identical fact is always `error.code` in the body (§8 B8
|
|
34
34
|
* AC: "HTTP and MCP return the same structured error codes as SDK and CLI").
|
|
35
35
|
*
|
|
36
|
-
* CONTEXT: an operation's `
|
|
36
|
+
* CONTEXT: an operation's `ToolContext` (projectRoot, editorUrl, an
|
|
37
37
|
* injected editor transport for tests, ...) is produced per request by the
|
|
38
38
|
* caller-supplied `createContext(req)` (or the static `context`), so the same
|
|
39
39
|
* server can serve one project or resolve the project per request — the SDK
|
|
@@ -45,22 +45,22 @@ import { createServer } from 'node:http';
|
|
|
45
45
|
import { z } from 'zod';
|
|
46
46
|
import { CORE_ERROR_CODES } from '../errors.js';
|
|
47
47
|
import { operations as defaultRegistry } from '../index.js';
|
|
48
|
-
import type {
|
|
49
|
-
import type {
|
|
48
|
+
import type { ToolOutcome, ToolRegistry, ToolSummary } from '../registry.js';
|
|
49
|
+
import type { ToolContext } from '../types.js';
|
|
50
50
|
|
|
51
51
|
/** Options for building the HTTP projection handler/server. */
|
|
52
52
|
export interface HttpProjectionOptions {
|
|
53
53
|
/** Registry to project. Defaults to the shared `operations` registry (same one the CLI uses). */
|
|
54
|
-
registry?:
|
|
54
|
+
registry?: ToolRegistry;
|
|
55
55
|
/**
|
|
56
56
|
* URL prefix every operation route lives under. Default `/op`, so
|
|
57
57
|
* `project.scene.read` is `POST /op/project.scene.read`. No trailing slash.
|
|
58
58
|
*/
|
|
59
59
|
basePath?: string;
|
|
60
60
|
/** Static context handed to every `dispatch`. Ignored when `createContext` is given. */
|
|
61
|
-
context?:
|
|
61
|
+
context?: ToolContext;
|
|
62
62
|
/** Per-request context factory (wins over `context`) — e.g. resolve `projectRoot` from a header. */
|
|
63
|
-
createContext?: (req: IncomingMessage) =>
|
|
63
|
+
createContext?: (req: IncomingMessage) => ToolContext | Promise<ToolContext>;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** One entry of the `GET <basePath>` manifest — the HTTP analogue of an MCP tool descriptor. */
|
|
@@ -73,21 +73,21 @@ export interface HttpRouteDescriptor {
|
|
|
73
73
|
mutates: boolean;
|
|
74
74
|
supportsDryRun: boolean;
|
|
75
75
|
host: string;
|
|
76
|
-
requires:
|
|
77
|
-
permission:
|
|
76
|
+
requires: ToolSummary['requires'];
|
|
77
|
+
permission: ToolSummary['permission'];
|
|
78
78
|
/** Zod-derived JSON Schema for the request body / `?input=` — same conversion MCP tool schemas use. */
|
|
79
79
|
inputSchema: unknown;
|
|
80
80
|
errorCodes: string[];
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/** Convert an operation's Zod input schema to JSON Schema — the repo's established `z.toJSONSchema` call (see `scripts/generate-schema.ts`). */
|
|
84
|
-
export function inputJsonSchema(summary:
|
|
84
|
+
export function inputJsonSchema(summary: ToolSummary): unknown {
|
|
85
85
|
return z.toJSONSchema(summary.input, { unrepresentable: 'any' });
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/** Build the `GET <basePath>` manifest from `listOperations()` — routes are derived, never hand-written. */
|
|
89
89
|
export function buildRouteManifest(
|
|
90
|
-
registry:
|
|
90
|
+
registry: ToolRegistry,
|
|
91
91
|
basePath: string,
|
|
92
92
|
): HttpRouteDescriptor[] {
|
|
93
93
|
return registry
|
|
@@ -109,19 +109,19 @@ export function buildRouteManifest(
|
|
|
109
109
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/** A projection-level (not `dispatch`) failure, still shaped as an `
|
|
113
|
-
function failure(code: string, message: string, data?: unknown):
|
|
112
|
+
/** A projection-level (not `dispatch`) failure, still shaped as an `ToolOutcome` so every response body is uniform. */
|
|
113
|
+
function failure(code: string, message: string, data?: unknown): ToolOutcome {
|
|
114
114
|
return { ok: false, error: { code, message, ...(data !== undefined ? { data } : {}) } };
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* Map an `
|
|
118
|
+
* Map an `ToolOutcome` to an HTTP status. Best-effort ONLY — the
|
|
119
119
|
* authoritative, cross-projection-identical signal is `error.code` in the
|
|
120
120
|
* body, not this status. Success is 200; a handful of well-known core codes
|
|
121
121
|
* get their conventional status; every other structured failure defaults to
|
|
122
122
|
* 400 (a caller-actionable problem) rather than 500.
|
|
123
123
|
*/
|
|
124
|
-
export function httpStatusForOutcome(outcome:
|
|
124
|
+
export function httpStatusForOutcome(outcome: ToolOutcome): number {
|
|
125
125
|
if (outcome.ok) return 200;
|
|
126
126
|
switch (outcome.error.code) {
|
|
127
127
|
case CORE_ERROR_CODES.OPERATION_NOT_FOUND:
|
|
@@ -205,15 +205,15 @@ async function parseRouteInput(
|
|
|
205
205
|
/**
|
|
206
206
|
* The pure routing core, exported for tests: given a method, pathname and
|
|
207
207
|
* already-parsed input, resolve the target operation and dispatch it —
|
|
208
|
-
* returning the `
|
|
208
|
+
* returning the `ToolOutcome` and the HTTP status, with no `http`
|
|
209
209
|
* objects involved. `serveHttpRequest` is the thin I/O wrapper around this.
|
|
210
210
|
*/
|
|
211
211
|
export async function dispatchRoute(
|
|
212
|
-
registry:
|
|
212
|
+
registry: ToolRegistry,
|
|
213
213
|
basePath: string,
|
|
214
214
|
route: ParsedRoute,
|
|
215
|
-
ctx:
|
|
216
|
-
): Promise<
|
|
215
|
+
ctx: ToolContext,
|
|
216
|
+
): Promise<ToolOutcome> {
|
|
217
217
|
if (route.parseError !== undefined) {
|
|
218
218
|
// A malformed body is a caller input problem — surface it as the SAME
|
|
219
219
|
// INVALID_INPUT code the SDK/CLI use for a schema rejection.
|
|
@@ -240,18 +240,18 @@ export async function dispatchRoute(
|
|
|
240
240
|
return registry.dispatch(route.name, route.input, ctx);
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
/** Send an `
|
|
244
|
-
function sendOutcome(res: ServerResponse, outcome:
|
|
243
|
+
/** Send an `ToolOutcome` as a JSON response with the derived status. */
|
|
244
|
+
function sendOutcome(res: ServerResponse, outcome: ToolOutcome): void {
|
|
245
245
|
const body = `${JSON.stringify(outcome)}\n`;
|
|
246
246
|
res.writeHead(httpStatusForOutcome(outcome), { 'content-type': 'application/json' });
|
|
247
247
|
res.end(body);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
/** Resolve the per-request `
|
|
250
|
+
/** Resolve the per-request `ToolContext` from the options (static or factory). */
|
|
251
251
|
async function resolveHttpContext(
|
|
252
252
|
options: HttpProjectionOptions,
|
|
253
253
|
req: IncomingMessage,
|
|
254
|
-
): Promise<
|
|
254
|
+
): Promise<ToolContext> {
|
|
255
255
|
if (options.createContext) return options.createContext(req);
|
|
256
256
|
if (options.context) return options.context;
|
|
257
257
|
return {};
|
|
@@ -260,7 +260,7 @@ async function resolveHttpContext(
|
|
|
260
260
|
/** The full request lifecycle, factored out of the returned closure to keep each step flat. */
|
|
261
261
|
async function handleProjectionRequest(
|
|
262
262
|
options: HttpProjectionOptions,
|
|
263
|
-
registry:
|
|
263
|
+
registry: ToolRegistry,
|
|
264
264
|
basePath: string,
|
|
265
265
|
req: IncomingMessage,
|
|
266
266
|
res: ServerResponse,
|
|
@@ -296,7 +296,7 @@ async function handleProjectionRequest(
|
|
|
296
296
|
? { name, method, input: undefined, parseError: parsed.parseError }
|
|
297
297
|
: { name, method, input: parsed.input };
|
|
298
298
|
|
|
299
|
-
let ctx:
|
|
299
|
+
let ctx: ToolContext;
|
|
300
300
|
try {
|
|
301
301
|
ctx = await resolveHttpContext(options, req);
|
|
302
302
|
} catch (err) {
|
package/src/index.ts
CHANGED
|
@@ -4,9 +4,9 @@ export * from './editor/index.js';
|
|
|
4
4
|
export {
|
|
5
5
|
CORE_ERROR_CODES,
|
|
6
6
|
type CoreErrorCode,
|
|
7
|
-
OperationError,
|
|
8
7
|
type StructuredIssue,
|
|
9
8
|
type StructuredOperationError,
|
|
9
|
+
ToolError,
|
|
10
10
|
toStructuredIssues,
|
|
11
11
|
} from './errors.js';
|
|
12
12
|
export * from './http/index.js';
|
|
@@ -15,62 +15,54 @@ export {
|
|
|
15
15
|
ProjectStatusInput,
|
|
16
16
|
ProjectStatusResult,
|
|
17
17
|
projectStatus,
|
|
18
|
-
|
|
18
|
+
registerBuiltinTools,
|
|
19
19
|
} from './operations.js';
|
|
20
20
|
export * from './perf/index.js';
|
|
21
21
|
export * from './play/index.js';
|
|
22
22
|
export * from './project/index.js';
|
|
23
23
|
export * from './project-tool-catalog.js';
|
|
24
24
|
export {
|
|
25
|
-
|
|
26
|
-
type
|
|
27
|
-
type
|
|
28
|
-
type
|
|
29
|
-
|
|
30
|
-
type
|
|
25
|
+
defineTool,
|
|
26
|
+
type ToolDefinition,
|
|
27
|
+
type ToolErrorDefinition,
|
|
28
|
+
type ToolOutcome,
|
|
29
|
+
ToolRegistry,
|
|
30
|
+
type ToolSummary,
|
|
31
31
|
} from './registry.js';
|
|
32
32
|
export * from './render/index.js';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
ToolHost,
|
|
38
|
-
ToolOutcome,
|
|
39
|
-
ToolPermission,
|
|
40
|
-
ToolPermissionRisk,
|
|
41
|
-
ToolRequirements,
|
|
42
|
-
ToolSummary,
|
|
43
|
-
} from './tools.js';
|
|
44
|
-
export { defineTool, ToolError, ToolRegistry } from './tools.js';
|
|
33
|
+
// The tool vocabulary is exported once, from its defining modules below.
|
|
34
|
+
// `./tools.js` is the consumer-facing entry point and re-exports the same
|
|
35
|
+
// symbols; re-exporting them here too would duplicate every identifier.
|
|
36
|
+
export type { ToolHost, ToolPermission, ToolPermissionRisk, ToolRequirements } from './tools.js';
|
|
45
37
|
export {
|
|
46
38
|
type ExecutionHost,
|
|
47
39
|
type ExecutionRequirements,
|
|
48
|
-
OPERATION_NAMESPACES,
|
|
49
|
-
type OperationContext,
|
|
50
|
-
type OperationNamespace,
|
|
51
40
|
type PermissionMetadata,
|
|
52
41
|
type PermissionRisk,
|
|
42
|
+
TOOL_NAMESPACES,
|
|
43
|
+
type ToolContext,
|
|
44
|
+
type ToolNamespace,
|
|
53
45
|
} from './types.js';
|
|
54
46
|
|
|
55
47
|
import { registerCinematicOperations } from './cinematic/index.js';
|
|
56
48
|
import { registerEditorOperations } from './editor/index.js';
|
|
57
|
-
import {
|
|
49
|
+
import { registerBuiltinTools } from './operations.js';
|
|
58
50
|
import { registerPlayOperations } from './play/index.js';
|
|
59
51
|
import { registerProjectOperations } from './project/index.js';
|
|
60
|
-
import {
|
|
52
|
+
import { ToolRegistry } from './registry.js';
|
|
61
53
|
|
|
62
54
|
/**
|
|
63
55
|
* Build a fresh registry preloaded with B1's sample operations plus B2's
|
|
64
56
|
* real `project.*` operations, B3's real `editor.*` operations, B4's real
|
|
65
57
|
* `play.*` operations, and B5's real `cinematic.*` operations. Deliberately
|
|
66
58
|
* separate register calls (not one function grown to cover all five) —
|
|
67
|
-
* `
|
|
59
|
+
* `registerBuiltinTools` is B1's own registration function, and its
|
|
68
60
|
* registry test asserts exactly what it registers; growing it here would
|
|
69
61
|
* break that assertion for a reason unrelated to what it actually tests.
|
|
70
62
|
*/
|
|
71
|
-
export function
|
|
72
|
-
const registry = new
|
|
73
|
-
|
|
63
|
+
export function createToolRegistry(): ToolRegistry {
|
|
64
|
+
const registry = new ToolRegistry();
|
|
65
|
+
registerBuiltinTools(registry);
|
|
74
66
|
registerProjectOperations(registry);
|
|
75
67
|
registerEditorOperations(registry);
|
|
76
68
|
registerPlayOperations(registry);
|
|
@@ -79,4 +71,4 @@ export function createOperationRegistry(): OperationRegistry {
|
|
|
79
71
|
}
|
|
80
72
|
|
|
81
73
|
/** Convenience default registry — the same one CLI/HTTP/MCP projections (B6-B8) will build on. */
|
|
82
|
-
export const operations =
|
|
74
|
+
export const operations = createToolRegistry();
|
package/src/mcp/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP projection of the operation registry (B8, §8 B8).
|
|
3
3
|
*
|
|
4
|
-
* The SAME `
|
|
4
|
+
* The SAME `ToolRegistry` behind the SDK, the CLI (B6), and the HTTP
|
|
5
5
|
* projection is registered here as MCP TOOLS (and, for a principled subset,
|
|
6
6
|
* MCP RESOURCES). Every tool is derived from a `listOperations()` summary —
|
|
7
7
|
* name, description, and a Zod-derived JSON Schema for its input — so adding
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
import { z } from 'zod';
|
|
41
41
|
import { CORE_ERROR_CODES } from '../errors.js';
|
|
42
42
|
import { operations as defaultRegistry } from '../index.js';
|
|
43
|
-
import type {
|
|
44
|
-
import type {
|
|
43
|
+
import type { ToolOutcome, ToolRegistry, ToolSummary } from '../registry.js';
|
|
44
|
+
import type { ToolContext } from '../types.js';
|
|
45
45
|
|
|
46
46
|
/** URI scheme/prefix under which read-only-state operations are also exposed as MCP resources. */
|
|
47
47
|
export const MCP_RESOURCE_PREFIX = 'vgai://op/';
|
|
@@ -72,11 +72,11 @@ export interface McpResource {
|
|
|
72
72
|
|
|
73
73
|
export interface McpProjectionOptions {
|
|
74
74
|
/** Registry to project. Defaults to the shared `operations` registry (same one the CLI/HTTP projections use). */
|
|
75
|
-
registry?:
|
|
75
|
+
registry?: ToolRegistry;
|
|
76
76
|
/** Static context handed to every dispatch. Ignored when `createContext` is given. */
|
|
77
|
-
context?:
|
|
77
|
+
context?: ToolContext;
|
|
78
78
|
/** Per-call context factory (wins over `context`). */
|
|
79
|
-
createContext?: () =>
|
|
79
|
+
createContext?: () => ToolContext | Promise<ToolContext>;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/** The Zod-derived JSON Schema for an op's input — the SAME conversion the HTTP manifest uses. */
|
|
@@ -91,7 +91,7 @@ function objectWireSchema(schema: Record<string, unknown>): Record<string, unkno
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
function fullOperationSchema(
|
|
94
|
-
summary:
|
|
94
|
+
summary: ToolSummary,
|
|
95
95
|
direction: 'input' | 'output',
|
|
96
96
|
): Record<string, unknown> {
|
|
97
97
|
const operationSchema = objectWireSchema(
|
|
@@ -122,7 +122,7 @@ function fullOperationSchema(
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function schemaResourceUri(summary:
|
|
125
|
+
function schemaResourceUri(summary: ToolSummary, direction: 'input' | 'output'): string {
|
|
126
126
|
return `${MCP_SCHEMA_RESOURCE_PREFIX}${direction}/${summary.name}`;
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -143,7 +143,7 @@ function compactLargeProperty(value: unknown, resourceUri: string, limit = 4_096
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
function compactOperationSchema(
|
|
146
|
-
summary:
|
|
146
|
+
summary: ToolSummary,
|
|
147
147
|
direction: 'input' | 'output',
|
|
148
148
|
): Record<string, unknown> {
|
|
149
149
|
const full = fullOperationSchema(summary, direction);
|
|
@@ -176,12 +176,12 @@ function compactOperationSchema(
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/** Compact MCP-list schema; the full version is exposed as an on-demand resource when large. */
|
|
179
|
-
export function toolInputSchema(summary:
|
|
179
|
+
export function toolInputSchema(summary: ToolSummary): unknown {
|
|
180
180
|
return compactOperationSchema(summary, 'input');
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
/** Derive one MCP tool from an operation summary: dotted name (mirrors §5.7 hierarchy), description, Zod-derived schema. */
|
|
184
|
-
export function
|
|
184
|
+
export function toolToMcpTool(summary: ToolSummary): McpTool {
|
|
185
185
|
return {
|
|
186
186
|
name: summary.name,
|
|
187
187
|
description: `${summary.summary}\n\n${summary.description}`.trim(),
|
|
@@ -199,13 +199,13 @@ export function operationToTool(summary: OperationSummary): McpTool {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
/** True when a read-only op takes no required input, so it names readable STATE addressable by URI alone (resource-worthy). See module jsdoc. */
|
|
202
|
-
export function isResourceEligible(summary:
|
|
202
|
+
export function isResourceEligible(summary: ToolSummary): boolean {
|
|
203
203
|
if (summary.mutates) return false;
|
|
204
204
|
return summary.input.safeParse({}).success;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
/** Derive the MCP resource descriptor for a resource-eligible op. */
|
|
208
|
-
export function
|
|
208
|
+
export function toolToMcpResource(summary: ToolSummary): McpResource {
|
|
209
209
|
return {
|
|
210
210
|
uri: `${MCP_RESOURCE_PREFIX}${summary.name}`,
|
|
211
211
|
name: summary.name,
|
|
@@ -214,7 +214,7 @@ export function operationToResource(summary: OperationSummary): McpResource {
|
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
function schemaResources(summary:
|
|
217
|
+
function schemaResources(summary: ToolSummary): McpResource[] {
|
|
218
218
|
const resources: McpResource[] = [];
|
|
219
219
|
for (const direction of ['input', 'output'] as const) {
|
|
220
220
|
const full = fullOperationSchema(summary, direction);
|
|
@@ -267,7 +267,7 @@ function rpcError(
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
/**
|
|
270
|
-
* Wrap an `
|
|
270
|
+
* Wrap an `ToolOutcome` as an MCP tool-call result. MCP tool results are
|
|
271
271
|
* `{ content: [...], isError?, structuredContent? }`; we put the outcome JSON
|
|
272
272
|
* as a `text` content block (so a model sees it) AND — crucially for machine
|
|
273
273
|
* consumers and the contract test — pass it through verbatim in
|
|
@@ -275,10 +275,10 @@ function rpcError(
|
|
|
275
275
|
* tool result) so `error.code` survives unchanged. `isError` mirrors
|
|
276
276
|
* `!outcome.ok`.
|
|
277
277
|
*/
|
|
278
|
-
export function outcomeToToolResult(outcome:
|
|
278
|
+
export function outcomeToToolResult(outcome: ToolOutcome): {
|
|
279
279
|
content: { type: 'text'; text: string }[];
|
|
280
280
|
isError: boolean;
|
|
281
|
-
structuredContent:
|
|
281
|
+
structuredContent: ToolOutcome;
|
|
282
282
|
} {
|
|
283
283
|
return {
|
|
284
284
|
content: [{ type: 'text', text: JSON.stringify(outcome) }],
|
|
@@ -293,18 +293,18 @@ export function outcomeToToolResult(outcome: OperationOutcome): {
|
|
|
293
293
|
* transport (or a test) drives.
|
|
294
294
|
*/
|
|
295
295
|
export interface McpProjection {
|
|
296
|
-
registry:
|
|
296
|
+
registry: ToolRegistry;
|
|
297
297
|
listTools(): McpTool[];
|
|
298
298
|
listResources(): McpResource[];
|
|
299
|
-
callTool(name: string, args: unknown): Promise<
|
|
300
|
-
readResource(uri: string): Promise<
|
|
299
|
+
callTool(name: string, args: unknown): Promise<ToolOutcome>;
|
|
300
|
+
readResource(uri: string): Promise<ToolOutcome>;
|
|
301
301
|
handle(request: JsonRpcRequest): Promise<JsonRpcResponse>;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
export function createMcpProjection(options: McpProjectionOptions = {}): McpProjection {
|
|
305
305
|
const registry = options.registry ?? defaultRegistry;
|
|
306
306
|
|
|
307
|
-
async function resolveContext(): Promise<
|
|
307
|
+
async function resolveContext(): Promise<ToolContext> {
|
|
308
308
|
if (options.createContext) return options.createContext();
|
|
309
309
|
return options.context ?? {};
|
|
310
310
|
}
|
|
@@ -312,25 +312,25 @@ export function createMcpProjection(options: McpProjectionOptions = {}): McpProj
|
|
|
312
312
|
function listTools(): McpTool[] {
|
|
313
313
|
return registry
|
|
314
314
|
.listOperations()
|
|
315
|
-
.map(
|
|
315
|
+
.map(toolToMcpTool)
|
|
316
316
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
function listResources(): McpResource[] {
|
|
320
320
|
const summaries = registry.listOperations();
|
|
321
321
|
return [
|
|
322
|
-
...summaries.filter(isResourceEligible).map(
|
|
322
|
+
...summaries.filter(isResourceEligible).map(toolToMcpResource),
|
|
323
323
|
...summaries.flatMap(schemaResources),
|
|
324
324
|
].sort((a, b) => a.uri.localeCompare(b.uri));
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
async function callTool(name: string, args: unknown): Promise<
|
|
327
|
+
async function callTool(name: string, args: unknown): Promise<ToolOutcome> {
|
|
328
328
|
// dispatch already returns OPERATION_NOT_FOUND for an unknown name and
|
|
329
329
|
// INVALID_INPUT for a bad-shaped `args` — same codes as SDK/CLI/HTTP.
|
|
330
330
|
return registry.dispatch(name, args ?? {}, await resolveContext());
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
async function readResource(uri: string): Promise<
|
|
333
|
+
async function readResource(uri: string): Promise<ToolOutcome> {
|
|
334
334
|
if (uri.startsWith(MCP_SCHEMA_RESOURCE_PREFIX)) {
|
|
335
335
|
const match = /^vgai:\/\/schema\/(input|output)\/(.+)$/.exec(uri);
|
|
336
336
|
const summary = match
|
package/src/mcp/mcp-server.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This deliberately uses the official SDK's lower-level `Server` rather than
|
|
5
5
|
* re-registering operations through `McpServer.registerTool`. VGAI already has
|
|
6
|
-
* one authoritative validation/dispatch layer (`
|
|
6
|
+
* one authoritative validation/dispatch layer (`ToolRegistry`): letting
|
|
7
7
|
* the high-level helper pre-validate arguments would turn malformed operation
|
|
8
8
|
* input into an SDK protocol error before the registry could return the same
|
|
9
9
|
* structured `INVALID_INPUT` outcome as SDK/CLI/HTTP. The lower-level server
|
|
@@ -48,7 +48,7 @@ export const DEFAULT_MCP_INSTRUCTIONS =
|
|
|
48
48
|
'success. For an existing non-VGAI game, inspect first and never rewrite source without consent. ' +
|
|
49
49
|
'Read operations are safe to call for discovery. Before calling a write or destructive tool, ' +
|
|
50
50
|
'inspect its annotations and description, preserve project source, and use dryRun when the ' +
|
|
51
|
-
'tool offers it. Tool results are VGAI
|
|
51
|
+
'tool offers it. Tool results are VGAI ToolOutcome objects: {ok:true,data} on success or ' +
|
|
52
52
|
'{ok:false,error:{code,message,...}} on failure. Operations requiring an editor target the ' +
|
|
53
53
|
'live vgai edit session matching the project unless VGAI_EDITOR_URL/--url selected one explicitly.';
|
|
54
54
|
|
package/src/operations.ts
CHANGED
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
import { existsSync } from 'node:fs';
|
|
22
22
|
import { join } from 'node:path';
|
|
23
23
|
import { z } from 'zod';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { ToolError } from './errors.js';
|
|
25
|
+
import { defineTool, type ToolRegistry } from './registry.js';
|
|
26
26
|
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
// project.status — real, file-native (B2 project.* stand-in)
|
|
@@ -41,7 +41,7 @@ export const ProjectStatusResult = z
|
|
|
41
41
|
})
|
|
42
42
|
.describe('Discovery result for the project at ctx.projectRoot.');
|
|
43
43
|
|
|
44
|
-
export const projectStatus =
|
|
44
|
+
export const projectStatus = defineTool({
|
|
45
45
|
name: 'project.status',
|
|
46
46
|
summary: 'Report whether a vgai project manifest exists at the execution context project root.',
|
|
47
47
|
description:
|
|
@@ -64,7 +64,7 @@ export const projectStatus = defineOperation({
|
|
|
64
64
|
permission: { risk: 'read', summary: "Reads one file's existence under projectRoot; no writes." },
|
|
65
65
|
async impl(_input, ctx) {
|
|
66
66
|
if (!ctx.projectRoot) {
|
|
67
|
-
throw new
|
|
67
|
+
throw new ToolError(
|
|
68
68
|
'NO_PROJECT_ROOT',
|
|
69
69
|
'dispatch() was called without ctx.projectRoot.',
|
|
70
70
|
{},
|
|
@@ -78,6 +78,6 @@ export const projectStatus = defineOperation({
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
/** Register every B1 sample operation onto `registry`. */
|
|
81
|
-
export function
|
|
81
|
+
export function registerBuiltinTools(registry: ToolRegistry): void {
|
|
82
82
|
registry.register(projectStatus);
|
|
83
83
|
}
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
import { z } from 'zod';
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
37
|
+
import { ToolError } from '../errors.js';
|
|
38
|
+
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
39
39
|
import {
|
|
40
40
|
getPlayTransport,
|
|
41
41
|
PLAY_COMMAND_TIMEOUT_MS,
|
|
@@ -88,7 +88,7 @@ const PlaySeedSetInput = z.object({
|
|
|
88
88
|
seed: z.number().int().describe('Deterministic seed value to apply to the running play session.'),
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
export const playSeedSet =
|
|
91
|
+
export const playSeedSet = defineTool({
|
|
92
92
|
name: 'play.seed.set',
|
|
93
93
|
summary: 'Live-reseed a running play session (future draws only).',
|
|
94
94
|
description:
|
|
@@ -122,7 +122,7 @@ export const playSeedSet = defineOperation({
|
|
|
122
122
|
'play.seed.set',
|
|
123
123
|
).catch(() => undefined);
|
|
124
124
|
if (!result) {
|
|
125
|
-
throw new
|
|
125
|
+
throw new ToolError(
|
|
126
126
|
'SEED_CONTROL_UNSUPPORTED',
|
|
127
127
|
'The connected editor page predates the set-seed relay command — reload it.',
|
|
128
128
|
{},
|
|
@@ -132,9 +132,9 @@ export const playSeedSet = defineOperation({
|
|
|
132
132
|
const marker = relayErrorMarker(result);
|
|
133
133
|
const message = result.error ?? 'set-seed command failed';
|
|
134
134
|
if (marker?.code === 'DETERMINISM_NOT_DECLARED') {
|
|
135
|
-
throw new
|
|
135
|
+
throw new ToolError('DETERMINISM_NOT_DECLARED', message, {});
|
|
136
136
|
}
|
|
137
|
-
throw new
|
|
137
|
+
throw new ToolError('COMMAND_FAILED', message, { message });
|
|
138
138
|
}
|
|
139
139
|
return { ok: true as const };
|
|
140
140
|
},
|
|
@@ -152,7 +152,7 @@ const PlayTimeScaleSetInput = z.object({
|
|
|
152
152
|
.describe('Simulation time-scale to apply (clamped [0,8] to match GameLoop.timeScale).'),
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
export const playTimeScaleSet =
|
|
155
|
+
export const playTimeScaleSet = defineTool({
|
|
156
156
|
name: 'play.timeScale.set',
|
|
157
157
|
summary: "Set a running play session's simulation time-scale.",
|
|
158
158
|
description:
|
|
@@ -183,7 +183,7 @@ export const playTimeScaleSet = defineOperation({
|
|
|
183
183
|
'play.timeScale.set',
|
|
184
184
|
).catch(() => undefined);
|
|
185
185
|
if (!result) {
|
|
186
|
-
throw new
|
|
186
|
+
throw new ToolError(
|
|
187
187
|
'TIME_SCALE_CONTROL_UNSUPPORTED',
|
|
188
188
|
"The connected editor has no wire-level command to change a live play session's " +
|
|
189
189
|
'time-scale today.',
|
|
@@ -191,7 +191,7 @@ export const playTimeScaleSet = defineOperation({
|
|
|
191
191
|
);
|
|
192
192
|
}
|
|
193
193
|
if (!result.ok) {
|
|
194
|
-
throw new
|
|
194
|
+
throw new ToolError('COMMAND_FAILED', result.error ?? 'set-time-scale command failed', {
|
|
195
195
|
message: result.error ?? 'set-time-scale command failed',
|
|
196
196
|
});
|
|
197
197
|
}
|
|
@@ -199,7 +199,7 @@ export const playTimeScaleSet = defineOperation({
|
|
|
199
199
|
},
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
export function registerControlOperations(registry:
|
|
202
|
+
export function registerControlOperations(registry: ToolRegistry): void {
|
|
203
203
|
registry.register(playSeedSet);
|
|
204
204
|
registry.register(playTimeScaleSet);
|
|
205
205
|
}
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { z } from 'zod';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
25
|
+
import { ToolError } from '../errors.js';
|
|
26
|
+
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
27
27
|
import {
|
|
28
28
|
getPlayTransport,
|
|
29
29
|
PLAY_COMMAND_TIMEOUT_MS,
|
|
@@ -99,7 +99,7 @@ const PlayDebugCommandListResult = z.object({
|
|
|
99
99
|
.describe('Every debug command the running game registered, with locus and arg shapes.'),
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
export const playDebugCommandList =
|
|
102
|
+
export const playDebugCommandList = defineTool({
|
|
103
103
|
name: 'play.debugCommand.list',
|
|
104
104
|
summary: 'List the debug commands a running play session registers, with locus and arg shapes.',
|
|
105
105
|
description:
|
|
@@ -131,7 +131,7 @@ export const playDebugCommandList = defineOperation({
|
|
|
131
131
|
'play.debugCommand.list',
|
|
132
132
|
).catch(() => undefined);
|
|
133
133
|
if (result === undefined) {
|
|
134
|
-
throw new
|
|
134
|
+
throw new ToolError(
|
|
135
135
|
'DEBUG_COMMANDS_UNSUPPORTED',
|
|
136
136
|
'The connected editor page predates the debug-command relay commands — reload it.',
|
|
137
137
|
{},
|
|
@@ -139,7 +139,7 @@ export const playDebugCommandList = defineOperation({
|
|
|
139
139
|
}
|
|
140
140
|
if (!result.ok) {
|
|
141
141
|
const message = result.error ?? 'list-debug-commands command failed';
|
|
142
|
-
throw new
|
|
142
|
+
throw new ToolError('COMMAND_FAILED', message, { message });
|
|
143
143
|
}
|
|
144
144
|
const commands =
|
|
145
145
|
(
|
|
@@ -174,7 +174,7 @@ const PlayDebugCommandInvokeResult = z.object({
|
|
|
174
174
|
result: z.unknown().describe("The command's return value (JSON-serializable), when any."),
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
export const playDebugCommandInvoke =
|
|
177
|
+
export const playDebugCommandInvoke = defineTool({
|
|
178
178
|
name: 'play.debugCommand.invoke',
|
|
179
179
|
summary: 'Invoke a game-registered debug command in a running play session.',
|
|
180
180
|
description:
|
|
@@ -211,7 +211,7 @@ export const playDebugCommandInvoke = defineOperation({
|
|
|
211
211
|
'play.debugCommand.invoke',
|
|
212
212
|
).catch(() => undefined);
|
|
213
213
|
if (result === undefined) {
|
|
214
|
-
throw new
|
|
214
|
+
throw new ToolError(
|
|
215
215
|
'DEBUG_COMMANDS_UNSUPPORTED',
|
|
216
216
|
'The connected editor page predates the debug-command relay commands — reload it.',
|
|
217
217
|
{},
|
|
@@ -222,24 +222,24 @@ export const playDebugCommandInvoke = defineOperation({
|
|
|
222
222
|
const message = result.error ?? 'invoke-debug-command command failed';
|
|
223
223
|
switch (marker?.code) {
|
|
224
224
|
case 'DEBUG_COMMAND_NOT_REGISTERED':
|
|
225
|
-
throw new
|
|
225
|
+
throw new ToolError('DEBUG_COMMAND_NOT_REGISTERED', message, {
|
|
226
226
|
registered: relayStringArray(marker.data['registered']),
|
|
227
227
|
});
|
|
228
228
|
case 'DEBUG_COMMAND_ARGS_INVALID':
|
|
229
|
-
throw new
|
|
229
|
+
throw new ToolError('DEBUG_COMMAND_ARGS_INVALID', message, {
|
|
230
230
|
issues: Array.isArray(marker.data['issues']) ? marker.data['issues'] : [],
|
|
231
231
|
});
|
|
232
232
|
case 'DEBUG_COMMAND_FAILED':
|
|
233
|
-
throw new
|
|
233
|
+
throw new ToolError('DEBUG_COMMAND_FAILED', message, marker.data);
|
|
234
234
|
default:
|
|
235
|
-
throw new
|
|
235
|
+
throw new ToolError('COMMAND_FAILED', message, { message });
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
return { result: (result.data as { result?: unknown } | undefined)?.result };
|
|
239
239
|
},
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
-
export function registerDebugCommandOperations(registry:
|
|
242
|
+
export function registerDebugCommandOperations(registry: ToolRegistry): void {
|
|
243
243
|
registry.register(playDebugCommandList);
|
|
244
244
|
registry.register(playDebugCommandInvoke);
|
|
245
245
|
}
|