@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
@@ -13,6 +13,7 @@ import { createKilnMaterialDef } from "./materials.js";
13
13
  import { createKilnReviewDef } from "./review.js";
14
14
  import { withWorkspaceContext } from "./workspace.js";
15
15
  import { describeInputError } from "./actions.js";
16
+ import { projectOperationDefs } from "./operations.js";
16
17
  import { advancedCaptureInput, cameraShotInput, cameraShotRecordInput, cameraVec3Input, captureInput, captureRecordInput, } from "./capture-input.js";
17
18
  import { compactEditResult, compactReviewResult, partsHint, requestedDetail, reviewDetailInput, } from "./review-detail.js";
18
19
  import { createCachedEvaluatorPort, MemoryBuildCache } from "../build-cache.js";
@@ -1605,22 +1606,25 @@ const rendererInput = z.strictObject({
1605
1606
  action: z.enum(['status', 'reprobe']).default('status'),
1606
1607
  });
1607
1608
  /** A saved-revision read whose failure names the listing call instead of a filesystem path (rule 9). */
1608
- async function readSavedRevision(target, collection, assetId, revisionId) {
1609
+ async function readSavedRevision(target, collection, assetId, revisionId, listCall = `kiln_assets { action: 'list', collection: '${collection}' }`) {
1609
1610
  try {
1610
1611
  return await target.read(collection, assetId, revisionId);
1611
1612
  }
1612
1613
  catch (error) {
1613
1614
  if (error.code === 'ENOENT')
1614
- throw new Error(`Unknown asset ${assetId} revision ${revisionId} in collection ${collection}. kiln_assets { action: 'list', collection: '${collection}' } lists the saved revisions.`);
1615
+ throw new Error(`Unknown asset ${assetId} revision ${revisionId} in collection ${collection}. ${listCall} lists the saved revisions.`);
1615
1616
  throw error;
1616
1617
  }
1617
1618
  }
1618
1619
  export function createKilnProgramToolRegistry(suppliedContext = {}) {
1620
+ return createProgramDefs(suppliedContext, 'grouped');
1621
+ }
1622
+ function createProgramDefs(suppliedContext, presentation) {
1619
1623
  toolRequirements(suppliedContext);
1620
1624
  const store = suppliedContext.programStore ?? new MemoryProgramStore();
1621
1625
  const context = withCaptureCache(withBuildCache({ ...suppliedContext, programStore: store }));
1622
1626
  return [
1623
- createKilnDiscoveryDef({ ...suppliedContext, programStore: store }),
1627
+ createKilnDiscoveryDef({ ...suppliedContext, programStore: store }, presentation),
1624
1628
  {
1625
1629
  name: 'kiln_renderer',
1626
1630
  description: 'Inspect status, or reprobe after renderer setup/repair to refresh this session and reset failed starts. Never installs, starts, stops or renders. Preserves CPU/local/remote selection; environment/credential changes require a host restart. Read viewFidelity after rendering.',
@@ -1665,7 +1669,9 @@ export function createKilnProgramToolRegistry(suppliedContext = {}) {
1665
1669
  ...(context.projectStore
1666
1670
  ? [createKilnProjectDef(context.projectStore, context.projectBundleReader)]
1667
1671
  : []),
1668
- ...(context.materialLibrary ? [createKilnMaterialDef(context.materialLibrary)] : []),
1672
+ ...(context.materialLibrary
1673
+ ? [createKilnMaterialDef(context.materialLibrary, presentation)]
1674
+ : []),
1669
1675
  ...(context.reviewStore && context.assetLibrary
1670
1676
  ? [
1671
1677
  createKilnReviewDef({
@@ -1675,7 +1681,7 @@ export function createKilnProgramToolRegistry(suppliedContext = {}) {
1675
1681
  }),
1676
1682
  ]
1677
1683
  : []),
1678
- ...createKilnAssetDefs({ ...context, programStore: store }),
1684
+ ...createKilnAssetDefs({ ...context, programStore: store }, presentation),
1679
1685
  ]
1680
1686
  .map((def) => withWorkspaceContext(def, context))
1681
1687
  .map((def) => ({
@@ -1689,6 +1695,10 @@ export function createKilnProgramToolRegistry(suppliedContext = {}) {
1689
1695
  },
1690
1696
  }));
1691
1697
  }
1698
+ /** Opt-in named domain operations; grouped local/native contracts remain unchanged. */
1699
+ export function createKilnOperationToolRegistry(context = {}) {
1700
+ return projectOperationDefs(createProgramDefs(context, 'operations'), context);
1701
+ }
1692
1702
  /** Canonical native workflow: shared definitions plus one host-owned terminal. */
1693
1703
  export function createKilnNativeToolRegistry(suppliedContext, completion) {
1694
1704
  const active = toolRequirements(suppliedContext);
@@ -1772,15 +1782,42 @@ const assetSelector = {
1772
1782
  assetId: z.string().regex(/^[a-z][a-z0-9_-]{0,79}$/),
1773
1783
  revisionId: z.string().regex(/^[a-z][a-z0-9_-]{0,79}$/),
1774
1784
  };
1785
+ const ASSET_CONTRACT = {
1786
+ collections: {
1787
+ fields: [],
1788
+ required: [],
1789
+ description: 'List configured asset storage collections. Collection is not project membership.',
1790
+ },
1791
+ catalog: {
1792
+ fields: ['assetId', 'query', 'offset', 'limit'],
1793
+ required: [],
1794
+ description: 'Search saved assets across configured collections with pagination.',
1795
+ },
1796
+ list: {
1797
+ fields: ['collection', 'assetId', 'query', 'offset', 'limit'],
1798
+ required: [],
1799
+ description: 'Search saved assets in one collection with pagination.',
1800
+ },
1801
+ get: {
1802
+ fields: ['collection', 'assetId', 'revisionId'],
1803
+ required: ['assetId'],
1804
+ description: 'Read a saved revision and its download descriptors. Omit revisionId for the newest revision.',
1805
+ },
1806
+ restore: {
1807
+ fields: ['collection', 'assetId', 'revisionId'],
1808
+ required: ['assetId'],
1809
+ description: 'Restore exact saved source as programRef for editing or rendering. Omit revisionId for the newest revision. Does not overwrite the saved revision. Binary-only assets have no source.',
1810
+ },
1811
+ };
1775
1812
  /**
1776
1813
  * The revision a get or restore without `revisionId` means: the newest, the
1777
1814
  * one no later revision names as its parent, ties to the latest `createdAt`
1778
1815
  * (c40 of 1 October 2026 called get with the asset id alone and was refused).
1779
1816
  */
1780
- export async function newestRevision(target, collection, assetId) {
1817
+ export async function newestRevision(target, collection, assetId, listCall = `kiln_assets { action: 'list', collection: '${collection}' }`) {
1781
1818
  const revisions = (await target.list(collection)).filter((a) => a.assetId === assetId);
1782
1819
  if (!revisions.length)
1783
- throw new Error(`Unknown asset ${assetId} in ${collection}; kiln_assets { action: 'list', collection: '${collection}' } lists them.`);
1820
+ throw new Error(`Unknown asset ${assetId} in ${collection}; ${listCall} lists them.`);
1784
1821
  const parents = new Set(revisions.map((a) => a.parentRevision).filter(Boolean));
1785
1822
  const heads = revisions.filter((a) => !parents.has(a.revisionId));
1786
1823
  const newest = (heads.length ? heads : revisions).sort((a, b) => a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0);
@@ -1857,7 +1894,16 @@ export async function buildProgramAssetDraft(code, context, backdrop) {
1857
1894
  };
1858
1895
  }
1859
1896
  /** All asset operation schemas live here, alongside the existing tool definitions. */
1860
- export function createKilnAssetDefs(context) {
1897
+ export function createKilnAssetDefs(context, presentation = 'grouped') {
1898
+ const listCall = (collection) => presentation === 'operations'
1899
+ ? `kiln_assets_search({ collection: '${collection}' })`
1900
+ : `kiln_assets { action: 'list', collection: '${collection}' }`;
1901
+ const selector = presentation === 'operations'
1902
+ ? {
1903
+ ...assetSelector,
1904
+ collection: assetSelector.collection.describe('Collection ID from kiln_assets_collections; default project.'),
1905
+ }
1906
+ : assetSelector;
1861
1907
  const library = () => {
1862
1908
  if (!context.assetLibrary)
1863
1909
  throw new Error('No asset library configured. The local CLI/MCP host supplies workspace collections; embedded hosts must inject assetLibrary.');
@@ -1886,11 +1932,11 @@ export function createKilnAssetDefs(context) {
1886
1932
  downloadUrls: await context.assetDownloadUrls?.(collection, asset.assetId, asset.revisionId),
1887
1933
  });
1888
1934
  const saveInput = z.object({
1889
- collection: assetSelector.collection,
1935
+ collection: selector.collection,
1890
1936
  programRef: z.string(),
1891
1937
  name: z.string().min(1).max(200),
1892
- assetId: assetSelector.assetId.optional(),
1893
- parentRevision: assetSelector.revisionId.optional(),
1938
+ assetId: selector.assetId.optional(),
1939
+ parentRevision: selector.revisionId.optional(),
1894
1940
  tags: z.array(z.string().max(80)).max(30).optional(),
1895
1941
  brief: z.string().max(8000).optional(),
1896
1942
  description: z.string().max(4000).optional(),
@@ -1908,16 +1954,16 @@ export function createKilnAssetDefs(context) {
1908
1954
  });
1909
1955
  const assetsInput = z.object({
1910
1956
  action: z.enum(['collections', 'catalog', 'list', 'get', 'restore']).default('list'),
1911
- collection: assetSelector.collection,
1912
- assetId: assetSelector.assetId.optional(),
1913
- revisionId: assetSelector.revisionId
1957
+ collection: selector.collection,
1958
+ assetId: selector.assetId.optional(),
1959
+ revisionId: selector.revisionId
1914
1960
  .optional()
1915
1961
  .describe('get, restore: the saved revision; omitted, the newest.'),
1916
1962
  query: z.string().max(200).optional(),
1917
1963
  offset: z.number().int().min(0).default(0),
1918
1964
  limit: z.number().int().min(1).max(50).default(20),
1919
1965
  });
1920
- const exportInput = z.object(assetSelector);
1966
+ const exportInput = z.object(selector);
1921
1967
  const profileExportInput = exportInput.extend({
1922
1968
  profile: z
1923
1969
  .enum(['editable', 'runtime'])
@@ -1925,8 +1971,8 @@ export function createKilnAssetDefs(context) {
1925
1971
  .describe('editable preserves canonical source/GLB/build resources. runtime returns a standalone GLB and versioned review-metadata sidecar; no source bundle or geometry optimization.'),
1926
1972
  });
1927
1973
  const importInput = z.object({
1928
- ...assetSelector,
1929
- sourceCollection: assetSelector.collection,
1974
+ ...selector,
1975
+ sourceCollection: selector.collection,
1930
1976
  });
1931
1977
  return [
1932
1978
  {
@@ -1942,7 +1988,7 @@ export function createKilnAssetDefs(context) {
1942
1988
  requirements: activeRequirements.binding,
1943
1989
  };
1944
1990
  if (input.assetId && input.parentRevision) {
1945
- const previous = await readSavedRevision(target, input.collection, input.assetId, input.parentRevision);
1991
+ const previous = await readSavedRevision(target, input.collection, input.assetId, input.parentRevision, listCall(input.collection));
1946
1992
  assertSavedRequirementsAuthorized(previous.manifest, activeRequirements);
1947
1993
  }
1948
1994
  const code = await context.programStore.get(input.programRef);
@@ -1957,6 +2003,7 @@ export function createKilnAssetDefs(context) {
1957
2003
  name: 'kiln_assets',
1958
2004
  description: 'Browse saved assets: collections discovers storage; catalog searches all configured collections; list searches one. Both searches paginate. get returns a build record/downloads; restore loads exact source for kiln_source/kiln_edit. Collection is not project membership. Binary-only assets cannot restore source.',
1959
2005
  inputSchema: assetsInput,
2006
+ actionContract: ASSET_CONTRACT,
1960
2007
  run: async (raw) => {
1961
2008
  const input = assetsInput.parse(raw);
1962
2009
  const activeRequirements = toolRequirements(context);
@@ -2005,10 +2052,11 @@ export function createKilnAssetDefs(context) {
2005
2052
  })),
2006
2053
  };
2007
2054
  }
2008
- if (!input.assetId)
2055
+ if (ASSET_CONTRACT[input.action].required.some((field) => !input[field]))
2009
2056
  throw new Error(`kiln_assets ${input.action} requires assetId; kiln_assets { action: 'list', collection: '${input.collection}' } lists them.`);
2010
- const revisionId = input.revisionId ?? (await newestRevision(target, input.collection, input.assetId));
2011
- const record = await readSavedRevision(target, input.collection, input.assetId, revisionId);
2057
+ const revisionId = input.revisionId ??
2058
+ (await newestRevision(target, input.collection, input.assetId, listCall(input.collection)));
2059
+ const record = await readSavedRevision(target, input.collection, input.assetId, revisionId, listCall(input.collection));
2012
2060
  if (input.action === 'get')
2013
2061
  return links(input.collection, record.manifest);
2014
2062
  const saved = assertSavedRequirementsAuthorized(record.manifest, activeRequirements);
@@ -2080,7 +2128,7 @@ export function createKilnAssetDefs(context) {
2080
2128
  inputSchema: profileExportInput,
2081
2129
  run: async (raw) => {
2082
2130
  const input = profileExportInput.parse(raw);
2083
- const record = await readSavedRevision(library(), input.collection, input.assetId, input.revisionId);
2131
+ const record = await readSavedRevision(library(), input.collection, input.assetId, input.revisionId, listCall(input.collection));
2084
2132
  const { inspectDrawDiagnostics } = await import("../draw-diagnostics.js");
2085
2133
  if (input.profile === 'runtime') {
2086
2134
  const output = await (await import("../asset-export.js")).exportAssetGlb(record, {
@@ -2114,7 +2162,7 @@ export function createKilnAssetDefs(context) {
2114
2162
  run: async (raw) => {
2115
2163
  const input = importInput.parse(raw);
2116
2164
  const target = library();
2117
- const record = await readSavedRevision(target, input.sourceCollection, input.assetId, input.revisionId);
2165
+ const record = await readSavedRevision(target, input.sourceCollection, input.assetId, input.revisionId, listCall(input.sourceCollection));
2118
2166
  await target.import(input.collection, [record]);
2119
2167
  return links(input.collection, record.manifest);
2120
2168
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruktlabs/kiln",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Create and refine procedural 3D assets with JavaScript, image feedback, and reusable source revisions.",
5
5
  "license": "MIT",
6
6
  "author": "Matthew Kissinger",
@@ -299,6 +299,7 @@
299
299
  "docs/runtime.md",
300
300
  "docs/sdk.md",
301
301
  "docs/tools.md",
302
+ "docs/tools-operations.md",
302
303
  "README.md",
303
304
  "CONTRIBUTING.md",
304
305
  "LICENSE",
package/plugin.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kiln",
3
3
  "displayName": "Kiln",
4
4
  "description": "Build and revise 3D assets with your coding agent. Write JavaScript, inspect selected views, edit saved source revisions, and export GLB files locally.",
5
- "version": "1.1.0",
5
+ "version": "1.2.0",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/instruktlabs/kiln",
8
8
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiln-engine",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Set up a local Kiln workspace to create, inspect and revise editable 3D assets with Claude Code or Codex.",
5
5
  "author": {
6
6
  "name": "Instrukt Labs",
@@ -1,7 +1,7 @@
1
1
  # Kiln local plugin
2
2
 
3
- Plugin version 1.1.0, maintained by Instrukt Labs under the MIT license.
4
- This plugin pins engine 1.1.0. Registry setup requires that exact version
3
+ Plugin version 1.2.0, maintained by Instrukt Labs under the MIT license.
4
+ This plugin pins engine 1.2.0. Registry setup requires that exact version
5
5
  to be published. Use a published release tag for normal installation or a reviewed
6
6
  local archive for development qualification. The installation ID remains
7
7
  `kiln-engine@instruktlabs`.
@@ -31,7 +31,7 @@ codex plugin add kiln-engine@instruktlabs
31
31
  Restart or reload your coding agent's plugins as its installation message directs.
32
32
  Then ask it to set up a Kiln workspace. The maintained setup skill runs this
33
33
  plugin's `bin/kiln-setup-workspace.mjs` with Node, installs
34
- `@instruktlabs/kiln@1.1.0` outside the project and configures your chosen
34
+ `@instruktlabs/kiln@1.2.0` outside the project and configures your chosen
35
35
  workspace. The same initializer supports existing projects and new workspaces.
36
36
 
37
37
  The plugin registers setup only. The workspace supplies authoring skills and one
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "kind": "kiln-local-plugin",
4
- "pluginVersion": "1.1.0",
5
- "engineVersion": "1.1.0",
4
+ "pluginVersion": "1.2.0",
5
+ "engineVersion": "1.2.0",
6
6
  "executables": [
7
7
  "bin/kiln-setup-workspace.mjs"
8
8
  ],
9
9
  "files": {
10
10
  ".claude-plugin/icon.png": "sha256:b4b651c402793b7e27cab780f04b14f80daec6d0cd5b7e4e90313930581a54e9",
11
- ".claude-plugin/plugin.json": "sha256:189394253cf57f988fd82b31dad05012d693615a50126eebb29bab3e80e3a2a9",
11
+ ".claude-plugin/plugin.json": "sha256:616a6a61da072fcfdd4d0e1cf513e0e91bf1e56fb61b9b02d23e72fa291556bc",
12
12
  "bin/kiln-setup-workspace.mjs": "sha256:3d9a30236919a6ca91acb6b26f808cec442e6edc3dd88fe010645b010d76e838",
13
13
  "LICENSE": "sha256:cdba278ebf1d6de618869936226bf03be4887179c128efc7aaa13d22034826a5",
14
- "plugin.json": "sha256:428ab085c6257ccc0e2dec3c7d4f9b6e02db0024a5f0b355410a30d6282dcd1d",
14
+ "plugin.json": "sha256:3437107d46ad4fef4a710a8e821e56b43894cc1722504fe4e8ff0120fa6a8d8d",
15
15
  "PRIVACY.md": "sha256:5f58d981667e40441e21c2dec411f269a9a67a6141cec61976d27088fb12573b",
16
- "README.md": "sha256:6a7fc436113f7493c884b2c495629351b01b2cd7e82e7b808f3abe73b9eb9af4",
17
- "runtime.json": "sha256:253f76044dcc162295ca760ff584d1d6894068357c727a56b077b44278f38eb3",
16
+ "README.md": "sha256:0b82e6eb8b679f8b991c1a5567957574ed7b3341a87ef989403e5521f83ab50e",
17
+ "runtime.json": "sha256:5ebac8387f4bedcc7cac02a36f73c5ffba969ce6c8c6e2b0e356ef569168d0a5",
18
18
  "skills/kiln-setup-workspace/references/plugin-install.md": "sha256:c338fb792e7fad0364c3e1a13c94c506afa1395489ec1b6e83b36d04728935fb",
19
19
  "skills/kiln-setup-workspace/references/renderer-setup.md": "sha256:b743b1b2827fa814385990e1e5b849391cb427413e9c016d273aa19cebd46031",
20
20
  "skills/kiln-setup-workspace/SKILL.md": "sha256:65ed926b3503a33f4fe793e84e4a045ce36e3967f0e6b340e456a15dd010ce54"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "kiln-engine",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "description": "Set up a local Kiln workspace to create, inspect and revise editable 3D assets with Claude Code or Codex.",
6
6
  "author": {
7
7
  "name": "Instrukt Labs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruktlabs/kiln",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "harnesses": [
5
5
  "claude",
6
6
  "codex",
@@ -21,6 +21,7 @@ export function createDiscovery(
21
21
  discovery: { mode: 'lexical', offline: true, requiresModel: false },
22
22
  }),
23
23
  notes: () => Promise<string[]> = async () => [],
24
+ capabilitiesCall = 'capabilities:true',
24
25
  ) {
25
26
  snapshot ??= (() => {
26
27
  const entries = listDiscoveryEntries();
@@ -32,5 +33,6 @@ export function createDiscovery(
32
33
  capabilities,
33
34
  REMOVED_AUTHORING_HELPERS,
34
35
  notes,
36
+ capabilitiesCall,
35
37
  );
36
38
  }
@@ -63,6 +63,8 @@ export function createDiscoveryService(
63
63
  retirements: Readonly<Record<string, string>> = {},
64
64
  /** Host notes for the overview, such as the project an omitted `projectId` selects. */
65
65
  notes: () => Promise<string[]> = async () => [],
66
+ /** Host-selected capability entrypoint, never a model-provided instruction. */
67
+ capabilitiesCall = 'capabilities:true',
66
68
  ): (input: unknown) => Promise<DiscoveryResponse> {
67
69
  const entries = parseCatalog(source);
68
70
  const byId = new Map(entries.map((entry) => [entry.id, entry]));
@@ -94,7 +96,7 @@ export function createDiscoveryService(
94
96
  'Parts auto-add to opts.parent; do not wrap createPart in parent.add. Part rotation uses degrees.',
95
97
  'Custom THREE.BufferGeometry and ordinary JavaScript functions remain available. Recipes offer guidance and never restrict modeling.',
96
98
  'Send source once and reuse programRef for reads, edits, inspection and rendering. Review geometry, materials and destination constraints separately.',
97
- 'Search for the operation you need in ordinary language, then fetch exact contracts with ids. Use capabilities:true for the current host.',
99
+ `Search for the operation you need in ordinary language, then fetch exact contracts with ids. Use ${capabilitiesCall} for the current host.`,
98
100
  ],
99
101
  };
100
102
  const error = (
@@ -27,7 +27,7 @@
27
27
  * `src/__tests__/package-contents.test.ts` fails if it disagrees with the manifest
28
28
  * or with any plugin manifest, which is what keeps the literal honest.
29
29
  */
30
- export const ENGINE_VERSION = '1.1.0';
30
+ export const ENGINE_VERSION = '1.2.0';
31
31
 
32
32
  /**
33
33
  * Where this engine was loaded from, as a `file://` URL of the package root.
package/src/mcp-core.ts CHANGED
@@ -65,6 +65,9 @@ export const MCP_SERVER_VERSION = ENGINE_VERSION;
65
65
  export const MCP_SERVER_INSTRUCTIONS =
66
66
  "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.";
67
67
 
68
+ export const MCP_OPERATION_INSTRUCTIONS =
69
+ '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.';
70
+
68
71
  /** One MCP content block. Mirrors the SDK's `CallToolResult['content']` element. */
69
72
  export type KilnContentBlock =
70
73
  | AssetLink
package/src/mcp-engine.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  type KilnResourceContents,
27
27
  type KilnToolResult,
28
28
  withoutLocalPaths,
29
+ MCP_OPERATION_INSTRUCTIONS,
29
30
  } from './mcp-core';
30
31
  import { buildMcpManifest } from './mcp-manifest';
31
32
  import { localProgramStore } from './program-store-node';
@@ -33,6 +34,7 @@ import { validateRequirementsBinding } from './requirements-context';
33
34
  import { describeInputError, schemaFields } from './tools/actions';
34
35
  import {
35
36
  createKilnProgramToolRegistry,
37
+ createKilnOperationToolRegistry,
36
38
  type KilnToolContext,
37
39
  type KilnToolDef,
38
40
  } from './tools/registry';
@@ -42,6 +44,8 @@ export type { KilnToolResult } from './mcp-core';
42
44
 
43
45
  /** Optional wire features whose support is not negotiated by core MCP. */
44
46
  export type KilnMcpCompatibilityOptions = {
47
+ /** Opt-in domain operation presentation generated from the shared registry. */
48
+ toolPresentation?: 'grouped' | 'operations';
45
49
  /**
46
50
  * Add core `resource_link` blocks for saved artifact files.
47
51
  *
@@ -160,7 +164,10 @@ export function createKilnToolHost(
160
164
  };
161
165
  },
162
166
  };
163
- const defs = kilnMcpToolDefs(requestContext);
167
+ const defs =
168
+ options.toolPresentation === 'operations'
169
+ ? createKilnOperationToolRegistry(requestContext)
170
+ : kilnMcpToolDefs(requestContext);
164
171
  const byName = new Map(defs.map((def) => [def.name, def] as const));
165
172
  return {
166
173
  defs,
@@ -259,6 +266,9 @@ export function createKilnMcpServer(
259
266
  ): Server {
260
267
  const host = createKilnToolHost(context, options);
261
268
  return createKilnServer({
269
+ ...(options.toolPresentation === 'operations'
270
+ ? { instructions: MCP_OPERATION_INSTRUCTIONS }
271
+ : {}),
262
272
  manifest: buildMcpManifest(host.defs, context),
263
273
  host: async () => host,
264
274
  });
@@ -11,10 +11,14 @@ import { resolveGltfExporter } from '../community-exporter';
11
11
  import { approvedTextureCatalogV1 } from '../material-resources';
12
12
 
13
13
  /** Shared native tool definition; all transports use the same Discovery service. */
14
- export function createKilnDiscoveryDef(context: KilnToolContext): KilnToolDef {
14
+ export function createKilnDiscoveryDef(
15
+ context: KilnToolContext,
16
+ presentation: 'grouped' | 'operations' = 'grouped',
17
+ ): KilnToolDef {
15
18
  const run = createDiscovery(
16
- () => currentCapabilities(context),
19
+ () => currentCapabilities(context, presentation),
17
20
  async () => projectNotes(context),
21
+ presentation === 'operations' ? 'kiln_capabilities({})' : 'capabilities:true',
18
22
  );
19
23
  return {
20
24
  name: 'kiln_discover',
@@ -48,7 +52,10 @@ function projectNotes(context: KilnToolContext): string[] {
48
52
  : [];
49
53
  }
50
54
 
51
- async function currentCapabilities(context: KilnToolContext) {
55
+ async function currentCapabilities(
56
+ context: KilnToolContext,
57
+ presentation: 'grouped' | 'operations',
58
+ ) {
52
59
  const externalEvaluator =
53
60
  Boolean(context.evaluatorPort) || context.evaluatorProfile === 'evaluator-required';
54
61
  const approvedTextures = context.approvedTextureResources
@@ -102,12 +109,15 @@ async function currentCapabilities(context: KilnToolContext) {
102
109
  // you -- so the workspace guide's "do not substitute it silently" had
103
110
  // nothing to check against. Compare with `runtime` in .kiln/workspace.json.
104
111
  engine: engineIdentity(),
105
- project: {
106
- configured: configuredProject(context),
107
- select:
108
- 'projectId per call; omitted uses the configured project, null selects standalone work',
109
- read: 'kiln_project { action: "get", projectId } returns the brief, design, inventory and material pins',
110
- },
112
+ project:
113
+ presentation === 'operations' && !context.projectStore
114
+ ? { available: false }
115
+ : {
116
+ configured: configuredProject(context),
117
+ select:
118
+ 'projectId per call; omitted uses the configured project, null selects standalone work',
119
+ read: 'kiln_project { action: "get", projectId } returns the brief, design, inventory and material pins',
120
+ },
111
121
  renderer,
112
122
  execution:
113
123
  context.localExecution ??
@@ -128,7 +138,7 @@ async function currentCapabilities(context: KilnToolContext) {
128
138
  available: Boolean(context.assetLibrary),
129
139
  collections: context.assetLibrary?.collections() ?? [],
130
140
  save: 'kiln_save persists exact GLB, source and provenance; draft renders do not populate collections',
131
- resume: 'kiln_assets action=restore imports a saved revision into the current program store',
141
+ resume: `${presentation === 'operations' ? 'kiln_assets_restore' : 'kiln_assets action=restore'} imports a saved revision into the current program store`,
132
142
  downloads: 'kiln_export returns GLB/source/ZIP resource links; client presentation varies',
133
143
  viewer:
134
144
  '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',
@@ -14,6 +14,7 @@ import { createMaterialRecordV1, decodeMaterialLibraryPayload } from '../materia
14
14
  import { createMaterialPresetDraft, listMaterialPresets } from '../material-presets';
15
15
  import { nestedRecordDescription, requireActionFields } from './actions';
16
16
  import type { KilnToolDef } from './registry';
17
+ import type { KilnActionContract } from './operation-contract';
17
18
 
18
19
  const MATERIAL_ACTIONS = [
19
20
  'presets',
@@ -24,13 +25,43 @@ const MATERIAL_ACTIONS = [
24
25
  'import',
25
26
  ] as const;
26
27
  type MaterialAction = (typeof MATERIAL_ACTIONS)[number];
27
- const REQUIREMENTS: Record<MaterialAction, Parameters<typeof requireActionFields>[3]> = {
28
- presets: { required: [] },
29
- 'create-preset': { required: ['presetId', 'seed', 'creator', 'license'] },
30
- list: { required: [] },
31
- get: { required: ['materialId'] },
32
- 'create-procedural': { required: ['draft'], shapes: { draft: 'shape:material-draft' } },
33
- import: { required: ['payload'], shapes: { payload: 'shape:material-import' } },
28
+ const MATERIAL_CONTRACT: Record<MaterialAction, KilnActionContract> = {
29
+ presets: {
30
+ fields: ['tag'],
31
+ required: [],
32
+ description: 'List shipped material recipes, optionally filtered by tag.',
33
+ },
34
+ 'create-preset': {
35
+ fields: ['presetId', 'seed', 'creator', 'license', 'size', 'materialId'],
36
+ required: ['presetId', 'seed', 'creator', 'license'],
37
+ description:
38
+ 'Create an immutable material from a shipped preset with explicit seed, creator and license.',
39
+ },
40
+ list: {
41
+ fields: ['tag'],
42
+ required: [],
43
+ description: 'List saved immutable material revisions, optionally filtered by tag.',
44
+ },
45
+ get: {
46
+ fields: ['materialId', 'revisionId'],
47
+ required: ['materialId'],
48
+ description:
49
+ 'Read a material revision, provenance and portable specification. Omit revisionId only when exactly one revision exists.',
50
+ },
51
+ 'create-procedural': {
52
+ fields: ['draft'],
53
+ required: ['draft'],
54
+ shapes: { draft: 'shape:material-draft' },
55
+ description:
56
+ 'Create an immutable material from a bounded procedural draft. Does not fetch URLs or execute source.',
57
+ },
58
+ import: {
59
+ fields: ['payload'],
60
+ required: ['payload'],
61
+ shapes: { payload: 'shape:material-import' },
62
+ description:
63
+ 'Import complete normalized material records and embedded PNGs. Does not fetch URLs or execute source.',
64
+ },
34
65
  };
35
66
  const record = (description: string) =>
36
67
  z.record(z.string(), z.unknown()).optional().describe(description);
@@ -74,9 +105,9 @@ const materialResult = (material: MaterialManifestV1) => ({
74
105
  material,
75
106
  portableSpec: materialLibraryPortableSpec(material),
76
107
  });
77
- const unknownMaterial = (materialId: string, revisionId?: string) =>
108
+ const unknownMaterial = (materialId: string, revisionId: string | undefined, listCall: string) =>
78
109
  new Error(
79
- `Unknown material ${materialId}${revisionId ? ` at ${revisionId}` : ''}. kiln_material { action: 'list' } lists the materials in this workspace with their revision IDs.`,
110
+ `Unknown material ${materialId}${revisionId ? ` at ${revisionId}` : ''}. ${listCall} lists the materials in this workspace with their revision IDs.`,
80
111
  );
81
112
  /**
82
113
  * The revision a `get` without `revisionId` means: the material's only one.
@@ -84,13 +115,17 @@ const unknownMaterial = (materialId: string, revisionId?: string) =>
84
115
  * caller to choose (two live sessions of 1 October 2026 called get with the
85
116
  * id alone and were refused).
86
117
  */
87
- async function onlyRevision(library: MaterialLibrary, materialId: string): Promise<string> {
118
+ async function onlyRevision(
119
+ library: MaterialLibrary,
120
+ materialId: string,
121
+ listCall: string,
122
+ ): Promise<string> {
88
123
  const revisions = (await library.list())
89
124
  .filter((material) => material.materialId === materialId)
90
125
  .map((material) => material.revisionId)
91
126
  .sort();
92
127
  if (revisions.length === 1) return revisions[0]!;
93
- if (revisions.length === 0) throw unknownMaterial(materialId);
128
+ if (revisions.length === 0) throw unknownMaterial(materialId, undefined, listCall);
94
129
  throw new Error(
95
130
  `Material ${materialId} has ${revisions.length} revisions: ${revisions.join(', ')}. Pass revisionId to get one.`,
96
131
  );
@@ -111,12 +146,20 @@ function aliasMaterialId(raw: unknown): unknown {
111
146
  );
112
147
  return { ...rest, materialId: resourceId };
113
148
  }
114
- export function createKilnMaterialDef(library: MaterialLibrary): KilnToolDef {
149
+ export function createKilnMaterialDef(
150
+ library: MaterialLibrary,
151
+ presentation: 'grouped' | 'operations' = 'grouped',
152
+ ): KilnToolDef {
153
+ const listCall =
154
+ presentation === 'operations'
155
+ ? 'kiln_material_search({ scope: "saved" })'
156
+ : "kiln_material { action: 'list' }";
115
157
  return {
116
158
  name: 'kiln_material',
117
159
  description:
118
160
  '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.',
119
161
  inputSchema: materialToolInput,
162
+ actionContract: MATERIAL_CONTRACT,
120
163
  annotations: {
121
164
  readOnlyHint: false,
122
165
  destructiveHint: false,
@@ -126,7 +169,7 @@ export function createKilnMaterialDef(library: MaterialLibrary): KilnToolDef {
126
169
  async run(raw) {
127
170
  assertMaterialJson(raw);
128
171
  const input = materialToolInput.parse(aliasMaterialId(raw));
129
- requireActionFields('kiln_material', input.action, input, REQUIREMENTS[input.action]);
172
+ requireActionFields('kiln_material', input.action, input, MATERIAL_CONTRACT[input.action]);
130
173
  switch (input.action) {
131
174
  case 'presets':
132
175
  return {
@@ -170,13 +213,14 @@ export function createKilnMaterialDef(library: MaterialLibrary): KilnToolDef {
170
213
  };
171
214
  }
172
215
  case 'get': {
173
- const revisionId = input.revisionId ?? (await onlyRevision(library, input.materialId!));
216
+ const revisionId =
217
+ input.revisionId ?? (await onlyRevision(library, input.materialId!, listCall));
174
218
  try {
175
219
  return materialResult((await library.read(input.materialId!, revisionId)).manifest);
176
220
  } catch (error) {
177
221
  // Rule 9: an unknown id names the call that lists the known ones.
178
222
  if ((error as NodeJS.ErrnoException).code === 'ENOENT')
179
- throw unknownMaterial(input.materialId!, revisionId);
223
+ throw unknownMaterial(input.materialId!, revisionId, listCall);
180
224
  throw error;
181
225
  }
182
226
  }