@instruktlabs/kiln 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +12 -0
  4. package/dist/agent-run.mjs +220 -150
  5. package/dist/build.json +22 -22
  6. package/dist/cli.mjs +225 -150
  7. package/dist/mcp-engine.mjs +398 -144
  8. package/dist/mcp-server.mjs +1 -1
  9. package/docs/sdk.md +23 -0
  10. package/docs/tools-operations.md +1529 -0
  11. package/lib/discovery/index.d.ts +1 -1
  12. package/lib/discovery/index.js +2 -2
  13. package/lib/discovery/service.d.ts +3 -1
  14. package/lib/discovery/service.js +4 -2
  15. package/lib/engine-identity.d.ts +1 -1
  16. package/lib/engine-identity.js +1 -1
  17. package/lib/mcp-core.d.ts +2 -1
  18. package/lib/mcp-core.js +1 -0
  19. package/lib/mcp-engine.d.ts +2 -0
  20. package/lib/mcp-engine.js +8 -3
  21. package/lib/tools/discovery.d.ts +1 -1
  22. package/lib/tools/discovery.js +11 -9
  23. package/lib/tools/materials.d.ts +1 -1
  24. package/lib/tools/materials.js +44 -14
  25. package/lib/tools/operation-contract.d.ts +8 -0
  26. package/lib/tools/operation-contract.js +37 -0
  27. package/lib/tools/operations.d.ts +3 -0
  28. package/lib/tools/operations.js +154 -0
  29. package/lib/tools/registry.d.ts +6 -2
  30. package/lib/tools/registry.js +71 -23
  31. package/package.json +2 -1
  32. package/plugin.json +1 -1
  33. package/plugins/kiln-engine/.claude-plugin/plugin.json +1 -1
  34. package/plugins/kiln-engine/README.md +3 -3
  35. package/plugins/kiln-engine/package-provenance.json +6 -6
  36. package/plugins/kiln-engine/plugin.json +1 -1
  37. package/plugins/kiln-engine/runtime.json +1 -1
  38. package/src/discovery/index.ts +2 -0
  39. package/src/discovery/service.ts +3 -1
  40. package/src/engine-identity.ts +1 -1
  41. package/src/mcp-core.ts +3 -0
  42. package/src/mcp-engine.ts +11 -1
  43. package/src/tools/discovery.ts +20 -10
  44. package/src/tools/materials.ts +59 -15
  45. package/src/tools/operation-contract.ts +51 -0
  46. package/src/tools/operations.ts +177 -0
  47. package/src/tools/registry.ts +100 -20
@@ -6,4 +6,4 @@ export type { DiscoveryRequest } from "./query-schema.js";
6
6
  export { createDiscoveryService } from "./service.js";
7
7
  export type { DiscoveryIndex, DiscoveryResponse } from "./service.js";
8
8
  /** Share one immutable catalog/index per process; host capabilities stay live. */
9
- export declare function createDiscovery(capabilities?: () => Promise<unknown>, notes?: () => Promise<string[]>): (input: unknown) => Promise<import("./service.js").DiscoveryResponse>;
9
+ export declare function createDiscovery(capabilities?: () => Promise<unknown>, notes?: () => Promise<string[]>, capabilitiesCall?: string): (input: unknown) => Promise<import("./service.js").DiscoveryResponse>;
@@ -11,10 +11,10 @@ let snapshot;
11
11
  export function createDiscovery(capabilities = async () => ({
12
12
  host: 'No host capability provider was supplied.',
13
13
  discovery: { mode: 'lexical', offline: true, requiresModel: false },
14
- }), notes = async () => []) {
14
+ }), notes = async () => [], capabilitiesCall = 'capabilities:true') {
15
15
  snapshot ??= (() => {
16
16
  const entries = listDiscoveryEntries();
17
17
  return { entries, index: createLexicalDiscoveryIndex(entries) };
18
18
  })();
19
- return createDiscoveryService(snapshot.entries, snapshot.index, capabilities, REMOVED_AUTHORING_HELPERS, notes);
19
+ return createDiscoveryService(snapshot.entries, snapshot.index, capabilities, REMOVED_AUTHORING_HELPERS, notes, capabilitiesCall);
20
20
  }
@@ -53,5 +53,7 @@ export interface DiscoveryResponse {
53
53
  }
54
54
  export declare function createDiscoveryService(source: unknown, index: DiscoveryIndex, capabilities: () => Promise<unknown>, retirements?: Readonly<Record<string, string>>,
55
55
  /** Host notes for the overview, such as the project an omitted `projectId` selects. */
56
- notes?: () => Promise<string[]>): (input: unknown) => Promise<DiscoveryResponse>;
56
+ notes?: () => Promise<string[]>,
57
+ /** Host-selected capability entrypoint, never a model-provided instruction. */
58
+ capabilitiesCall?: string): (input: unknown) => Promise<DiscoveryResponse>;
57
59
  export {};
@@ -7,7 +7,9 @@ const DETAIL_RESULT_BUDGET = 15_000;
7
7
  const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
8
8
  export function createDiscoveryService(source, index, capabilities, retirements = {},
9
9
  /** Host notes for the overview, such as the project an omitted `projectId` selects. */
10
- notes = async () => []) {
10
+ notes = async () => [],
11
+ /** Host-selected capability entrypoint, never a model-provided instruction. */
12
+ capabilitiesCall = 'capabilities:true') {
11
13
  const entries = parseCatalog(source);
12
14
  const byId = new Map(entries.map((entry) => [entry.id, entry]));
13
15
  const byName = new Map(entries.filter((entry) => entry.kind !== 'recipe').map((entry) => [entry.name, entry]));
@@ -34,7 +36,7 @@ notes = async () => []) {
34
36
  'Parts auto-add to opts.parent; do not wrap createPart in parent.add. Part rotation uses degrees.',
35
37
  'Custom THREE.BufferGeometry and ordinary JavaScript functions remain available. Recipes offer guidance and never restrict modeling.',
36
38
  'Send source once and reuse programRef for reads, edits, inspection and rendering. Review geometry, materials and destination constraints separately.',
37
- 'Search for the operation you need in ordinary language, then fetch exact contracts with ids. Use capabilities:true for the current host.',
39
+ `Search for the operation you need in ordinary language, then fetch exact contracts with ids. Use ${capabilitiesCall} for the current host.`,
38
40
  ],
39
41
  };
40
42
  const error = (mode, code, message) => ({
@@ -26,7 +26,7 @@
26
26
  * `src/__tests__/package-contents.test.ts` fails if it disagrees with the manifest
27
27
  * or with any plugin manifest, which is what keeps the literal honest.
28
28
  */
29
- export declare const ENGINE_VERSION = "1.1.0";
29
+ export declare const ENGINE_VERSION = "1.2.0";
30
30
  /**
31
31
  * Where this engine was loaded from, as a `file://` URL of the package root.
32
32
  *
@@ -26,7 +26,7 @@
26
26
  * `src/__tests__/package-contents.test.ts` fails if it disagrees with the manifest
27
27
  * or with any plugin manifest, which is what keeps the literal honest.
28
28
  */
29
- export const ENGINE_VERSION = '1.1.0';
29
+ export const ENGINE_VERSION = '1.2.0';
30
30
  /**
31
31
  * Where this engine was loaded from, as a `file://` URL of the package root.
32
32
  *
package/lib/mcp-core.d.ts CHANGED
@@ -23,7 +23,7 @@ import { type ServeStdioOptions, type StdioServerHandle } from '@modelcontextpro
23
23
  import type { AssetLink } from "./assets-resources.js";
24
24
  /** Server identity reported in the handshake and stamped into every 2026-07-28 result. */
25
25
  export declare const MCP_SERVER_NAME = "kiln";
26
- export declare const MCP_SERVER_VERSION = "1.1.0";
26
+ export declare const MCP_SERVER_VERSION = "1.2.0";
27
27
  /**
28
28
  * The spec's optional `instructions` field, returned by `initialize` and
29
29
  * `server/discover`.
@@ -39,6 +39,7 @@ export declare const MCP_SERVER_VERSION = "1.1.0";
39
39
  * same bytes serve every installation.
40
40
  */
41
41
  export declare const MCP_SERVER_INSTRUCTIONS = "Kiln turns JavaScript you write into GLB assets and returns rendered views. Work by reference: send a program once to kiln_validate or kiln_render, keep the returned programRef exactly, and use it for every later render, inspect, kiln_source and kiln_edit call; never resend a program to change part of it. Call kiln_discover first for helper contracts, with capabilities:true for the host's runtime, project and camera facts. Read viewFidelity before judging materials: a CPU view shows shape, not material; GPU views need the render service in render-service/. Agent Skills for authoring, refining, QA and scenes ship in skills/ beside this server's dist/; read the matching SKILL.md first.";
42
+ export declare const MCP_OPERATION_INSTRUCTIONS = "Kiln turns authored JavaScript into editable GLB assets and rendered views. Send source once to kiln_validate or kiln_render, then preserve the exact programRef for later reads, edits and renders. Use kiln_discover for helper contracts and full input shapes; use kiln_capabilities for host configuration and limits. Read viewFidelity before judging materials: CPU geometry views are not material evidence. Search saved assets with kiln_assets_search; reopen exact source with kiln_assets_restore. Browse materials with kiln_material_search and create with kiln_material_create. Save only when requested. Pin collection, asset and revision IDs for reuse and export.";
42
43
  /** One MCP content block. Mirrors the SDK's `CallToolResult['content']` element. */
43
44
  export type KilnContentBlock = AssetLink | {
44
45
  type: 'text';
package/lib/mcp-core.js CHANGED
@@ -43,6 +43,7 @@ export const MCP_SERVER_VERSION = ENGINE_VERSION;
43
43
  * same bytes serve every installation.
44
44
  */
45
45
  export const MCP_SERVER_INSTRUCTIONS = "Kiln turns JavaScript you write into GLB assets and returns rendered views. Work by reference: send a program once to kiln_validate or kiln_render, keep the returned programRef exactly, and use it for every later render, inspect, kiln_source and kiln_edit call; never resend a program to change part of it. Call kiln_discover first for helper contracts, with capabilities:true for the host's runtime, project and camera facts. Read viewFidelity before judging materials: a CPU view shows shape, not material; GPU views need the render service in render-service/. Agent Skills for authoring, refining, QA and scenes ship in skills/ beside this server's dist/; read the matching SKILL.md first.";
46
+ export const MCP_OPERATION_INSTRUCTIONS = 'Kiln turns authored JavaScript into editable GLB assets and rendered views. Send source once to kiln_validate or kiln_render, then preserve the exact programRef for later reads, edits and renders. Use kiln_discover for helper contracts and full input shapes; use kiln_capabilities for host configuration and limits. Read viewFidelity before judging materials: CPU geometry views are not material evidence. Search saved assets with kiln_assets_search; reopen exact source with kiln_assets_restore. Browse materials with kiln_material_search and create with kiln_material_create. Save only when requested. Pin collection, asset and revision IDs for reuse and export.';
46
47
  /** Presentation hints for the asset viewer app, hidden from the language model. */
47
48
  export const KILN_WIDGET_META = {
48
49
  ui: { prefersBorder: true, csp: { connectDomains: [], resourceDomains: [] } },
@@ -14,6 +14,8 @@ import { type KilnToolContext, type KilnToolDef } from "./tools/registry.js";
14
14
  export type { KilnToolResult } from "./mcp-core.js";
15
15
  /** Optional wire features whose support is not negotiated by core MCP. */
16
16
  export type KilnMcpCompatibilityOptions = {
17
+ /** Opt-in domain operation presentation generated from the shared registry. */
18
+ toolPresentation?: 'grouped' | 'operations';
17
19
  /**
18
20
  * Add core `resource_link` blocks for saved artifact files.
19
21
  *
package/lib/mcp-engine.js CHANGED
@@ -14,12 +14,12 @@ import { buildRenderPort, resolveRenderMode } from "./cli-render-mode.js";
14
14
  import { FileLiveReview } from "./live-review-node.js";
15
15
  import { createPackagedLocalToolContext } from "./local-runtime.js";
16
16
  import { LOOPBACK_HOSTNAMES } from "./loopback.js";
17
- import { createKilnServer, errorMessage, withoutLocalPaths, } from "./mcp-core.js";
17
+ import { createKilnServer, errorMessage, withoutLocalPaths, MCP_OPERATION_INSTRUCTIONS, } from "./mcp-core.js";
18
18
  import { buildMcpManifest } from "./mcp-manifest.js";
19
19
  import { localProgramStore } from "./program-store-node.js";
20
20
  import { validateRequirementsBinding } from "./requirements-context.js";
21
21
  import { describeInputError, schemaFields } from "./tools/actions.js";
22
- import { createKilnProgramToolRegistry, } from "./tools/registry.js";
22
+ import { createKilnProgramToolRegistry, createKilnOperationToolRegistry, } from "./tools/registry.js";
23
23
  import { localWorkspaceRoot } from "./workspace-node.js";
24
24
  /** Use the shared program-aware definitions, including standalone validation. */
25
25
  export function kilnMcpToolDefs(context = {}) {
@@ -112,7 +112,9 @@ export function createKilnToolHost(context = {}, options = {}) {
112
112
  };
113
113
  },
114
114
  };
115
- const defs = kilnMcpToolDefs(requestContext);
115
+ const defs = options.toolPresentation === 'operations'
116
+ ? createKilnOperationToolRegistry(requestContext)
117
+ : kilnMcpToolDefs(requestContext);
116
118
  const byName = new Map(defs.map((def) => [def.name, def]));
117
119
  return {
118
120
  defs,
@@ -200,6 +202,9 @@ function resourceText(name, bytes) {
200
202
  export function createKilnMcpServer(context = {}, options = {}) {
201
203
  const host = createKilnToolHost(context, options);
202
204
  return createKilnServer({
205
+ ...(options.toolPresentation === 'operations'
206
+ ? { instructions: MCP_OPERATION_INSTRUCTIONS }
207
+ : {}),
203
208
  manifest: buildMcpManifest(host.defs, context),
204
209
  host: async () => host,
205
210
  });
@@ -1,3 +1,3 @@
1
1
  import type { KilnToolContext, KilnToolDef } from "./registry.js";
2
2
  /** Shared native tool definition; all transports use the same Discovery service. */
3
- export declare function createKilnDiscoveryDef(context: KilnToolContext): KilnToolDef;
3
+ export declare function createKilnDiscoveryDef(context: KilnToolContext, presentation?: 'grouped' | 'operations'): KilnToolDef;
@@ -8,8 +8,8 @@ import { geometryExportAttributes } from "../geometry-export.js";
8
8
  import { resolveGltfExporter } from "../community-exporter.js";
9
9
  import { approvedTextureCatalogV1 } from "../material-resources.js";
10
10
  /** Shared native tool definition; all transports use the same Discovery service. */
11
- export function createKilnDiscoveryDef(context) {
12
- const run = createDiscovery(() => currentCapabilities(context), async () => projectNotes(context));
11
+ export function createKilnDiscoveryDef(context, presentation = 'grouped') {
12
+ const run = createDiscovery(() => currentCapabilities(context, presentation), async () => projectNotes(context), presentation === 'operations' ? 'kiln_capabilities({})' : 'capabilities:true');
13
13
  return {
14
14
  name: 'kiln_discover',
15
15
  description: 'Discover Kiln operations, assemblies, recipes, tool-input shapes and current host capabilities. Omit arguments for a compact overview. Search with ordinary modeling language using query; refine with family, kind or tags. Fetch complete contracts/examples with ids (up to six exact IDs, executable names or shape: ids). Overview/search pages default to six summaries. Recipes guide construction without restricting the asset. Search runs locally without models or network calls.',
@@ -39,7 +39,7 @@ function projectNotes(context) {
39
39
  ]
40
40
  : [];
41
41
  }
42
- async function currentCapabilities(context) {
42
+ async function currentCapabilities(context, presentation) {
43
43
  const externalEvaluator = Boolean(context.evaluatorPort) || context.evaluatorProfile === 'evaluator-required';
44
44
  const approvedTextures = context.approvedTextureResources
45
45
  ? context.approvedTextureResources()
@@ -91,11 +91,13 @@ async function currentCapabilities(context) {
91
91
  // you -- so the workspace guide's "do not substitute it silently" had
92
92
  // nothing to check against. Compare with `runtime` in .kiln/workspace.json.
93
93
  engine: engineIdentity(),
94
- project: {
95
- configured: configuredProject(context),
96
- select: 'projectId per call; omitted uses the configured project, null selects standalone work',
97
- read: 'kiln_project { action: "get", projectId } returns the brief, design, inventory and material pins',
98
- },
94
+ project: presentation === 'operations' && !context.projectStore
95
+ ? { available: false }
96
+ : {
97
+ configured: configuredProject(context),
98
+ select: 'projectId per call; omitted uses the configured project, null selects standalone work',
99
+ read: 'kiln_project { action: "get", projectId } returns the brief, design, inventory and material pins',
100
+ },
99
101
  renderer,
100
102
  execution: context.localExecution ??
101
103
  (context.evaluatorPort
@@ -115,7 +117,7 @@ async function currentCapabilities(context) {
115
117
  available: Boolean(context.assetLibrary),
116
118
  collections: context.assetLibrary?.collections() ?? [],
117
119
  save: 'kiln_save persists exact GLB, source and provenance; draft renders do not populate collections',
118
- resume: 'kiln_assets action=restore imports a saved revision into the current program store',
120
+ resume: `${presentation === 'operations' ? 'kiln_assets_restore' : 'kiln_assets action=restore'} imports a saved revision into the current program store`,
119
121
  downloads: 'kiln_export returns GLB/source/ZIP resource links; client presentation varies',
120
122
  viewer: 'kiln_present opens a saved revision in supporting chat clients with 3D viewing and downloads; kiln view opens a local collection or standalone GLB/ZIP',
121
123
  },
@@ -26,4 +26,4 @@ export declare const materialToolInput: z.ZodObject<{
26
26
  draft: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
27
27
  payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
28
28
  }, z.core.$strict>;
29
- export declare function createKilnMaterialDef(library: MaterialLibrary): KilnToolDef;
29
+ export declare function createKilnMaterialDef(library: MaterialLibrary, presentation?: 'grouped' | 'operations'): KilnToolDef;
@@ -11,13 +11,39 @@ const MATERIAL_ACTIONS = [
11
11
  'create-procedural',
12
12
  'import',
13
13
  ];
14
- const REQUIREMENTS = {
15
- presets: { required: [] },
16
- 'create-preset': { required: ['presetId', 'seed', 'creator', 'license'] },
17
- list: { required: [] },
18
- get: { required: ['materialId'] },
19
- 'create-procedural': { required: ['draft'], shapes: { draft: 'shape:material-draft' } },
20
- import: { required: ['payload'], shapes: { payload: 'shape:material-import' } },
14
+ const MATERIAL_CONTRACT = {
15
+ presets: {
16
+ fields: ['tag'],
17
+ required: [],
18
+ description: 'List shipped material recipes, optionally filtered by tag.',
19
+ },
20
+ 'create-preset': {
21
+ fields: ['presetId', 'seed', 'creator', 'license', 'size', 'materialId'],
22
+ required: ['presetId', 'seed', 'creator', 'license'],
23
+ description: 'Create an immutable material from a shipped preset with explicit seed, creator and license.',
24
+ },
25
+ list: {
26
+ fields: ['tag'],
27
+ required: [],
28
+ description: 'List saved immutable material revisions, optionally filtered by tag.',
29
+ },
30
+ get: {
31
+ fields: ['materialId', 'revisionId'],
32
+ required: ['materialId'],
33
+ description: 'Read a material revision, provenance and portable specification. Omit revisionId only when exactly one revision exists.',
34
+ },
35
+ 'create-procedural': {
36
+ fields: ['draft'],
37
+ required: ['draft'],
38
+ shapes: { draft: 'shape:material-draft' },
39
+ description: 'Create an immutable material from a bounded procedural draft. Does not fetch URLs or execute source.',
40
+ },
41
+ import: {
42
+ fields: ['payload'],
43
+ required: ['payload'],
44
+ shapes: { payload: 'shape:material-import' },
45
+ description: 'Import complete normalized material records and embedded PNGs. Does not fetch URLs or execute source.',
46
+ },
21
47
  };
22
48
  const record = (description) => z.record(z.string(), z.unknown()).optional().describe(description);
23
49
  const tag = z.string().min(1).max(80).optional().describe('presets, list: keep one tag.');
@@ -49,14 +75,14 @@ const materialResult = (material) => ({
49
75
  material,
50
76
  portableSpec: materialLibraryPortableSpec(material),
51
77
  });
52
- const unknownMaterial = (materialId, revisionId) => new Error(`Unknown material ${materialId}${revisionId ? ` at ${revisionId}` : ''}. kiln_material { action: 'list' } lists the materials in this workspace with their revision IDs.`);
78
+ const unknownMaterial = (materialId, revisionId, listCall) => new Error(`Unknown material ${materialId}${revisionId ? ` at ${revisionId}` : ''}. ${listCall} lists the materials in this workspace with their revision IDs.`);
53
79
  /**
54
80
  * The revision a `get` without `revisionId` means: the material's only one.
55
81
  * Revisions are content hashes with no order, so several are named for the
56
82
  * caller to choose (two live sessions of 1 October 2026 called get with the
57
83
  * id alone and were refused).
58
84
  */
59
- async function onlyRevision(library, materialId) {
85
+ async function onlyRevision(library, materialId, listCall) {
60
86
  const revisions = (await library.list())
61
87
  .filter((material) => material.materialId === materialId)
62
88
  .map((material) => material.revisionId)
@@ -64,7 +90,7 @@ async function onlyRevision(library, materialId) {
64
90
  if (revisions.length === 1)
65
91
  return revisions[0];
66
92
  if (revisions.length === 0)
67
- throw unknownMaterial(materialId);
93
+ throw unknownMaterial(materialId, undefined, listCall);
68
94
  throw new Error(`Material ${materialId} has ${revisions.length} revisions: ${revisions.join(', ')}. Pass revisionId to get one.`);
69
95
  }
70
96
  /**
@@ -83,11 +109,15 @@ function aliasMaterialId(raw) {
83
109
  throw new Error(`kiln_material: resourceId (${String(resourceId)}) and materialId (${String(rest.materialId)}) name different materials; pass materialId alone.`);
84
110
  return { ...rest, materialId: resourceId };
85
111
  }
86
- export function createKilnMaterialDef(library) {
112
+ export function createKilnMaterialDef(library, presentation = 'grouped') {
113
+ const listCall = presentation === 'operations'
114
+ ? 'kiln_material_search({ scope: "saved" })'
115
+ : "kiln_material { action: 'list' }";
87
116
  return {
88
117
  name: 'kiln_material',
89
118
  description: 'Manage optional immutable material resources in this workspace. presets discovers shipped architecture, wood, metal, fabric and ground recipes; create-preset bakes one with an explicit seed, creator and license; list returns compact material/revision summaries; get returns full provenance, hashes, map conventions, physical repeat scale and a code-ready portable material spec; create-procedural bakes bounded editable layer recipes including optional height-derived normals; import accepts complete normalized records with embedded PNG bytes. No action downloads URLs or executes source. Pin the returned materialId/revisionId through per-invocation materialDependencies or project dependencies before authored evaluation resolves the resources; no project is required.',
90
119
  inputSchema: materialToolInput,
120
+ actionContract: MATERIAL_CONTRACT,
91
121
  annotations: {
92
122
  readOnlyHint: false,
93
123
  destructiveHint: false,
@@ -97,7 +127,7 @@ export function createKilnMaterialDef(library) {
97
127
  async run(raw) {
98
128
  assertMaterialJson(raw);
99
129
  const input = materialToolInput.parse(aliasMaterialId(raw));
100
- requireActionFields('kiln_material', input.action, input, REQUIREMENTS[input.action]);
130
+ requireActionFields('kiln_material', input.action, input, MATERIAL_CONTRACT[input.action]);
101
131
  switch (input.action) {
102
132
  case 'presets':
103
133
  return {
@@ -135,14 +165,14 @@ export function createKilnMaterialDef(library) {
135
165
  };
136
166
  }
137
167
  case 'get': {
138
- const revisionId = input.revisionId ?? (await onlyRevision(library, input.materialId));
168
+ const revisionId = input.revisionId ?? (await onlyRevision(library, input.materialId, listCall));
139
169
  try {
140
170
  return materialResult((await library.read(input.materialId, revisionId)).manifest);
141
171
  }
142
172
  catch (error) {
143
173
  // Rule 9: an unknown id names the call that lists the known ones.
144
174
  if (error.code === 'ENOENT')
145
- throw unknownMaterial(input.materialId, revisionId);
175
+ throw unknownMaterial(input.materialId, revisionId, listCall);
146
176
  throw error;
147
177
  }
148
178
  }
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod';
2
+ import type { ActionRequirements } from "./actions.js";
3
+ /** Domain action metadata shared by grouped execution and named presentations. */
4
+ export interface KilnActionContract extends ActionRequirements {
5
+ readonly fields: readonly string[];
6
+ readonly description: string;
7
+ }
8
+ export declare function projectActionSchemas<A extends string>(name: string, input: z.ZodObject, actions: Readonly<Record<A, KilnActionContract>>): Record<A, z.ZodObject>;
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ export function projectActionSchemas(name, input, actions) {
3
+ if (input.def.checks?.length)
4
+ throw new Error(`${name}: cross-field constraints require an explicit operation schema`);
5
+ const json = z.toJSONSchema(input, { io: 'input' });
6
+ const selector = json.properties?.action;
7
+ const expected = Object.keys(actions).sort();
8
+ if (!Array.isArray(selector?.enum) ||
9
+ JSON.stringify([...selector.enum].sort()) !== JSON.stringify(expected))
10
+ throw new Error(`${name}: unclassified actions`);
11
+ const classified = new Set([
12
+ 'action',
13
+ ...Object.values(actions).flatMap((contract) => [...contract.fields]),
14
+ ]);
15
+ const unclassified = Object.keys(input.shape).filter((field) => !classified.has(field));
16
+ if (unclassified.length)
17
+ throw new Error(`${name}: Unclassified fields: ${unclassified.join(', ')}`);
18
+ const schemas = [];
19
+ for (const [action, contract] of Object.entries(actions)) {
20
+ for (const field of contract.required)
21
+ if (!contract.fields.includes(field))
22
+ throw new Error(`${name}.${action}: unmapped required ${field}`);
23
+ for (const field of json.required ?? [])
24
+ if (field !== 'action' && !contract.fields.includes(field))
25
+ throw new Error(`${name}.${action}: would omit required ${field}`);
26
+ const shape = {};
27
+ for (const field of contract.fields) {
28
+ if (field === 'action' || !Object.hasOwn(input.shape, field))
29
+ throw new Error(`${name}.${action}: missing or reserved field ${field}`);
30
+ classified.add(field);
31
+ const source = input.shape[field];
32
+ shape[field] = contract.required.includes(field) ? source.nonoptional() : source;
33
+ }
34
+ schemas.push([action, z.strictObject(shape)]);
35
+ }
36
+ return Object.fromEntries(schemas);
37
+ }
@@ -0,0 +1,3 @@
1
+ import type { KilnToolContext, KilnToolDef } from "./registry.js";
2
+ /** Adapt domain operations in the engine, never in a transport-specific skin. */
3
+ export declare function projectOperationDefs(defs: readonly KilnToolDef[], context: KilnToolContext): KilnToolDef[];
@@ -0,0 +1,154 @@
1
+ import { z } from 'zod';
2
+ import { projectActionSchemas } from "./operation-contract.js";
3
+ /** Adapt domain operations in the engine, never in a transport-specific skin. */
4
+ export function projectOperationDefs(defs, context) {
5
+ return defs.flatMap((def) => {
6
+ if (!context.assetLibrary &&
7
+ ['kiln_save', 'kiln_assets', 'kiln_present', 'kiln_export', 'kiln_import'].includes(def.name))
8
+ return [];
9
+ if (def.name === 'kiln_renderer')
10
+ return rendererOperations(def, context);
11
+ if (def.name === 'kiln_discover')
12
+ return discoveryOperations(def);
13
+ if (def.name === 'kiln_material')
14
+ return materialOperations(def);
15
+ if (def.name === 'kiln_assets')
16
+ return assetOperations(def, context);
17
+ return [def];
18
+ });
19
+ }
20
+ function actionTools(def) {
21
+ if (!(def.inputSchema instanceof z.ZodObject) || !def.actionContract)
22
+ throw new Error(`${def.name}: missing shared action contract`);
23
+ const schemas = projectActionSchemas(def.name, def.inputSchema, def.actionContract);
24
+ return Object.fromEntries(Object.entries(def.actionContract).map(([action, contract]) => [
25
+ action,
26
+ {
27
+ ...def,
28
+ actionContract: undefined,
29
+ name: `${def.name}_${action.replaceAll('-', '_')}`,
30
+ description: contract.description,
31
+ inputSchema: schemas[action],
32
+ run: async (raw) => def.run({ ...schemas[action].parse(raw), action }),
33
+ },
34
+ ]));
35
+ }
36
+ function assetOperations(def, context) {
37
+ const tools = actionTools(def);
38
+ const list = tools.list;
39
+ const listSchema = list.inputSchema;
40
+ // Remove only the legacy default: omission means all collections in this operation.
41
+ const collection = listSchema.shape.collection
42
+ .unwrap()
43
+ .optional()
44
+ .describe('Collection ID from kiln_assets_collections; omitted searches all configured collections.');
45
+ const inputSchema = listSchema.extend({ collection });
46
+ const search = {
47
+ ...list,
48
+ name: 'kiln_assets_search',
49
+ description: 'Search saved assets with pagination. Omit collection for all configured collections, or select one explicitly. Results identify exact collection, asset and revision; partial read failures are reported.',
50
+ inputSchema,
51
+ run: async (raw) => {
52
+ const input = inputSchema.parse(raw);
53
+ const { collection: _collection, ...filters } = input;
54
+ if (input.collection === undefined)
55
+ return tools.catalog.run(filters);
56
+ if (!context.assetLibrary?.collections().some((item) => item.id === input.collection))
57
+ throw new Error('Unknown collection. Use kiln_assets_collections to list available destinations.');
58
+ const result = (await list.run(input));
59
+ return {
60
+ errors: [],
61
+ total: result.total,
62
+ nextOffset: result.nextOffset,
63
+ assets: result.assets.map((asset) => ({ ...asset, collection: input.collection })),
64
+ };
65
+ },
66
+ };
67
+ return [tools.collections, search, tools.get, tools.restore];
68
+ }
69
+ function materialOperations(def) {
70
+ const tools = actionTools(def);
71
+ const presets = tools.presets;
72
+ const inputSchema = presets.inputSchema.extend({
73
+ scope: z.enum(['all', 'presets', 'saved']).default('all'),
74
+ });
75
+ const search = {
76
+ ...presets,
77
+ name: 'kiln_material_search',
78
+ description: 'Browse shipped material recipes and saved material revisions, optionally filtered by exact tag. Recipe IDs and saved material IDs are distinct. Use scope to select presets, saved or both.',
79
+ inputSchema,
80
+ run: async (raw) => {
81
+ const { scope, ...input } = inputSchema.parse(raw);
82
+ const recipes = scope === 'saved'
83
+ ? { presets: [] }
84
+ : (await presets.run(input));
85
+ const saved = scope === 'presets'
86
+ ? { materials: [] }
87
+ : (await tools.list.run(input));
88
+ return {
89
+ ok: true,
90
+ presets: recipes.presets.map((item) => ({ ...item, kind: 'preset' })),
91
+ materials: saved.materials.map((item) => ({ ...item, kind: 'saved' })),
92
+ };
93
+ },
94
+ };
95
+ const preset = tools['create-preset'];
96
+ const procedural = tools['create-procedural'];
97
+ const createInput = z.strictObject({
98
+ definition: z.discriminatedUnion('kind', [
99
+ preset.inputSchema.extend({ kind: z.literal('preset') }),
100
+ procedural.inputSchema.extend({ kind: z.literal('procedural') }),
101
+ ]),
102
+ });
103
+ const create = {
104
+ ...preset,
105
+ name: 'kiln_material_create',
106
+ description: 'Create and retain one immutable material from a preset or procedural definition. Presets require explicit seed, creator and license. Pin the returned materialId/revisionId for rendering. Does not fetch URLs or execute source.',
107
+ inputSchema: createInput,
108
+ run: async (raw) => {
109
+ const { kind, ...input } = createInput.parse(raw).definition;
110
+ return (kind === 'preset' ? preset : procedural).run(input);
111
+ },
112
+ };
113
+ return [search, tools.get, create, tools.import];
114
+ }
115
+ function rendererOperations(def, context) {
116
+ if (!context.renderCapabilities)
117
+ return [];
118
+ return ['status', ...(context.reprobeRenderer ? ['reprobe'] : [])].map((action) => {
119
+ const inputSchema = z.strictObject({});
120
+ return {
121
+ ...def,
122
+ name: `kiln_renderer_${action}`,
123
+ inputSchema,
124
+ run: async (raw) => {
125
+ inputSchema.parse(raw);
126
+ return def.run({ action });
127
+ },
128
+ };
129
+ });
130
+ }
131
+ function discoveryOperations(def) {
132
+ const original = def.inputSchema;
133
+ const { capabilities: _capabilities, ...fields } = original.shape;
134
+ const inputSchema = z.strictObject(fields);
135
+ const empty = z.strictObject({});
136
+ return [
137
+ {
138
+ ...def,
139
+ description: 'Discover geometry helpers, assemblies, recipes and complete input shapes. Search with modeling language or fetch up to six exact IDs. Use kiln_capabilities for current host facts.',
140
+ inputSchema,
141
+ run: async (raw) => def.run(inputSchema.parse(raw)),
142
+ },
143
+ {
144
+ ...def,
145
+ name: 'kiln_capabilities',
146
+ description: 'Inspect declared host capabilities, execution limits and available storage. Observed rendering fidelity is returned by each render.',
147
+ inputSchema: empty,
148
+ run: async (raw) => {
149
+ empty.parse(raw);
150
+ return def.run({ capabilities: true });
151
+ },
152
+ },
153
+ ];
154
+ }
@@ -19,6 +19,8 @@ import { type BackdropId } from "../views/background.js";
19
19
  import type { TextureUsage } from "../textures.js";
20
20
  export declare const KILN_ASSET_WIDGET_URI = "ui://kiln/asset-v5.html";
21
21
  export interface KilnToolDef {
22
+ /** Shared action ownership and requirements; never hand-maintained by transports. */
23
+ actionContract?: Readonly<Record<string, import("./operation-contract.js").KilnActionContract>>;
22
24
  outputSchema?: z.ZodType;
23
25
  /** Optional MCP App presentation; metadata is hidden from the language model. */
24
26
  ui?: {
@@ -770,6 +772,8 @@ export declare function createKilnEditDef(context?: KilnToolContext): KilnToolDe
770
772
  /** Neutral compatibility export. */
771
773
  export declare const kilnEditDef: KilnToolDef;
772
774
  export declare function createKilnProgramToolRegistry(suppliedContext?: KilnToolContext): KilnToolDef[];
775
+ /** Opt-in named domain operations; grouped local/native contracts remain unchanged. */
776
+ export declare function createKilnOperationToolRegistry(context?: KilnToolContext): KilnToolDef[];
773
777
  /** Canonical native workflow: shared definitions plus one host-owned terminal. */
774
778
  export declare function createKilnNativeToolRegistry(suppliedContext: KilnToolContext, completion: NativeCompletion): KilnToolDef[];
775
779
  /**
@@ -784,9 +788,9 @@ export declare function newestRevision(target: {
784
788
  parentRevision?: string | null;
785
789
  createdAt: string;
786
790
  }[]>;
787
- }, collection: string, assetId: string): Promise<string>;
791
+ }, collection: string, assetId: string, listCall?: string): Promise<string>;
788
792
  /** Build one current-policy revision for ordinary save or explicit migration. */
789
793
  export declare function buildProgramAssetDraft(code: string, context: KilnToolContext, backdrop?: BackdropId): Promise<Pick<import("../assets.js").AssetDraft, 'code' | 'glb' | 'preview' | 'previewInfo' | 'build'>>;
790
794
  /** All asset operation schemas live here, alongside the existing tool definitions. */
791
- export declare function createKilnAssetDefs(context: KilnToolContext): KilnToolDef[];
795
+ export declare function createKilnAssetDefs(context: KilnToolContext, presentation?: 'grouped' | 'operations'): KilnToolDef[];
792
796
  export {};