@call-me-sensei/toonlab 0.4.19 → 0.4.21

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 (123) hide show
  1. package/NPM-LIBRARY.md +230 -0
  2. package/README.md +78 -36
  3. package/agents/PROMPTS.md +91 -0
  4. package/agents/README.md +9 -14
  5. package/agents/claude/CLAUDE.md +8 -0
  6. package/agents/codex/AGENTS.md +161 -0
  7. package/agents/cursor/toonlab.mdc +37 -0
  8. package/agents/references/mcp-asset-discovery.md +12 -9
  9. package/agents/references/runtime-entry-points.md +5 -4
  10. package/agents/skills/claude/asset-sourcing/SKILL.md +9 -6
  11. package/agents/skills/claude/rock-ground-shaders/SKILL.md +16 -0
  12. package/agents/skills/claude/vegetation-sky/SKILL.md +7 -4
  13. package/agents/skills/codex/asset-sourcing/SKILL.md +9 -6
  14. package/agents/skills/codex/rock-ground-shaders/SKILL.md +16 -0
  15. package/agents/skills/codex/vegetation-sky/SKILL.md +7 -4
  16. package/cli/toonlab.mjs +3 -2
  17. package/database/fal-patina.mjs +131 -0
  18. package/database/generation-service.mjs +108 -7
  19. package/database/migrations/0007_creation_revisions.sql +80 -0
  20. package/database/migrations/0008_creation_revision_hardening.sql +98 -0
  21. package/database/migrations/0009_creation_revision_delete_integrity.sql +11 -0
  22. package/database/migrations/0010_external_catalog_assets.sql +28 -0
  23. package/database/providers.mjs +195 -16
  24. package/database/repository.mjs +518 -70
  25. package/database/seeds/catalog/0003_2026-08-open-assets.sql +159086 -0
  26. package/database/seeds/catalog/0004_2026-08-c7-rocks.sql +39405 -0
  27. package/mcp/public-catalog.mjs +4 -52
  28. package/mcp/server.mjs +175 -12
  29. package/mcp/vite-plugin.mjs +78 -2
  30. package/package.json +32 -12
  31. package/scripts/generate-catalog-seed.mjs +34 -7
  32. package/src/asset-policy/catalogLicenses.js +64 -6
  33. package/src/assetlib/loadImported.js +15 -1
  34. package/src/catalog/officialCatalogAssetRuntime.js +13 -2
  35. package/src/catalog/officialCatalogLod.js +34 -1
  36. package/src/catalog/officialCatalogPlacement.js +29 -14
  37. package/src/character/animationRetarget.js +2 -0
  38. package/src/character/characterRig.js +6 -1
  39. package/src/character/characterRuntime.js +175 -7
  40. package/src/environment/environmentMaterialAdapter.js +30 -0
  41. package/src/environment/environmentPresets.js +90 -0
  42. package/src/environment/environmentSunShadowPass.js +42 -0
  43. package/src/environment/toonLabSurfaceLighting.js +108 -10
  44. package/src/environment/urbanPropMaterial.js +104 -2
  45. package/src/lighting/lightingSystem.js +65 -10
  46. package/src/renderer/index.js +7 -0
  47. package/src/renderer/styleComparison.js +985 -0
  48. package/src/rock-shader/index.js +5 -0
  49. package/src/rock-shader/rockGeometryDetail.js +701 -0
  50. package/src/rock-shader/rockMaterial.js +782 -39
  51. package/src/rock-shader/rockRegionRuntime.js +185 -0
  52. package/src/rock-shader/rockSemanticMaterialRuntime.js +337 -0
  53. package/src/rock-shader/rockShaderRuntime.js +354 -67
  54. package/src/rock-shader/rockShaderSettings.js +246 -16
  55. package/src/rock-shader/rockTangentIntegrity.js +136 -0
  56. package/src/rock-shader/rockTextureIntegrity.js +115 -0
  57. package/src/rockgen/index.js +1 -0
  58. package/src/rockgen/rockDocument.js +27 -3
  59. package/src/rockgen/surface/c7GeologySurface.js +372 -0
  60. package/src/shaders-tsl/water.js +8 -0
  61. package/src/sky/cloudShadow.js +25 -1
  62. package/src/sky/skySystem.js +9 -0
  63. package/src/styles/index.js +23 -0
  64. package/src/styles/neutralStylePresets.js +712 -0
  65. package/src/styles/sceneStyleRuntime.js +51 -4
  66. package/src/styles/styleAdapters.js +13 -2
  67. package/src/styles/styleBundle.js +1 -1
  68. package/src/texgen/evaluateTexture.js +30 -3
  69. package/src/texgen/textureGenerators.js +63 -31
  70. package/src/texgen/textureSettings.js +25 -8
  71. package/src/toon/toonSettings.js +96 -2
  72. package/src/vegetation/branchTree.js +327 -42
  73. package/src/vegetation/index.js +24 -2
  74. package/src/vegetation/scatter.js +372 -1
  75. package/src/vegetation/stylizedTree.js +312 -126
  76. package/src/vegetation/stylizedTreeFoliage.js +229 -7
  77. package/src/vegetation/treeSurfaceTextures.js +328 -0
  78. package/src/version.js +1 -1
  79. package/src/water/waterSettings.js +15 -7
  80. package/types/asset-policy/catalogLicenses.d.ts +2 -0
  81. package/types/asset-policy/index.d.ts +2 -0
  82. package/types/assetlib/loadImported.d.ts +3 -1
  83. package/types/catalog/officialCatalogLod.d.ts +63 -5
  84. package/types/character/characterRig.d.ts +59 -0
  85. package/types/character/characterRuntime.d.ts +1 -0
  86. package/types/environment/environmentMaterialAdapter.d.ts +3 -1
  87. package/types/environment/toonLabSurfaceLighting.d.ts +20 -3
  88. package/types/index.d.ts +89 -7
  89. package/types/lighting/lightingSystem.d.ts +7 -0
  90. package/types/react/index.d.ts +20 -0
  91. package/types/renderer/index.d.ts +1 -0
  92. package/types/renderer/styleComparison.d.ts +409 -0
  93. package/types/rock-shader/index.d.ts +5 -0
  94. package/types/rock-shader/rockGeometryDetail.d.ts +300 -0
  95. package/types/rock-shader/rockMaterial.d.ts +3 -0
  96. package/types/rock-shader/rockRegionRuntime.d.ts +86 -0
  97. package/types/rock-shader/rockSemanticMaterialRuntime.d.ts +29 -0
  98. package/types/rock-shader/rockShaderRuntime.d.ts +2 -0
  99. package/types/rock-shader/rockShaderSettings.d.ts +19 -5
  100. package/types/rock-shader/rockTangentIntegrity.d.ts +51 -0
  101. package/types/rock-shader/rockTextureIntegrity.d.ts +61 -0
  102. package/types/rockgen/index.d.ts +1 -0
  103. package/types/rockgen/rockDocument.d.ts +29 -1
  104. package/types/rockgen/surface/c7GeologySurface.d.ts +8 -0
  105. package/types/sky/cloudShadow.d.ts +1 -0
  106. package/types/styles/index.d.ts +1 -0
  107. package/types/styles/neutralStylePresets.d.ts +944 -0
  108. package/types/styles/sceneStyleRuntime.d.ts +20 -0
  109. package/types/vegetation/branchTree.d.ts +4 -0
  110. package/types/vegetation/index.d.ts +9 -7
  111. package/types/vegetation/scatter.d.ts +1 -0
  112. package/types/vegetation/stylizedTree.d.ts +1 -2
  113. package/types/vegetation/stylizedTreeFoliage.d.ts +1 -0
  114. package/types/version.d.ts +1 -1
  115. package/agents/references/geology-playbook.md +0 -118
  116. package/agents/skills/claude/karst-cliff-construction/SKILL.md +0 -258
  117. package/agents/skills/claude/outdoor-world/SKILL.md +0 -492
  118. package/agents/skills/claude/outdoor-world/references/coastal-landform-formation.md +0 -100
  119. package/agents/skills/codex/karst-cliff-construction/SKILL.md +0 -258
  120. package/agents/skills/codex/outdoor-world/SKILL.md +0 -492
  121. package/agents/skills/codex/outdoor-world/references/coastal-landform-formation.md +0 -100
  122. package/src/vegetation/legacyTreePresets.js +0 -120
  123. package/types/vegetation/legacyTreePresets.d.ts +0 -22
@@ -25,62 +25,14 @@ function entry(input) {
25
25
  });
26
26
  }
27
27
 
28
- const CURATED_PLANT_RECIPES = Object.freeze([
29
- entry({
30
- cluster: 'vegetation',
31
- description: 'Mid-size broadleaf starting recipe for anime environments.',
32
- id: 'tree/broadleaf/sensei',
33
- kind: 'recipe',
34
- label: 'Sensei broadleaf',
35
- recipe: {
36
- options: { seed: 3, size: 3.2 },
37
- schema: 'treeRecipe',
38
- type: 'tree',
39
- version: 1,
40
- },
41
- spawn: 'createPlantFromRecipe(entry.recipe)',
42
- tags: ['tree', 'vegetation', 'anime'],
43
- thumbnail: thumbnailFor('tree/broadleaf/sensei'),
44
- }),
45
- entry({
46
- cluster: 'vegetation',
47
- description: 'Ground-hugging foliage mass for yards and path edges.',
48
- id: 'tree/bush/hedge',
49
- kind: 'recipe',
50
- label: 'Hedge bush',
51
- recipe: {
52
- options: { seed: 5, size: 1.2 },
53
- schema: 'treeRecipe',
54
- type: 'bush',
55
- version: 1,
56
- },
57
- spawn: 'createPlantFromRecipe(entry.recipe)',
58
- tags: ['bush', 'vegetation', 'anime'],
59
- thumbnail: thumbnailFor('tree/bush/hedge'),
60
- }),
61
- entry({
62
- cluster: 'vegetation',
63
- description: 'Flowering plant with daisy heads.',
64
- id: 'tree/flower/daisy',
65
- kind: 'recipe',
66
- label: 'Daisy flower',
67
- recipe: {
68
- options: { seed: 9, size: 0.5, species: 'daisy' },
69
- schema: 'treeRecipe',
70
- type: 'flower',
71
- version: 1,
72
- },
73
- spawn: 'createPlantFromRecipe(entry.recipe)',
74
- tags: ['flower', 'vegetation', 'anime'],
75
- thumbnail: thumbnailFor('tree/flower/daisy'),
76
- }),
77
- ]);
78
-
79
28
  let cached = null;
80
29
 
81
30
  export function publicMcpCatalogEntries() {
82
31
  if (cached) return cached;
83
- const entries = [...CURATED_PLANT_RECIPES];
32
+ // Authored vegetation recipes are Gallery assets, not package built-ins.
33
+ // The packaged MCP discovers them from the public Gallery so provenance,
34
+ // license, review state, and immutable documents stay attached.
35
+ const entries = [];
84
36
 
85
37
  for (const option of getRockgenPresetOptions()) {
86
38
  const id = `rock/${slug(option.value)}`;
package/mcp/server.mjs CHANGED
@@ -82,6 +82,8 @@ const SERVER_NAME = 'toonlab-oss';
82
82
  const SERVER_VERSION = JSON.parse(
83
83
  readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
84
84
  ).version;
85
+ const PUBLIC_GALLERY_ENDPOINT = process.env.TOONLAB_PUBLIC_GALLERY_URL
86
+ ?? 'https://toonlab.io/api/v1/creations';
85
87
  const LATEST_PROTOCOL_VERSION = '2025-11-25';
86
88
  const SUPPORTED_PROTOCOL_VERSIONS = new Set([
87
89
  LATEST_PROTOCOL_VERSION,
@@ -103,7 +105,9 @@ function parseArguments(argv) {
103
105
  }
104
106
 
105
107
  const { workspace } = parseArguments(process.argv.slice(2));
106
- const legacyWorkspace = process.env.TOONLAB_LEGACY_WORKSPACE === '1';
108
+ const legacyWorkspaceMode = process.env.TOONLAB_LEGACY_WORKSPACE;
109
+ const legacyWorkspace = legacyWorkspaceMode === '1'
110
+ || (legacyWorkspaceMode !== '0' && !process.env.DATABASE_URL);
107
111
 
108
112
  async function getWorkspaceInfo() {
109
113
  if (legacyWorkspace) return getLegacyWorkspaceInfo(workspace);
@@ -287,6 +291,112 @@ function normalizeDiscovery(entry, summary, { domain = '', policy = null } = {})
287
291
  };
288
292
  }
289
293
 
294
+ function publicGalleryUrl(path = '') {
295
+ let endpoint;
296
+ try {
297
+ endpoint = new URL(PUBLIC_GALLERY_ENDPOINT);
298
+ } catch {
299
+ throw new Error('TOONLAB_PUBLIC_GALLERY_URL must be an absolute HTTP(S) URL.');
300
+ }
301
+ if (!['http:', 'https:'].includes(endpoint.protocol)) {
302
+ throw new Error('TOONLAB_PUBLIC_GALLERY_URL must use HTTP(S).');
303
+ }
304
+ endpoint.search = '';
305
+ endpoint.hash = '';
306
+ if (!path) return endpoint;
307
+ endpoint.pathname = `${endpoint.pathname.replace(/\/$/u, '')}/${encodeURIComponent(path)}`;
308
+ return endpoint;
309
+ }
310
+
311
+ function absoluteGalleryReference(reference) {
312
+ if (typeof reference !== 'string' || !reference.trim()) return null;
313
+ return new URL(reference, publicGalleryUrl()).href;
314
+ }
315
+
316
+ async function fetchPublicGalleryJson(url) {
317
+ let response;
318
+ try {
319
+ response = await fetch(url, {
320
+ headers: { accept: 'application/json' },
321
+ signal: AbortSignal.timeout(15_000),
322
+ });
323
+ } catch (cause) {
324
+ throw new Error(`ToonLab public Gallery request failed: ${cause?.message ?? String(cause)}`);
325
+ }
326
+ if (!response.ok) {
327
+ throw new Error(`ToonLab public Gallery returned HTTP ${response.status}.`);
328
+ }
329
+ try {
330
+ return await response.json();
331
+ } catch {
332
+ throw new Error('ToonLab public Gallery returned invalid JSON.');
333
+ }
334
+ }
335
+
336
+ function normalizePublicGalleryItem(item, args = {}) {
337
+ const id = String(item?.id ?? '').trim();
338
+ if (!id) throw new Error('ToonLab public Gallery returned an item without an id.');
339
+ const type = String(item.type ?? 'creation');
340
+ const entry = {
341
+ attribution: item.attribution ?? null,
342
+ author: item.author ?? null,
343
+ cluster: type,
344
+ description: item.description ?? null,
345
+ documentUrl: absoluteGalleryReference(item.download ?? item.documentUrl),
346
+ id,
347
+ kind: type,
348
+ label: String(item.label ?? id),
349
+ license: item.license ?? null,
350
+ source: 'gallery',
351
+ sourceClass: 'toonlab-gallery',
352
+ tags: Array.isArray(item.tags) ? item.tags.map(String) : [],
353
+ thumbnail: absoluteGalleryReference(item.thumbnail),
354
+ updatedAt: item.updated_at ?? null,
355
+ url: absoluteGalleryReference(item.url ?? `/c/${item.slug ?? id}`),
356
+ };
357
+ return normalizeDiscovery(entry, entry, args);
358
+ }
359
+
360
+ async function searchPublicGallery(args = {}) {
361
+ const limit = limitValue(args.limit, 20, 60);
362
+ const offset = Math.max(0, Math.floor(Number(args.offset) || 0));
363
+ const endpoint = publicGalleryUrl();
364
+ endpoint.searchParams.set('limit', String(limit));
365
+ endpoint.searchParams.set('offset', String(offset));
366
+ if (String(args.query ?? '').trim()) endpoint.searchParams.set('q', String(args.query).trim());
367
+ if (String(args.type ?? '').trim()) endpoint.searchParams.set('type', String(args.type).trim());
368
+ const response = await fetchPublicGalleryJson(endpoint);
369
+ if (!Array.isArray(response?.items)) {
370
+ throw new Error('ToonLab public Gallery response is missing items.');
371
+ }
372
+ const items = response.items.map((item) => normalizePublicGalleryItem(item, args));
373
+ return {
374
+ count: items.length,
375
+ items,
376
+ limit,
377
+ nextOffset: items.length === limit ? offset + items.length : null,
378
+ offset,
379
+ source: 'toonlab-public-gallery',
380
+ };
381
+ }
382
+
383
+ async function getPublicGalleryAsset(args = {}) {
384
+ const id = String(args.id ?? '').trim();
385
+ if (!id) throw new Error('A public Gallery asset id or slug is required.');
386
+ const item = await fetchPublicGalleryJson(publicGalleryUrl(id));
387
+ const normalized = normalizePublicGalleryItem({
388
+ ...item,
389
+ download: item.documentUrl ?? null,
390
+ thumbnail: item.thumbnail ?? null,
391
+ url: item.url ?? `/c/${item.slug ?? id}`,
392
+ }, args);
393
+ return {
394
+ ...normalized,
395
+ document: item.document ?? null,
396
+ files: Array.isArray(item.files) ? item.files : [],
397
+ };
398
+ }
399
+
290
400
  function summarizeCatalogEntry(entry) {
291
401
  const metadata = entry.metadata && typeof entry.metadata === 'object' && !Array.isArray(entry.metadata)
292
402
  ? entry.metadata
@@ -987,7 +1097,7 @@ const TOOLS = [
987
1097
  },
988
1098
  {
989
1099
  annotations: { readOnlyHint: true },
990
- description: 'Search built-in procedural assets, the paginated complete official Gallery catalog, saved library entries, lab presets, and files on disk. Official rock summaries include compact dimensions and taxonomy metadata; follow nextOffset until null.',
1100
+ description: 'Search approved built-in generator/settings entries, the local released official catalog, saved library entries, lab presets, and files on disk. Official rock summaries include compact dimensions and taxonomy metadata; follow nextOffset until null.',
991
1101
  inputSchema: {
992
1102
  additionalProperties: false,
993
1103
  properties: {
@@ -1006,6 +1116,40 @@ const TOOLS = [
1006
1116
  name: 'search_assets',
1007
1117
  title: 'Search ToonLab assets',
1008
1118
  },
1119
+ {
1120
+ annotations: { openWorldHint: true, readOnlyHint: true },
1121
+ description: 'Search public ToonLab Gallery creations with their license, attribution, tags, thumbnail, and immutable portable-document URL. Authored tree recipes live here rather than in npm; use type="tree-recipe" and follow nextOffset until null.',
1122
+ inputSchema: {
1123
+ additionalProperties: false,
1124
+ properties: {
1125
+ domain: { type: 'string' },
1126
+ limit: { maximum: 60, minimum: 1, type: 'integer' },
1127
+ offset: { minimum: 0, type: 'integer' },
1128
+ policy: { type: 'object' },
1129
+ query: { type: 'string' },
1130
+ type: { type: 'string' },
1131
+ },
1132
+ type: 'object',
1133
+ },
1134
+ name: 'search_public_gallery',
1135
+ title: 'Search the ToonLab public Gallery',
1136
+ },
1137
+ {
1138
+ annotations: { openWorldHint: true, readOnlyHint: true },
1139
+ description: 'Get one public ToonLab Gallery creation by publication id or slug, including its complete portable document and immutable public files.',
1140
+ inputSchema: {
1141
+ additionalProperties: false,
1142
+ properties: {
1143
+ domain: { type: 'string' },
1144
+ id: { type: 'string' },
1145
+ policy: { type: 'object' },
1146
+ },
1147
+ required: ['id'],
1148
+ type: 'object',
1149
+ },
1150
+ name: 'get_public_gallery_asset',
1151
+ title: 'Get a ToonLab public Gallery asset',
1152
+ },
1009
1153
  {
1010
1154
  annotations: { readOnlyHint: true },
1011
1155
  description: 'Get a complete asset, recipe, preset, library entry, or workspace file descriptor by id.',
@@ -1202,7 +1346,7 @@ const TOOLS = [
1202
1346
  },
1203
1347
  {
1204
1348
  annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
1205
- description: 'Start local managed image/3D generation with user-configured provider keys. Meshy 7 supports image/multiview-to-3D; text_to_model + Meshy first creates a concept image with image_model, then submits that PNG/JPEG to Meshy as one trackable job.',
1349
+ description: 'Start local managed image, Fal Patina PBR material, or 3D generation with user-configured provider keys. Material jobs preserve the seamless preview and typed PBR maps as one durable local bundle.',
1206
1350
  inputSchema: {
1207
1351
  additionalProperties: false,
1208
1352
  properties: {
@@ -1210,12 +1354,27 @@ const TOOLS = [
1210
1354
  domain: { maxLength: 120, minLength: 1, type: 'string' },
1211
1355
  gap_id: { description: 'ID returned by record_asset_gap.', minLength: 1, type: 'string' },
1212
1356
  image_model: { enum: ['nano-banana-2-lite', 'nano-banana-2', 'nano-banana-pro'], type: 'string' },
1213
- kind: { enum: ['image', 'texture_image', 'concept_image', 'text_to_model', 'image_to_model', 'multiview_to_model', 'model_segment'], type: 'string' },
1357
+ kind: { enum: ['image', 'texture_image', 'concept_image', 'material', 'text_to_model', 'image_to_model', 'multiview_to_model', 'model_segment'], type: 'string' },
1214
1358
  model_provider: { default: 'meshy', enum: ['meshy', 'tripo'], type: 'string' },
1215
1359
  policy: { description: 'Developer-approved asset-sourcing policy.', type: 'object' },
1216
1360
  prompt: { maxLength: 600, type: 'string' },
1217
1361
  reference_paths: { items: { type: 'string' }, maxItems: 6, type: 'array' },
1218
- resolution: { default: '1k', enum: ['1k', '2k', '4k'], type: 'string' },
1362
+ resolution: { default: '1k', enum: ['1k', '2k', '4k', '8k'], type: 'string' },
1363
+ maps: { description: 'material only; empty skips PBR estimation.', items: { enum: ['basecolor', 'normal', 'roughness', 'metalness', 'height'], type: 'string' }, maxItems: 5, type: 'array' },
1364
+ image_size: { description: 'material only; a Patina preset or {width,height}.', oneOf: [{ enum: ['square_hd', 'square', 'portrait_4_3', 'portrait_16_9', 'landscape_4_3', 'landscape_16_9'], type: 'string' }, { additionalProperties: false, properties: { width: { maximum: 2048, minimum: 256, type: 'integer' }, height: { maximum: 2048, minimum: 256, type: 'integer' } }, required: ['width', 'height'], type: 'object' }] },
1365
+ upscale_factor: { enum: [0, 2, 4], type: 'integer' },
1366
+ tiling_mode: { enum: ['both', 'horizontal', 'vertical'], type: 'string' },
1367
+ output_format: { enum: ['png', 'jpeg', 'webp'], type: 'string' },
1368
+ num_images: { maximum: 4, minimum: 1, type: 'integer' },
1369
+ num_inference_steps: { maximum: 8, minimum: 1, type: 'integer' },
1370
+ enable_prompt_expansion: { type: 'boolean' },
1371
+ enable_safety_checker: { type: 'boolean' },
1372
+ tile_size: { maximum: 256, minimum: 32, type: 'integer' },
1373
+ tile_stride: { maximum: 128, minimum: 16, type: 'integer' },
1374
+ seed: { type: 'integer' },
1375
+ image_url: { description: 'material only; public HTTPS source texture.', format: 'uri', type: 'string' },
1376
+ mask_url: { description: 'material only; public HTTPS inpainting mask. Requires image_url.', format: 'uri', type: 'string' },
1377
+ strength: { maximum: 1, minimum: 0, type: 'number' },
1219
1378
  source_image_path: { description: 'Workspace object path for image_to_model.', type: 'string' },
1220
1379
  source_job_id: { description: 'Succeeded image job for image_to_model, or Tripo model job for model_segment.', type: 'string' },
1221
1380
  style: { default: 'stylized', enum: ['stylized', 'cartoon', 'raw'], type: 'string' },
@@ -1225,7 +1384,7 @@ const TOOLS = [
1225
1384
  type: 'object',
1226
1385
  },
1227
1386
  name: 'generate_ai_asset',
1228
- title: 'Generate an image or 3D asset with local provider keys',
1387
+ title: 'Generate an image, PBR material, or 3D asset with local provider keys',
1229
1388
  },
1230
1389
  {
1231
1390
  annotations: { readOnlyHint: true, openWorldHint: true },
@@ -1257,7 +1416,7 @@ const TOOLS = [
1257
1416
  },
1258
1417
  {
1259
1418
  annotations: { destructiveHint: false, idempotentHint: true },
1260
- description: 'Save a succeeded local 3D generation job into the editable ToonLab library with its provider provenance and local model file.',
1419
+ description: 'Save a succeeded local 3D model or PBR material job into the editable ToonLab library with provider provenance and durable local files.',
1261
1420
  inputSchema: {
1262
1421
  additionalProperties: false,
1263
1422
  properties: { job_id: { type: 'string' }, label: { maxLength: 120, type: 'string' } },
@@ -1265,7 +1424,7 @@ const TOOLS = [
1265
1424
  type: 'object',
1266
1425
  },
1267
1426
  name: 'save_generated_asset',
1268
- title: 'Save a generated 3D asset',
1427
+ title: 'Save a generated model or material',
1269
1428
  },
1270
1429
  {
1271
1430
  annotations: { openWorldHint: true, readOnlyHint: true },
@@ -1398,6 +1557,8 @@ async function callTool(name, args = {}) {
1398
1557
  case 'get_lab_features': return getLabFeatures(args.lab);
1399
1558
  case 'create_lab_document': return createLabDocumentTool(args);
1400
1559
  case 'search_assets': return searchAssets(args);
1560
+ case 'search_public_gallery': return searchPublicGallery(args);
1561
+ case 'get_public_gallery_asset': return getPublicGalleryAsset(args);
1401
1562
  case 'get_asset': return getAsset(args);
1402
1563
  case 'list_my_creations': return listMyCreations(args);
1403
1564
  case 'get_my_creation': return getMyCreation(args);
@@ -1423,8 +1584,8 @@ async function callTool(name, args = {}) {
1423
1584
  policyDecision,
1424
1585
  pollAfterMs: 5_000,
1425
1586
  next: job.status === 'running'
1426
- ? 'Poll get_generation_job until succeeded, failed, or cancelled; then pass a succeeded 3D job to save_generated_asset.'
1427
- : 'The provider completed inline. Save a succeeded 3D job with save_generated_asset.',
1587
+ ? 'Poll get_generation_job until succeeded, failed, or cancelled; then save a succeeded model or material job if it belongs in the Library.'
1588
+ : 'The provider completed inline. Save a succeeded model or material job if it belongs in the Library.',
1428
1589
  };
1429
1590
  }
1430
1591
  case 'get_generation_job': {
@@ -1461,13 +1622,15 @@ async function callTool(name, args = {}) {
1461
1622
  case 'get_generation_capabilities': return {
1462
1623
  managedLocal: {
1463
1624
  ...providerConfiguration(),
1464
- kinds: ['image', 'texture_image', 'concept_image', 'text_to_model', 'image_to_model', 'multiview_to_model', 'model_segment'],
1625
+ kinds: ['image', 'texture_image', 'concept_image', 'material', 'text_to_model', 'image_to_model', 'multiview_to_model', 'model_segment'],
1626
+ patinaMaterialFlow: 'prompt or source image -> seamless preview + typed PBR map bundle (1k-8k)',
1465
1627
  meshyTextFlow: 'selected image_model concept_image -> meshy-7 image_to_model',
1466
1628
  tools: ['generate_ai_asset', 'get_generation_job', 'get_generation_jobs', 'save_generated_asset'],
1467
1629
  },
1468
1630
  available: [
1469
1631
  'Generate deterministic recipes from the stable public ToonLab catalog entries.',
1470
1632
  'Save and retrieve editable lab documents through the shared .toonlab workspace.',
1633
+ 'Search and retrieve authored public Gallery recipes with license and provenance.',
1471
1634
  'Search and import CC0 assets from Poly Haven and ambientCG.',
1472
1635
  'Use each recipe\'s spawn snippet to integrate it into a Three.js project.',
1473
1636
  ],
@@ -1518,7 +1681,7 @@ async function handleRequest(message) {
1518
1681
  jsonrpc: '2.0',
1519
1682
  result: {
1520
1683
  capabilities: { resources: {}, tools: {} },
1521
- instructions: 'ToonLab builds anime-style games, characters, and environments for Three.js. Call list_live_labs and get_lab_features when authoring a Lab artifact, use create_lab_document for a valid starter, and prefer mutate_lab_creation over hand-editing structural arrays. Official Rock details include a source-GLB starter. This OSS server may edit any non-secret creation or lab-state entry in its local workspace; deletions require explicit confirmation. Load the selected style bundle first. If no asset-sourcing policy exists, ask the developer, then continue with library-first advisory discovery while recording the unresolved decision. Search the project and ToonLab libraries before public sources; generate or hand-author only when policy permits. Validate every candidate, preserve provenance, evaluate anime-fit in scene, and record a gap before adding a custom shader, texture, model, or adapter. When local provider keys are configured, generate_ai_asset exposes image generation, Meshy 7 image/multiview-to-3D, and Text -> selected image model -> Meshy 7 through MCP.',
1684
+ instructions: 'ToonLab builds anime-style games, characters, and environments for Three.js. Call list_live_labs and get_lab_features when authoring a Lab artifact, use create_lab_document for a valid starter, and prefer mutate_lab_creation over hand-editing structural arrays. Official Rock details include a source-GLB starter. This OSS server may edit any non-secret creation or lab-state entry in its local workspace; deletions require explicit confirmation. Load the selected style bundle first. If no asset-sourcing policy exists, ask the developer, then continue with library-first advisory discovery while recording the unresolved decision. Search the project and local library first, then search_public_gallery for reviewed authored assets such as tree recipes, before external sources. Generate or hand-author only when policy permits. Validate every candidate, preserve provenance, evaluate anime-fit in scene, and record a gap before adding a custom shader, texture, model, or adapter. When local provider keys are configured, generate_ai_asset exposes image generation, Meshy 7 image/multiview-to-3D, and Text -> selected image model -> Meshy 7 through MCP.',
1522
1685
  protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.has(requested) ? requested : LATEST_PROTOCOL_VERSION,
1523
1686
  serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
1524
1687
  },
@@ -3,6 +3,7 @@ import { join, resolve } from 'node:path';
3
3
  import { readWorkspaceFile, resolveWorkspacePath } from './workspace.mjs';
4
4
  import {
5
5
  clearLabState,
6
+ annotateCreationRevision,
6
7
  cancelGenerationJob,
7
8
  createGenerationJob,
8
9
  databaseInfo,
@@ -11,14 +12,19 @@ import {
11
12
  hasLegacyImport,
12
13
  finishGenerationJob,
13
14
  getCatalogAsset,
15
+ getCreationRevision,
14
16
  getGenerationJob,
15
17
  listCatalogAssets,
18
+ listCatalogFacets,
16
19
  listGenerationJobs,
20
+ listCreationRevisions,
17
21
  listLibraryEntries,
18
22
  migrateLegacy,
19
23
  providerConfiguration,
20
24
  readLabState,
21
25
  readObject,
26
+ resolveStyleBundleEntry,
27
+ restoreCreationRevision,
22
28
  saveLibraryEntry,
23
29
  saveObject,
24
30
  setLabState,
@@ -30,6 +36,7 @@ import {
30
36
  pollTripoTask,
31
37
  runProvider,
32
38
  } from '../database/providers.mjs';
39
+ import { storeManagedMaterial } from '../database/generation-service.mjs';
33
40
 
34
41
  const JSON_LIMIT = 8 * 1024 * 1024;
35
42
  const FILE_LIMIT = 128 * 1024 * 1024;
@@ -97,6 +104,17 @@ function clientJob(job) {
97
104
  ...job.result,
98
105
  ...(job.result.file ? { file: clientFile(job.result.file) } : {}),
99
106
  ...(job.result.previewFile ? { previewFile: clientFile(job.result.previewFile) } : {}),
107
+ ...(job.result.materialBundle ? {
108
+ materialBundle: {
109
+ ...job.result.materialBundle,
110
+ manifestFile: clientFile(job.result.materialBundle.manifestFile),
111
+ variants: (job.result.materialBundle.variants ?? []).map((variant) => ({
112
+ ...variant,
113
+ preview: clientFile(variant.preview),
114
+ maps: Object.fromEntries(Object.entries(variant.maps ?? {}).map(([key, file]) => [key, clientFile(file)])),
115
+ })),
116
+ },
117
+ } : {}),
100
118
  }
101
119
  : job.result;
102
120
  return {
@@ -125,17 +143,18 @@ export function toonlabWorkspacePlugin({ rootDirectory = process.cwd(), workspac
125
143
  async function runGeneration(body) {
126
144
  const provider = String(body.provider ?? '');
127
145
  const kind = String(body.kind ?? '');
128
- if (!['ark', 'gemini', 'meshy', 'openai', 'tripo'].includes(provider)) {
146
+ if (!['ark', 'fal', 'gemini', 'meshy', 'openai', 'tripo'].includes(provider)) {
129
147
  throw Object.assign(new Error('Unsupported provider.'), { statusCode: 400 });
130
148
  }
131
149
  const imageKinds = new Set(['image', 'texture_image', 'concept_image']);
150
+ const materialKinds = new Set(['material']);
132
151
  const modelKinds = new Set([
133
152
  'text_to_model',
134
153
  'image_to_model',
135
154
  'multiview_to_model',
136
155
  'model_segment',
137
156
  ]);
138
- if (!imageKinds.has(kind) && !modelKinds.has(kind)) {
157
+ if (!imageKinds.has(kind) && !modelKinds.has(kind) && !materialKinds.has(kind)) {
139
158
  throw Object.assign(new Error('Unsupported generation kind.'), { statusCode: 400 });
140
159
  }
141
160
  const providerKinds = {
@@ -148,6 +167,9 @@ export function toonlabWorkspacePlugin({ rootDirectory = process.cwd(), workspac
148
167
  if (imageKinds.has(kind) && provider !== 'gemini') {
149
168
  throw Object.assign(new Error('OSS image generation currently supports Gemini only.'), { statusCode: 400 });
150
169
  }
170
+ if (materialKinds.has(kind) && provider !== 'fal') {
171
+ throw Object.assign(new Error('PBR material generation requires Fal Patina.'), { statusCode: 400 });
172
+ }
151
173
  const requestBody = body.request && typeof body.request === 'object'
152
174
  ? body.request
153
175
  : {};
@@ -213,6 +235,14 @@ export function toonlabWorkspacePlugin({ rootDirectory = process.cwd(), workspac
213
235
  status: 200,
214
236
  };
215
237
  }
238
+ if (result.material) {
239
+ return {
240
+ job: clientJob(await finishGenerationJob(job.id, {
241
+ result: await storeManagedMaterial(workspacePath, job.id, result),
242
+ })),
243
+ status: 200,
244
+ };
245
+ }
216
246
  return {
217
247
  job: clientJob(await finishGenerationJob(job.id, { result })),
218
248
  status: 200,
@@ -411,13 +441,20 @@ export function toonlabWorkspacePlugin({ rootDirectory = process.cwd(), workspac
411
441
  if (request.method !== 'GET') return methodNotAllowed(response, ['GET']);
412
442
  return sendJson(response, 200, await listCatalogAssets({
413
443
  kind: url.searchParams.get('kind') ?? '',
444
+ license: url.searchParams.get('license') ?? '',
414
445
  limit: url.searchParams.get('limit') ?? 60,
415
446
  offset: url.searchParams.get('offset') ?? 0,
416
447
  q: url.searchParams.get('q') ?? '',
448
+ size: url.searchParams.get('size') ?? '',
417
449
  source: url.searchParams.get('source') ?? '',
418
450
  }));
419
451
  }
420
452
 
453
+ if (url.pathname === '/api/toonlab/catalog-facets') {
454
+ if (request.method !== 'GET') return methodNotAllowed(response, ['GET']);
455
+ return sendJson(response, 200, await listCatalogFacets());
456
+ }
457
+
421
458
  if (url.pathname.startsWith('/api/toonlab/catalog/')) {
422
459
  if (request.method !== 'GET') return methodNotAllowed(response, ['GET']);
423
460
  const id = decodeURIComponent(url.pathname.slice('/api/toonlab/catalog/'.length));
@@ -513,6 +550,45 @@ export function toonlabWorkspacePlugin({ rootDirectory = process.cwd(), workspac
513
550
  }
514
551
 
515
552
  if (url.pathname.startsWith('/api/toonlab/library/')) {
553
+ const resolvedMatch = url.pathname.match(/^\/api\/toonlab\/library\/([^/]+)\/resolved$/);
554
+ if (resolvedMatch) {
555
+ if (request.method !== 'GET') return methodNotAllowed(response, ['GET']);
556
+ return sendJson(response, 200, {
557
+ bundle: await resolveStyleBundleEntry(decodeURIComponent(resolvedMatch[1])),
558
+ });
559
+ }
560
+ const revisionMatch = url.pathname.match(/^\/api\/toonlab\/library\/([^/]+)\/revisions(?:\/([^/]+))?(?:\/(restore))?$/);
561
+ if (revisionMatch) {
562
+ const creationId = decodeURIComponent(revisionMatch[1]);
563
+ const revisionId = revisionMatch[2] ? decodeURIComponent(revisionMatch[2]) : null;
564
+ if (!revisionId && request.method === 'GET') {
565
+ const history = await listCreationRevisions(creationId, {
566
+ limit: url.searchParams.get('limit'),
567
+ offset: url.searchParams.get('offset'),
568
+ });
569
+ return sendJson(response, 200, history);
570
+ }
571
+ if (revisionId && !revisionMatch[3] && request.method === 'GET') {
572
+ return sendJson(response, 200, {
573
+ revision: await getCreationRevision(creationId, revisionId),
574
+ });
575
+ }
576
+ if (revisionId && revisionMatch[3] === 'restore' && request.method === 'POST') {
577
+ return sendJson(response, 200, {
578
+ entry: await restoreCreationRevision(creationId, revisionId),
579
+ });
580
+ }
581
+ if (revisionId && !revisionMatch[3] && request.method === 'PATCH') {
582
+ return sendJson(response, 200, {
583
+ revision: await annotateCreationRevision(
584
+ creationId,
585
+ revisionId,
586
+ await readJsonBody(request),
587
+ ),
588
+ });
589
+ }
590
+ return methodNotAllowed(response, revisionId ? ['GET', 'PATCH', 'POST'] : ['GET']);
591
+ }
516
592
  const id = decodeURIComponent(url.pathname.slice('/api/toonlab/library/'.length));
517
593
  if (request.method === 'PUT') {
518
594
  const entry = await readJsonBody(request);