@skenora/tooling 0.1.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.
Files changed (44) hide show
  1. package/README.md +32 -0
  2. package/bin/skenora-mcp-http.mjs +131 -0
  3. package/bin/skenora-mcp.mjs +50 -0
  4. package/bin/skenora.mjs +388 -0
  5. package/dist/doctor.d.ts +3 -0
  6. package/dist/doctor.d.ts.map +1 -0
  7. package/dist/doctor.js +109 -0
  8. package/dist/doctor.js.map +1 -0
  9. package/dist/examples.d.ts +26 -0
  10. package/dist/examples.d.ts.map +1 -0
  11. package/dist/examples.js +207 -0
  12. package/dist/examples.js.map +1 -0
  13. package/dist/index.d.ts +8 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +7 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/mcp-http.d.ts +16 -0
  18. package/dist/mcp-http.d.ts.map +1 -0
  19. package/dist/mcp-http.js +279 -0
  20. package/dist/mcp-http.js.map +1 -0
  21. package/dist/mcp.d.ts +79 -0
  22. package/dist/mcp.d.ts.map +1 -0
  23. package/dist/mcp.js +817 -0
  24. package/dist/mcp.js.map +1 -0
  25. package/dist/resources.d.ts +24 -0
  26. package/dist/resources.d.ts.map +1 -0
  27. package/dist/resources.js +141 -0
  28. package/dist/resources.js.map +1 -0
  29. package/dist/wgsl.d.ts +42 -0
  30. package/dist/wgsl.d.ts.map +1 -0
  31. package/dist/wgsl.js +133 -0
  32. package/dist/wgsl.js.map +1 -0
  33. package/package.json +53 -0
  34. package/resources/docs/diagnostics.md +16 -0
  35. package/resources/docs/overview.md +26 -0
  36. package/resources/docs/runtime-probe.md +17 -0
  37. package/resources/examples/gallery.json +1818 -0
  38. package/resources/examples/invalid-scene.json +9 -0
  39. package/resources/examples/valid-scene.json +69 -0
  40. package/resources/llms-full.txt +52 -0
  41. package/resources/llms.txt +19 -0
  42. package/resources/manifest.json +97 -0
  43. package/resources/schemas/example-manifest.schema.json +454 -0
  44. package/resources/schemas/scene-check-report.schema.json +36 -0
package/dist/mcp.js ADDED
@@ -0,0 +1,817 @@
1
+ import { createHash } from "node:crypto";
2
+ import { checkSceneInput } from "@skenora/scene-plan";
3
+ import { createDoctorReport } from "./doctor.js";
4
+ import { getInstalledSkenoraExample, getInstalledSkenoraExampleSource, listInstalledSkenoraExamples, searchInstalledSkenoraExamples, } from "./examples.js";
5
+ import { getToolingResource, listToolingResources, searchToolingResources, } from "./resources.js";
6
+ export const SKENORA_MCP_PROTOCOL_VERSION = "2026-07-28";
7
+ export const SKENORA_MCP_LEGACY_PROTOCOL_VERSION = "2025-11-25";
8
+ const DEFAULT_SERVER_INFO = Object.freeze({
9
+ name: "skenora-readonly",
10
+ version: "0.1.2",
11
+ });
12
+ const MODERN_PROTOCOL_VERSIONS = Object.freeze([SKENORA_MCP_PROTOCOL_VERSION]);
13
+ const PUBLIC_CACHE = Object.freeze({
14
+ ttlMs: 3_600_000,
15
+ cacheScope: "public",
16
+ });
17
+ const PRIVATE_CACHE = Object.freeze({
18
+ ttlMs: 3_600_000,
19
+ cacheScope: "private",
20
+ });
21
+ const ASSET_KINDS = Object.freeze([
22
+ "model",
23
+ "texture",
24
+ "environment",
25
+ "audio",
26
+ "data",
27
+ ]);
28
+ export function createSkenoraMcpServer(options = {}) {
29
+ if (options.scenePlans &&
30
+ typeof options.scenePlans.authorize !== "function") {
31
+ throw new Error("ScenePlan MCP authoring requires an authorize callback");
32
+ }
33
+ const serverInfo = Object.freeze({
34
+ ...(options.scenePlans
35
+ ? { name: "skenora-authoring", version: "0.1.2" }
36
+ : DEFAULT_SERVER_INFO),
37
+ ...options.serverInfo,
38
+ });
39
+ const tools = Object.freeze([
40
+ ...READ_ONLY_TOOLS,
41
+ ...(options.scenePlans ? SCENE_PLAN_TOOLS : []),
42
+ ]);
43
+ return Object.freeze({
44
+ serverInfo,
45
+ authoring: options.scenePlans !== undefined,
46
+ async handle(input, context = { transport: "in-process" }) {
47
+ if (!isMcpRequest(input)) {
48
+ return createSkenoraMcpErrorResponse(requestId(input), -32600, "Invalid Request");
49
+ }
50
+ if (input.id === undefined)
51
+ return null;
52
+ if (isModernContext(context)) {
53
+ const issue = validateSkenoraMcpModernRequest(input);
54
+ if (issue) {
55
+ return createSkenoraMcpErrorResponse(input.id, issue.code, issue.message, issue.data);
56
+ }
57
+ }
58
+ else if (context.protocolVersion === SKENORA_MCP_LEGACY_PROTOCOL_VERSION &&
59
+ getSkenoraMcpRequestProtocolVersion(input) !== undefined) {
60
+ return createSkenoraMcpErrorResponse(input.id, -32022, "This MCP connection is pinned to the legacy protocol era", { supportedVersions: [SKENORA_MCP_LEGACY_PROTOCOL_VERSION] });
61
+ }
62
+ try {
63
+ const result = await handleRequest(input, context, serverInfo, tools, options.scenePlans);
64
+ return { jsonrpc: "2.0", id: input.id, result };
65
+ }
66
+ catch (error) {
67
+ return createSkenoraMcpErrorResponse(input.id, error instanceof SkenoraMcpError ? error.code : -32603, error instanceof Error ? error.message : "Internal error", error instanceof SkenoraMcpError ? error.data : undefined);
68
+ }
69
+ },
70
+ });
71
+ }
72
+ export function createSkenoraMcpErrorResponse(id, code, message, data) {
73
+ return {
74
+ jsonrpc: "2.0",
75
+ id,
76
+ error: Object.freeze({
77
+ code,
78
+ message,
79
+ ...(data === undefined ? {} : { data }),
80
+ }),
81
+ };
82
+ }
83
+ export function getSkenoraMcpRequestProtocolVersion(input) {
84
+ if (!isRecord(input) || !isRecord(input.params))
85
+ return undefined;
86
+ const meta = input.params._meta;
87
+ if (!isRecord(meta))
88
+ return undefined;
89
+ const version = meta["io.modelcontextprotocol/protocolVersion"];
90
+ return typeof version === "string" ? version : undefined;
91
+ }
92
+ export function validateSkenoraMcpModernRequest(input) {
93
+ if (!isRecord(input) || typeof input.method !== "string")
94
+ return null;
95
+ if (!isRecord(input.params) || !isRecord(input.params._meta)) {
96
+ return protocolIssue(-32602, "Modern MCP requests require params._meta");
97
+ }
98
+ const meta = input.params._meta;
99
+ const protocolVersion = meta["io.modelcontextprotocol/protocolVersion"];
100
+ if (typeof protocolVersion !== "string") {
101
+ return protocolIssue(-32602, "Modern MCP requests require a protocol version in params._meta");
102
+ }
103
+ if (protocolVersion !== SKENORA_MCP_PROTOCOL_VERSION) {
104
+ return protocolIssue(-32022, `Unsupported MCP protocol version: ${protocolVersion}`, { supportedVersions: MODERN_PROTOCOL_VERSIONS });
105
+ }
106
+ const clientCapabilities = meta["io.modelcontextprotocol/clientCapabilities"];
107
+ if (!isRecord(clientCapabilities)) {
108
+ return protocolIssue(-32602, "Modern MCP requests require client capabilities in params._meta");
109
+ }
110
+ const clientInfo = meta["io.modelcontextprotocol/clientInfo"];
111
+ if (clientInfo !== undefined &&
112
+ (!isRecord(clientInfo) ||
113
+ typeof clientInfo.name !== "string" ||
114
+ !clientInfo.name ||
115
+ typeof clientInfo.version !== "string" ||
116
+ !clientInfo.version)) {
117
+ return protocolIssue(-32602, "Invalid MCP clientInfo metadata");
118
+ }
119
+ return null;
120
+ }
121
+ export function createSkenoraMcpPlanHash(input) {
122
+ const normalized = parseJsonString(input);
123
+ const canonical = JSON.stringify(canonicalizeJson(normalized));
124
+ if (canonical === undefined) {
125
+ throw new Error("MCP plans must be JSON-compatible values");
126
+ }
127
+ return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
128
+ }
129
+ async function handleRequest(request, context, serverInfo, tools, scenePlans) {
130
+ const { method, params } = request;
131
+ const modern = isModernContext(context);
132
+ if (method === "initialize" && modern) {
133
+ throw new SkenoraMcpError(-32601, "Method not found: initialize");
134
+ }
135
+ if (method === "server/discover" && !modern) {
136
+ throw new SkenoraMcpError(-32601, "Method not found: server/discover");
137
+ }
138
+ switch (method) {
139
+ case "server/discover":
140
+ return complete({
141
+ ...PUBLIC_CACHE,
142
+ supportedVersions: MODERN_PROTOCOL_VERSIONS,
143
+ capabilities: { resources: {}, tools: {} },
144
+ instructions: scenePlans
145
+ ? "Skenora docs, examples, pure scene checks, and authorized ScenePlan authoring. Every scene call requires an explicit editorHandle."
146
+ : "Read-only Skenora docs, examples, schemas, pure scene checks, and doctor reports.",
147
+ }, serverInfo, context);
148
+ case "initialize":
149
+ return {
150
+ protocolVersion: negotiateLegacyVersion(params?.protocolVersion),
151
+ capabilities: { resources: {}, tools: {} },
152
+ serverInfo,
153
+ instructions: scenePlans
154
+ ? "Authorized Skenora ScenePlan authoring plus read-only developer resources."
155
+ : "Read-only Skenora docs, examples, schemas, pure scene checks, and doctor reports.",
156
+ };
157
+ case "ping":
158
+ if (modern) {
159
+ throw new SkenoraMcpError(-32601, "Method not found: ping");
160
+ }
161
+ return {};
162
+ case "resources/list": {
163
+ const entries = await listToolingResources();
164
+ return complete({
165
+ ...PUBLIC_CACHE,
166
+ resources: entries.map((entry) => ({
167
+ uri: resourceUri(entry.id),
168
+ name: entry.id,
169
+ title: entry.title,
170
+ mimeType: entry.mediaType,
171
+ description: `${entry.kind} v${entry.version}`,
172
+ })),
173
+ }, serverInfo, context);
174
+ }
175
+ case "resources/read": {
176
+ const id = parseResourceUri(requireString(params?.uri, "uri"));
177
+ const resource = await getToolingResource(id);
178
+ return complete({
179
+ ...PUBLIC_CACHE,
180
+ contents: [
181
+ {
182
+ uri: resourceUri(id),
183
+ mimeType: resource.entry.mediaType,
184
+ text: resource.text,
185
+ },
186
+ ],
187
+ }, serverInfo, context);
188
+ }
189
+ case "tools/list":
190
+ return complete({
191
+ ...(scenePlans ? PRIVATE_CACHE : PUBLIC_CACHE),
192
+ tools,
193
+ }, serverInfo, context);
194
+ case "tools/call":
195
+ return callTool(requireString(params?.name, "name"), params?.arguments, context, serverInfo, tools, scenePlans);
196
+ default:
197
+ throw new SkenoraMcpError(-32601, `Method not found: ${method}`);
198
+ }
199
+ }
200
+ async function callTool(name, input, context, serverInfo, tools, scenePlans) {
201
+ if (!tools.some((tool) => tool.name === name)) {
202
+ throw new SkenoraMcpError(-32602, `Unknown tool: ${name}`);
203
+ }
204
+ try {
205
+ const result = await executeTool(name, input, context, scenePlans);
206
+ return complete({
207
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
208
+ structuredContent: result,
209
+ isError: false,
210
+ }, serverInfo, context);
211
+ }
212
+ catch (error) {
213
+ return complete({
214
+ content: [
215
+ {
216
+ type: "text",
217
+ text: error instanceof Error ? error.message : "Tool call failed",
218
+ },
219
+ ],
220
+ isError: true,
221
+ }, serverInfo, context);
222
+ }
223
+ }
224
+ async function executeTool(name, rawInput, context, scenePlans) {
225
+ const input = optionalRecord(rawInput, "arguments");
226
+ switch (name) {
227
+ case "skenora_check_scene":
228
+ return checkSceneInput(input.input, {
229
+ ...(input.inputKind === undefined
230
+ ? {}
231
+ : {
232
+ inputKind: requireEnum(input.inputKind, "inputKind", [
233
+ "auto",
234
+ "scene",
235
+ "blueprint",
236
+ "patch",
237
+ ]),
238
+ }),
239
+ compile: input.compile === undefined
240
+ ? true
241
+ : requireBoolean(input.compile, "compile"),
242
+ });
243
+ case "skenora_search_resources":
244
+ return searchToolingResources(requireString(input.query, "query"), {
245
+ ...(input.kind === undefined
246
+ ? {}
247
+ : {
248
+ kind: requireEnum(input.kind, "kind", [
249
+ "documentation",
250
+ "example",
251
+ "llms",
252
+ "schema",
253
+ ]),
254
+ }),
255
+ ...(input.limit === undefined
256
+ ? {}
257
+ : { limit: requireInteger(input.limit, "limit", 1, 100) }),
258
+ });
259
+ case "skenora_get_resource": {
260
+ const resource = await getToolingResource(requireString(input.id, "id"));
261
+ return { ...resource.entry, text: resource.text };
262
+ }
263
+ case "skenora_list_examples":
264
+ return listInstalledSkenoraExamples(exampleOptions(input));
265
+ case "skenora_search_examples":
266
+ return searchInstalledSkenoraExamples(requireString(input.query, "query"), exampleOptions(input));
267
+ case "skenora_get_example":
268
+ return getInstalledSkenoraExample(requireString(input.id, "id"));
269
+ case "skenora_get_example_source":
270
+ return getInstalledSkenoraExampleSource(requireString(input.id, "id"), requireString(input.path, "path"));
271
+ case "skenora_doctor":
272
+ return createDoctorReport(input.probe);
273
+ case "skenora_inspect_scene": {
274
+ const options = compact({
275
+ cursor: optionalString(input.cursor, "cursor"),
276
+ limit: input.limit === undefined
277
+ ? undefined
278
+ : requireInteger(input.limit, "limit", 1, 500),
279
+ });
280
+ const access = await scenePlanAccess(scenePlans, name, "read", input, context);
281
+ return access.gateway.inspectScene(options);
282
+ }
283
+ case "skenora_search_scene_resources": {
284
+ const query = compact({
285
+ query: optionalString(input.query, "query"),
286
+ kind: input.kind === undefined
287
+ ? undefined
288
+ : requireEnum(input.kind, "kind", ASSET_KINDS),
289
+ tags: optionalStringArray(input.tags, "tags"),
290
+ providerTypes: optionalStringArray(input.providerTypes, "providerTypes"),
291
+ limit: input.limit === undefined
292
+ ? undefined
293
+ : requireInteger(input.limit, "limit", 1, 100),
294
+ cursor: optionalString(input.cursor, "cursor"),
295
+ });
296
+ const access = await scenePlanAccess(scenePlans, name, "read", input, context);
297
+ return access.gateway.searchResources(query);
298
+ }
299
+ case "skenora_validate_plan": {
300
+ requirePresent(input.plan, "plan");
301
+ const allowVariables = optionalBoolean(input.allowVariables, "allowVariables");
302
+ const access = await scenePlanAccess(scenePlans, name, "read", input, context);
303
+ return access.gateway.validatePlan(input.plan, {
304
+ allowVariables: allowVariables ?? false,
305
+ });
306
+ }
307
+ case "skenora_apply_plan": {
308
+ requirePresent(input.plan, "plan");
309
+ const expectedRevisionToken = revisionToken(input.expectedRevisionToken);
310
+ const idempotencyKey = requireToken(input.idempotencyKey, "idempotencyKey");
311
+ const operationId = optionalToken(input.operationId, "operationId");
312
+ const label = optionalString(input.label, "label");
313
+ const allowVariables = optionalBoolean(input.allowVariables, "allowVariables");
314
+ const writeIntent = Object.freeze({
315
+ planHash: createSkenoraMcpPlanHash(input.plan),
316
+ expectedRevisionToken,
317
+ idempotencyKey,
318
+ ...(operationId === undefined ? {} : { operationId }),
319
+ ...(label === undefined ? {} : { label }),
320
+ allowVariables: allowVariables ?? false,
321
+ modelLoadPolicy: "strict",
322
+ });
323
+ const access = await scenePlanAccess(scenePlans, name, "write", input, context, writeIntent);
324
+ const operationNamespace = createOperationNamespace(context, access.editorHandle);
325
+ return access.gateway.applyPlan(input.plan, {
326
+ expectedRevisionToken,
327
+ idempotencyKey,
328
+ modelLoadPolicy: "strict",
329
+ ...(operationId === undefined ? {} : { operationId }),
330
+ ...(label === undefined ? {} : { label }),
331
+ allowVariables: allowVariables ?? false,
332
+ operationNamespace,
333
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
334
+ });
335
+ }
336
+ case "skenora_wait_operation": {
337
+ const operationId = requireToken(input.operationId, "operationId");
338
+ const access = await scenePlanAccess(scenePlans, name, "read", input, context);
339
+ const operation = access.gateway.waitOperation(operationId, {
340
+ operationNamespace: createOperationNamespace(context, access.editorHandle),
341
+ });
342
+ if (!operation) {
343
+ throw new Error(`Unknown ScenePlan operation: ${operationId}`);
344
+ }
345
+ return operation;
346
+ }
347
+ default:
348
+ throw new SkenoraMcpError(-32602, `Unknown tool: ${name}`);
349
+ }
350
+ }
351
+ async function scenePlanAccess(host, toolName, access, input, context, writeIntent) {
352
+ if (!host)
353
+ throw new Error("ScenePlan authoring is not configured");
354
+ if (context.signal?.aborted) {
355
+ throw new DOMException("The MCP request was cancelled", "AbortError");
356
+ }
357
+ if (access === "write" && writeIntent === undefined) {
358
+ throw new Error("ScenePlan writes require a bound write intent");
359
+ }
360
+ const editorHandle = requireToken(input.editorHandle, "editorHandle");
361
+ const approvalToken = optionalToken(input.approvalToken, "approvalToken");
362
+ try {
363
+ await host.authorize({
364
+ toolName,
365
+ access,
366
+ editorHandle,
367
+ ...(approvalToken === undefined ? {} : { approvalToken }),
368
+ ...(writeIntent === undefined ? {} : { writeIntent }),
369
+ }, context);
370
+ }
371
+ catch {
372
+ throw new Error("ScenePlan access denied");
373
+ }
374
+ if (context.signal?.aborted) {
375
+ throw new DOMException("The MCP request was cancelled", "AbortError");
376
+ }
377
+ let gateway;
378
+ try {
379
+ gateway = await host.resolve(editorHandle, context);
380
+ }
381
+ catch {
382
+ throw new Error("ScenePlan editor is unavailable");
383
+ }
384
+ if (!gateway)
385
+ throw new Error("Unknown or unavailable editorHandle");
386
+ return { editorHandle, gateway };
387
+ }
388
+ const READ_ONLY_TOOLS = Object.freeze([
389
+ {
390
+ name: "skenora_check_scene",
391
+ title: "Check Skenora scene input",
392
+ description: "Purely validate a native SceneDocument, SceneBlueprint, or ScenePatch. Does not mutate runtime or files.",
393
+ inputSchema: {
394
+ type: "object",
395
+ additionalProperties: false,
396
+ required: ["input"],
397
+ properties: {
398
+ input: {},
399
+ inputKind: { enum: ["auto", "scene", "blueprint", "patch"] },
400
+ compile: { type: "boolean" },
401
+ },
402
+ },
403
+ annotations: { readOnlyHint: true },
404
+ },
405
+ {
406
+ name: "skenora_search_resources",
407
+ title: "Search installed Skenora resources",
408
+ description: "Search offline documentation, examples, schemas, and llms files.",
409
+ inputSchema: {
410
+ type: "object",
411
+ additionalProperties: false,
412
+ required: ["query"],
413
+ properties: {
414
+ query: { type: "string" },
415
+ kind: { enum: ["documentation", "example", "llms", "schema"] },
416
+ limit: { type: "integer", minimum: 1, maximum: 100 },
417
+ },
418
+ },
419
+ annotations: { readOnlyHint: true },
420
+ },
421
+ {
422
+ name: "skenora_get_resource",
423
+ title: "Read an installed Skenora resource",
424
+ description: "Read one versioned offline resource by manifest ID.",
425
+ inputSchema: {
426
+ type: "object",
427
+ additionalProperties: false,
428
+ required: ["id"],
429
+ properties: { id: { type: "string" } },
430
+ },
431
+ annotations: { readOnlyHint: true },
432
+ },
433
+ {
434
+ name: "skenora_list_examples",
435
+ title: "List installed Skenora examples",
436
+ description: "List immutable, package-versioned public examples and their declared evidence without writing files.",
437
+ inputSchema: exampleSearchSchema(false),
438
+ annotations: { readOnlyHint: true },
439
+ },
440
+ {
441
+ name: "skenora_search_examples",
442
+ title: "Search installed Skenora examples",
443
+ description: "Search immutable example manifests and text sources in the installed offline catalog.",
444
+ inputSchema: exampleSearchSchema(true),
445
+ annotations: { readOnlyHint: true },
446
+ },
447
+ {
448
+ name: "skenora_get_example",
449
+ title: "Read an installed Skenora example",
450
+ description: "Read one detached example manifest and its verified source inventory.",
451
+ inputSchema: {
452
+ type: "object",
453
+ additionalProperties: false,
454
+ required: ["id"],
455
+ properties: { id: { type: "string" } },
456
+ },
457
+ annotations: { readOnlyHint: true },
458
+ },
459
+ {
460
+ name: "skenora_get_example_source",
461
+ title: "Read installed Skenora example source",
462
+ description: "Read one integrity-checked example source as UTF-8 text or base64. Never writes files.",
463
+ inputSchema: {
464
+ type: "object",
465
+ additionalProperties: false,
466
+ required: ["id", "path"],
467
+ properties: { id: { type: "string" }, path: { type: "string" } },
468
+ },
469
+ annotations: { readOnlyHint: true },
470
+ },
471
+ {
472
+ name: "skenora_doctor",
473
+ title: "Create Skenora doctor report",
474
+ description: "Report environment status. Healthy requires an unmodified browser Runtime probe with render/readback evidence.",
475
+ inputSchema: {
476
+ type: "object",
477
+ additionalProperties: false,
478
+ properties: { probe: { type: "object" } },
479
+ },
480
+ annotations: { readOnlyHint: true },
481
+ },
482
+ ]);
483
+ const SCENE_PLAN_TOOLS = Object.freeze([
484
+ {
485
+ name: "skenora_inspect_scene",
486
+ title: "Inspect an active Skenora editor",
487
+ description: "Return a compact scene summary, a paginated model-safe scene outline, opaque revision token, declared capabilities, and observed availability for one authorized editor handle.",
488
+ inputSchema: editorHandleSchema({
489
+ cursor: { type: "string" },
490
+ limit: { type: "integer", minimum: 1, maximum: 500 },
491
+ }),
492
+ annotations: { readOnlyHint: true },
493
+ },
494
+ {
495
+ name: "skenora_search_scene_resources",
496
+ title: "Search resources for an active Skenora editor",
497
+ description: "Search the editor host's authorized resource providers and return model-visible descriptors without raw locators or credentials.",
498
+ inputSchema: editorHandleSchema({
499
+ query: { type: "string" },
500
+ kind: { enum: ASSET_KINDS },
501
+ tags: { type: "array", items: { type: "string" } },
502
+ providerTypes: { type: "array", items: { type: "string" } },
503
+ limit: { type: "integer", minimum: 1, maximum: 100 },
504
+ cursor: { type: "string" },
505
+ }),
506
+ annotations: { readOnlyHint: true },
507
+ },
508
+ {
509
+ name: "skenora_validate_plan",
510
+ title: "Validate a Skenora SceneBlueprint or ScenePatch",
511
+ description: "Validate and compile a plan against the active editor revision, capabilities, limits, and discovered logical resources without mutation.",
512
+ inputSchema: editorHandleSchema({
513
+ plan: {},
514
+ allowVariables: { type: "boolean" },
515
+ }, ["plan"]),
516
+ annotations: { readOnlyHint: true },
517
+ },
518
+ {
519
+ name: "skenora_apply_plan",
520
+ title: "Apply a Skenora SceneBlueprint or ScenePatch",
521
+ description: "Apply one authorized semantic plan with an opaque revision token, strict Runtime projection, and an idempotency key. The host may require approvalToken.",
522
+ inputSchema: editorHandleSchema({
523
+ plan: {},
524
+ expectedRevisionToken: revisionTokenSchema(),
525
+ idempotencyKey: { type: "string", minLength: 1 },
526
+ operationId: { type: "string", minLength: 1 },
527
+ label: { type: "string", minLength: 1 },
528
+ approvalToken: { type: "string", minLength: 1 },
529
+ allowVariables: { type: "boolean" },
530
+ }, ["plan", "expectedRevisionToken", "idempotencyKey"]),
531
+ annotations: {
532
+ readOnlyHint: false,
533
+ destructiveHint: true,
534
+ idempotentHint: true,
535
+ },
536
+ },
537
+ {
538
+ name: "skenora_wait_operation",
539
+ title: "Wait for a Skenora ScenePlan operation",
540
+ description: "Wait for the terminal result of an operation owned by the selected editor handle.",
541
+ inputSchema: editorHandleSchema({ operationId: { type: "string", minLength: 1 } }, ["operationId"]),
542
+ annotations: { readOnlyHint: true },
543
+ },
544
+ ]);
545
+ function editorHandleSchema(properties = {}, required = []) {
546
+ return {
547
+ type: "object",
548
+ additionalProperties: false,
549
+ required: ["editorHandle", ...required],
550
+ properties: {
551
+ editorHandle: { type: "string", minLength: 1 },
552
+ ...properties,
553
+ },
554
+ };
555
+ }
556
+ function revisionTokenSchema() {
557
+ return {
558
+ type: "object",
559
+ additionalProperties: false,
560
+ required: ["sceneId", "sessionEpoch", "revision", "documentHash"],
561
+ properties: {
562
+ sceneId: { type: "string", minLength: 1 },
563
+ sessionEpoch: { type: "string", minLength: 1 },
564
+ revision: { type: "integer", minimum: 0 },
565
+ documentHash: { type: "string", minLength: 1 },
566
+ },
567
+ };
568
+ }
569
+ function exampleSearchSchema(requireQuery) {
570
+ return {
571
+ type: "object",
572
+ additionalProperties: false,
573
+ required: requireQuery ? ["query"] : [],
574
+ properties: {
575
+ ...(requireQuery ? { query: { type: "string" } } : {}),
576
+ mode: { enum: ["blueprint", "recipe", "renderer-lab"] },
577
+ tier: {
578
+ enum: [
579
+ "foundation",
580
+ "bounded-effects",
581
+ "materials",
582
+ "render-graph",
583
+ "compute",
584
+ "advanced-simulation",
585
+ ],
586
+ },
587
+ tag: { type: "string" },
588
+ limit: { type: "integer", minimum: 1, maximum: 100 },
589
+ },
590
+ };
591
+ }
592
+ function exampleOptions(input) {
593
+ return {
594
+ ...(input.mode === undefined
595
+ ? {}
596
+ : {
597
+ mode: requireEnum(input.mode, "mode", [
598
+ "blueprint",
599
+ "recipe",
600
+ "renderer-lab",
601
+ ]),
602
+ }),
603
+ ...(input.tier === undefined
604
+ ? {}
605
+ : {
606
+ tier: requireEnum(input.tier, "tier", [
607
+ "foundation",
608
+ "bounded-effects",
609
+ "materials",
610
+ "render-graph",
611
+ "compute",
612
+ "advanced-simulation",
613
+ ]),
614
+ }),
615
+ ...(input.tag === undefined
616
+ ? {}
617
+ : { tag: requireString(input.tag, "tag") }),
618
+ ...(input.limit === undefined
619
+ ? {}
620
+ : { limit: requireInteger(input.limit, "limit", 1, 100) }),
621
+ };
622
+ }
623
+ function complete(result, serverInfo, context) {
624
+ if (!isModernContext(context)) {
625
+ const legacy = { ...result };
626
+ delete legacy.ttlMs;
627
+ delete legacy.cacheScope;
628
+ return legacy;
629
+ }
630
+ return {
631
+ resultType: "complete",
632
+ ...result,
633
+ _meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
634
+ };
635
+ }
636
+ function isModernContext(context) {
637
+ return context.protocolVersion === SKENORA_MCP_PROTOCOL_VERSION;
638
+ }
639
+ function protocolIssue(code, message, data) {
640
+ return Object.freeze({
641
+ code,
642
+ message,
643
+ ...(data === undefined ? {} : { data }),
644
+ });
645
+ }
646
+ function parseJsonString(input) {
647
+ if (typeof input !== "string")
648
+ return input;
649
+ try {
650
+ return JSON.parse(input);
651
+ }
652
+ catch {
653
+ return input;
654
+ }
655
+ }
656
+ function canonicalizeJson(value, ancestors = new Set()) {
657
+ if (value === null ||
658
+ typeof value === "string" ||
659
+ typeof value === "boolean") {
660
+ return value;
661
+ }
662
+ if (typeof value === "number") {
663
+ if (!Number.isFinite(value)) {
664
+ throw new Error("MCP plans must contain finite JSON numbers");
665
+ }
666
+ return Object.is(value, -0) ? 0 : value;
667
+ }
668
+ if (typeof value !== "object") {
669
+ throw new Error("MCP plans must be JSON-compatible values");
670
+ }
671
+ if (ancestors.has(value)) {
672
+ throw new Error("MCP plans must not contain circular references");
673
+ }
674
+ ancestors.add(value);
675
+ try {
676
+ if (Array.isArray(value)) {
677
+ return value.map((item) => canonicalizeJson(item, ancestors));
678
+ }
679
+ return Object.fromEntries(Object.entries(value)
680
+ .sort(([left], [right]) => left.localeCompare(right))
681
+ .map(([key, child]) => [key, canonicalizeJson(child, ancestors)]));
682
+ }
683
+ finally {
684
+ ancestors.delete(value);
685
+ }
686
+ }
687
+ function createOperationNamespace(context, editorHandle) {
688
+ const subject = context.principal?.subject ?? `${context.transport}:local`;
689
+ return `mcp:${createHash("sha256")
690
+ .update(JSON.stringify([subject, editorHandle]))
691
+ .digest("hex")}`;
692
+ }
693
+ function negotiateLegacyVersion(requested) {
694
+ return requested === SKENORA_MCP_LEGACY_PROTOCOL_VERSION
695
+ ? requested
696
+ : SKENORA_MCP_LEGACY_PROTOCOL_VERSION;
697
+ }
698
+ function resourceUri(id) {
699
+ return `skenora://resources/${encodeURIComponent(id)}`;
700
+ }
701
+ function parseResourceUri(uri) {
702
+ const prefix = "skenora://resources/";
703
+ if (!uri.startsWith(prefix)) {
704
+ throw new SkenoraMcpError(-32602, "Invalid resource URI");
705
+ }
706
+ return decodeURIComponent(uri.slice(prefix.length));
707
+ }
708
+ function revisionToken(input) {
709
+ const value = requireRecord(input, "expectedRevisionToken");
710
+ return Object.freeze({
711
+ sceneId: requireToken(value.sceneId, "expectedRevisionToken.sceneId"),
712
+ sessionEpoch: requireToken(value.sessionEpoch, "expectedRevisionToken.sessionEpoch"),
713
+ revision: requireInteger(value.revision, "expectedRevisionToken.revision", 0, Number.MAX_SAFE_INTEGER),
714
+ documentHash: requireToken(value.documentHash, "expectedRevisionToken.documentHash"),
715
+ });
716
+ }
717
+ function optionalRecord(value, name) {
718
+ return value === undefined ? {} : requireRecord(value, name);
719
+ }
720
+ function requireRecord(value, name) {
721
+ if (!isRecord(value))
722
+ throw new Error(`${name} must be an object`);
723
+ return value;
724
+ }
725
+ function requireString(value, name) {
726
+ if (typeof value !== "string" || !value) {
727
+ throw new SkenoraMcpError(-32602, `${name} must be a non-empty string`);
728
+ }
729
+ return value;
730
+ }
731
+ function optionalString(value, name) {
732
+ return value === undefined ? undefined : requireString(value, name);
733
+ }
734
+ function requireToken(value, name) {
735
+ const token = requireString(value, name);
736
+ if (token !== token.trim()) {
737
+ throw new Error(`${name} must not contain surrounding whitespace`);
738
+ }
739
+ return token;
740
+ }
741
+ function optionalToken(value, name) {
742
+ return value === undefined ? undefined : requireToken(value, name);
743
+ }
744
+ function requireBoolean(value, name) {
745
+ if (typeof value !== "boolean")
746
+ throw new Error(`${name} must be a boolean`);
747
+ return value;
748
+ }
749
+ function optionalBoolean(value, name) {
750
+ return value === undefined ? undefined : requireBoolean(value, name);
751
+ }
752
+ function requireInteger(value, name, minimum, maximum) {
753
+ if (typeof value !== "number" ||
754
+ !Number.isInteger(value) ||
755
+ value < minimum ||
756
+ value > maximum) {
757
+ throw new Error(`${name} must be an integer in ${minimum}..${maximum}`);
758
+ }
759
+ return value;
760
+ }
761
+ function requireEnum(value, name, allowed) {
762
+ if (typeof value !== "string" || !allowed.includes(value)) {
763
+ throw new Error(`${name} must be one of: ${allowed.join(", ")}`);
764
+ }
765
+ return value;
766
+ }
767
+ function optionalStringArray(value, name) {
768
+ if (value === undefined)
769
+ return undefined;
770
+ if (!Array.isArray(value) ||
771
+ !value.every((item) => typeof item === "string")) {
772
+ throw new Error(`${name} must be an array of strings`);
773
+ }
774
+ return Object.freeze([...value]);
775
+ }
776
+ function requirePresent(value, name) {
777
+ if (value === undefined)
778
+ throw new Error(`${name} is required`);
779
+ }
780
+ function compact(value) {
781
+ return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== undefined));
782
+ }
783
+ function isMcpRequest(value) {
784
+ if (!isRecord(value) ||
785
+ value.jsonrpc !== "2.0" ||
786
+ typeof value.method !== "string") {
787
+ return false;
788
+ }
789
+ if (value.id !== undefined &&
790
+ value.id !== null &&
791
+ typeof value.id !== "string" &&
792
+ typeof value.id !== "number") {
793
+ return false;
794
+ }
795
+ return value.params === undefined || isRecord(value.params);
796
+ }
797
+ function requestId(value) {
798
+ if (!isRecord(value))
799
+ return null;
800
+ const id = value.id;
801
+ return typeof id === "string" || typeof id === "number" || id === null
802
+ ? id
803
+ : null;
804
+ }
805
+ function isRecord(value) {
806
+ return value !== null && typeof value === "object" && !Array.isArray(value);
807
+ }
808
+ class SkenoraMcpError extends Error {
809
+ code;
810
+ data;
811
+ constructor(code, message, data) {
812
+ super(message);
813
+ this.code = code;
814
+ this.data = data;
815
+ }
816
+ }
817
+ //# sourceMappingURL=mcp.js.map