@call-me-sensei/toonlab 0.2.0 → 0.3.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.
- package/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +234 -34
- package/docs/characters.md +144 -0
- package/docs/debug-panel.md +126 -0
- package/docs/docs.css +589 -0
- package/docs/environment.md +186 -0
- package/docs/getting-started.md +180 -0
- package/docs/index.html +14 -0
- package/docs/lab-architecture.md +100 -0
- package/docs/lighting.md +825 -0
- package/docs/main.jsx +739 -0
- package/docs/mcp.md +97 -0
- package/docs/post-processing.md +98 -0
- package/docs/settings-reference.md +1878 -0
- package/docs/shader-constants.md +77 -0
- package/docs/sky.md +182 -0
- package/docs/style-labs.md +309 -0
- package/docs/texture-lab.md +135 -0
- package/docs/toon-shading.md +183 -0
- package/docs/tsl-conventions.md +167 -0
- package/docs/vegetation-sky.md +275 -0
- package/docs/water.md +430 -0
- package/docs/weather.md +200 -0
- package/docs/world-scale.md +111 -0
- package/mcp/server.mjs +610 -0
- package/mcp/style-lab-tools.mjs +371 -0
- package/mcp/vite-plugin.mjs +174 -0
- package/mcp/workspace.mjs +397 -0
- package/package.json +64 -5
- package/src/ambientfx/INTEGRATION.md +164 -0
- package/src/ambientfx/ambientFxPresets.js +83 -0
- package/src/ambientfx/ambientFxSettings.js +368 -0
- package/src/ambientfx/emitters.js +169 -0
- package/src/ambientfx/index.js +5 -0
- package/src/ambientfx/particleBackbone.js +493 -0
- package/src/ambientfx/stylizedAmbientFx.js +450 -0
- package/src/assetlib/ambientcg.js +117 -0
- package/src/assetlib/assetRef.js +91 -0
- package/src/assetlib/importedEntry.js +40 -0
- package/src/assetlib/index.js +23 -0
- package/src/assetlib/kaykit.js +192 -0
- package/src/assetlib/kaykitStaticIndex.js +700 -0
- package/src/assetlib/loadImported.js +143 -0
- package/src/assetlib/opensource3d.js +113 -0
- package/src/assetlib/polyhaven.js +182 -0
- package/src/assetlib/polypizza.js +115 -0
- package/src/assetlib/smithsonian.js +214 -0
- package/src/assetlib/sources.js +279 -0
- package/src/assetlib/zip.js +58 -0
- package/src/biome/biomeGenerator.js +385 -0
- package/src/biome/biomeRuntime.js +299 -0
- package/src/biome/index.js +2 -0
- package/src/buildinggen/buildingAsset.js +44 -0
- package/src/buildinggen/buildingGrammar.js +311 -0
- package/src/buildinggen/buildingMesh.js +451 -0
- package/src/buildinggen/buildingPresets.js +46 -0
- package/src/buildinggen/buildingRecipe.js +100 -0
- package/src/buildinggen/buildingSettings.js +238 -0
- package/src/buildinggen/index.js +6 -0
- package/src/camera/cameraDirector.js +157 -0
- package/src/camera/cameraGenerator.js +367 -0
- package/src/camera/cameraRig.js +570 -0
- package/src/camera/cameraSettings.js +236 -0
- package/src/camera/index.js +7 -0
- package/src/catalog/builtinEntries.js +245 -0
- package/src/catalog/catalog.js +210 -0
- package/src/catalog/index.js +3 -0
- package/src/catalog/manifest.js +84 -0
- package/src/core/generation.js +529 -0
- package/src/environment/environmentRigs.js +21 -1
- package/src/environment/environmentSettings.js +4 -0
- package/src/environment/environmentSunShadowPass.js +8 -0
- package/src/fauna/INTEGRATION.md +174 -0
- package/src/fauna/boids.js +861 -0
- package/src/fauna/faunaBodies.js +492 -0
- package/src/fauna/faunaPresets.js +52 -0
- package/src/fauna/faunaSettings.js +525 -0
- package/src/fauna/index.js +5 -0
- package/src/fauna/stylizedFauna.js +395 -0
- package/src/game-feel/gameFeelGenerator.js +402 -0
- package/src/game-feel/gameFeelRuntime.js +549 -0
- package/src/game-feel/index.js +2 -0
- package/src/index.js +17 -6
- package/src/lighting/colorIntensity.js +177 -0
- package/src/lighting/index.js +161 -0
- package/src/lighting/lightDescriptors.js +249 -0
- package/src/lighting/lightingCapabilities.js +79 -0
- package/src/lighting/lightingDocuments.js +247 -0
- package/src/lighting/lightingFixtures.js +446 -0
- package/src/lighting/lightingGenerator.js +449 -0
- package/src/lighting/lightingPresets.js +319 -0
- package/src/lighting/lightingRuntime.js +723 -0
- package/src/lighting/lightingStyle.js +386 -0
- package/src/lighting/lightingSystem.js +774 -0
- package/src/lighting/unrealExport.js +186 -0
- package/src/lighting/utils.js +87 -0
- package/src/motion/index.js +5 -0
- package/src/motion/motionClip.js +441 -0
- package/src/motion/motionController.js +628 -0
- package/src/motion/motionDocuments.js +225 -0
- package/src/motion/motionGraph.js +307 -0
- package/src/motion/motionSettings.js +222 -0
- package/src/pathgen/index.js +7 -0
- package/src/pathgen/pathBridge.js +232 -0
- package/src/pathgen/pathPresets.js +35 -0
- package/src/pathgen/pathRibbon.js +410 -0
- package/src/pathgen/pathRouter.js +380 -0
- package/src/pathgen/pathSettings.js +335 -0
- package/src/pathgen/pathTextures.js +123 -0
- package/src/pathgen/stylizedPaths.js +453 -0
- package/src/post/index.js +1 -0
- package/src/post/postGenerator.js +177 -0
- package/src/post/postProcessing.js +41 -0
- package/src/propgen/generatorsWave1.js +379 -0
- package/src/propgen/generatorsWave2.js +462 -0
- package/src/propgen/index.js +5 -0
- package/src/propgen/propAsset.js +323 -0
- package/src/propgen/propParts.js +170 -0
- package/src/propgen/propPlacement.js +459 -0
- package/src/propgen/propPresets.js +82 -0
- package/src/propgen/propSettings.js +395 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
- package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
- package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
- package/src/shaders-tsl/chunks/water-waves.js +90 -11
- package/src/shaders-tsl/environment.js +15 -1
- package/src/shaders-tsl/flower.js +279 -30
- package/src/shaders-tsl/grass.js +60 -33
- package/src/shaders-tsl/sky.js +125 -31
- package/src/shaders-tsl/tree-leaf.js +61 -25
- package/src/shaders-tsl/water-breaker.js +7 -4
- package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
- package/src/shaders-tsl/water.js +439 -49
- package/src/shaders-tsl/woody-surface.js +154 -0
- package/src/sky/sceneOverrideLayers.js +10 -0
- package/src/sky/skyQuality.js +26 -0
- package/src/sky/stylizedSky.js +753 -45
- package/src/soundscape/index.js +4 -0
- package/src/soundscape/soundscapeGenerator.js +179 -0
- package/src/soundscape/soundscapeRuntime.js +806 -0
- package/src/soundscape/soundscapeSettings.js +292 -0
- package/src/styles/index.js +13 -0
- package/src/styles/styleBundle.js +325 -0
- package/src/stylizedTerrain.js +32 -2
- package/src/stylizedWorld.js +423 -20
- package/src/texgen/evaluateTexture.js +675 -0
- package/src/texgen/index.js +60 -0
- package/src/texgen/noise2.js +210 -0
- package/src/texgen/textureAi.js +436 -0
- package/src/texgen/textureGenerators.js +516 -0
- package/src/texgen/texturePresets.js +490 -0
- package/src/texgen/textureSettings.js +342 -0
- package/src/texgen/textureThree.js +59 -0
- package/src/vegetation/flowerSpecies.js +15 -3
- package/src/vegetation/grassPalettes.js +153 -0
- package/src/vegetation/index.js +6 -0
- package/src/vegetation/stylizedBush.js +2 -0
- package/src/vegetation/stylizedFlower.js +82 -0
- package/src/vegetation/stylizedFlowers.js +48 -7
- package/src/vegetation/stylizedForest.js +29 -1
- package/src/vegetation/stylizedGrass.js +291 -56
- package/src/vegetation/stylizedTree.js +38 -2
- package/src/vegetation/stylizedTreeFoliage.js +2 -1
- package/src/vegetation/vegetationShaders.js +1110 -0
- package/src/vfxgen/INTEGRATION.md +145 -0
- package/src/vfxgen/core/burstBackbone.js +380 -0
- package/src/vfxgen/core/projectileCore.js +92 -0
- package/src/vfxgen/core/spriteShapes.js +98 -0
- package/src/vfxgen/core/trailRibbon.js +272 -0
- package/src/vfxgen/core/vfxRandom.js +29 -0
- package/src/vfxgen/effects/emitHelpers.js +37 -0
- package/src/vfxgen/effects/magicEffects.js +162 -0
- package/src/vfxgen/effects/movementEffects.js +87 -0
- package/src/vfxgen/effects/weaponEffects.js +118 -0
- package/src/vfxgen/index.js +18 -0
- package/src/vfxgen/moves/moveController.js +146 -0
- package/src/vfxgen/moves/moveLibrary.js +335 -0
- package/src/vfxgen/vfxPresets.js +98 -0
- package/src/vfxgen/vfxSettings.js +384 -0
- package/src/vfxgen/vfxSystem.js +449 -0
- package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
- package/src/villagegen/index.js +4 -0
- package/src/villagegen/stylizedVillage.js +490 -0
- package/src/villagegen/villageArchetypes.js +160 -0
- package/src/villagegen/villageNames.js +40 -0
- package/src/villagegen/villageSites.js +105 -0
- package/src/water/sceneOverrideLayers.js +23 -0
- package/src/water/water.js +5 -0
- package/src/water/waterBreakerSystem.js +15 -1
- package/src/water/waterCurrentField.js +447 -0
- package/src/water/waterMaterial.js +12 -0
- package/src/water/waterNearshorePhase.js +320 -0
- package/src/water/waterScenePasses.js +83 -30
- package/src/water/waterSettings.js +325 -13
- package/src/water/waterShoreMaterial.js +322 -0
- package/src/water/waterShoreStateField.js +605 -0
- package/src/water/waterSurface.js +797 -28
- package/src/weather/index.js +6 -0
- package/src/weather/weatherPrecipitation.js +221 -0
- package/src/weather/weatherPresets.js +258 -0
- package/src/weather/weatherSettings.js +269 -0
- package/src/weather/weatherSystem.js +871 -0
- package/src/worldMinimap.js +62 -0
- package/src/worldPresets.js +4 -1
package/mcp/server.mjs
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { basename, extname } from 'node:path';
|
|
4
|
+
import { builtinCatalogEntries } from '../src/catalog/builtinEntries.js';
|
|
5
|
+
import {
|
|
6
|
+
fetchPolyhavenFiles,
|
|
7
|
+
fetchPolyhavenIndex,
|
|
8
|
+
filterAssetRefs,
|
|
9
|
+
resolvePolyhavenModelDownload,
|
|
10
|
+
resolvePolyhavenTextureDownload,
|
|
11
|
+
searchAmbientcg,
|
|
12
|
+
resolveAmbientcgDownload,
|
|
13
|
+
} from '../src/assetlib/index.js';
|
|
14
|
+
import {
|
|
15
|
+
getWorkspaceInfo,
|
|
16
|
+
listLibraryEntries,
|
|
17
|
+
listStorageDocuments,
|
|
18
|
+
listWorkspaceFiles,
|
|
19
|
+
matchesText,
|
|
20
|
+
readWorkspaceFile,
|
|
21
|
+
resolveWorkspacePath,
|
|
22
|
+
saveCreation,
|
|
23
|
+
writeWorkspaceFile,
|
|
24
|
+
} from './workspace.mjs';
|
|
25
|
+
import {
|
|
26
|
+
STYLE_LAB_TOOLS,
|
|
27
|
+
callStyleLabTool,
|
|
28
|
+
isStyleLabTool,
|
|
29
|
+
} from './style-lab-tools.mjs';
|
|
30
|
+
|
|
31
|
+
const SERVER_NAME = 'toonlab-oss';
|
|
32
|
+
const SERVER_VERSION = '0.2.0';
|
|
33
|
+
const LATEST_PROTOCOL_VERSION = '2025-11-25';
|
|
34
|
+
const SUPPORTED_PROTOCOL_VERSIONS = new Set([
|
|
35
|
+
LATEST_PROTOCOL_VERSION,
|
|
36
|
+
'2025-06-18',
|
|
37
|
+
'2025-03-26',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function parseArguments(argv) {
|
|
41
|
+
const args = [...argv];
|
|
42
|
+
if (args[0] === 'mcp') args.shift();
|
|
43
|
+
let workspace = process.env.TOONLAB_WORKSPACE ?? '.toonlab';
|
|
44
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
45
|
+
if (args[index] === '--workspace' && args[index + 1]) {
|
|
46
|
+
workspace = args[index + 1];
|
|
47
|
+
index += 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { workspace: resolveWorkspacePath(workspace) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { workspace } = parseArguments(process.argv.slice(2));
|
|
54
|
+
|
|
55
|
+
function jsonContent(value) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ text: JSON.stringify(value, null, 2), type: 'text' }],
|
|
58
|
+
structuredContent: value,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function errorContent(error) {
|
|
63
|
+
return {
|
|
64
|
+
content: [{ text: error?.message ?? String(error), type: 'text' }],
|
|
65
|
+
isError: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function limitValue(value, fallback = 25, max = 100) {
|
|
70
|
+
const parsed = Number(value);
|
|
71
|
+
return Number.isFinite(parsed) ? Math.max(1, Math.min(max, Math.floor(parsed))) : fallback;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function clone(value) {
|
|
75
|
+
return JSON.parse(JSON.stringify(value));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function withSeed(recipe, seed) {
|
|
79
|
+
const next = clone(recipe ?? {});
|
|
80
|
+
if (next.settings?.asset) next.settings.asset.seed = seed;
|
|
81
|
+
else if (next.options) next.options.seed = seed;
|
|
82
|
+
else if (next.settings) next.settings.seed = seed;
|
|
83
|
+
else next.seed = seed;
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function summarizeCatalogEntry(entry) {
|
|
88
|
+
return {
|
|
89
|
+
cluster: entry.cluster,
|
|
90
|
+
description: entry.description ?? null,
|
|
91
|
+
id: entry.id,
|
|
92
|
+
kind: entry.kind,
|
|
93
|
+
label: entry.label,
|
|
94
|
+
source: entry.source ?? 'builtin',
|
|
95
|
+
tags: entry.tags ?? [],
|
|
96
|
+
thumbnail: entry.thumbnail ?? null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function summarizeStorageDocument(entry) {
|
|
101
|
+
return {
|
|
102
|
+
cluster: entry.cluster,
|
|
103
|
+
id: entry.id,
|
|
104
|
+
kind: entry.kind,
|
|
105
|
+
label: entry.label,
|
|
106
|
+
source: entry.source,
|
|
107
|
+
storageKey: entry.key,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function allAssets() {
|
|
112
|
+
const [library, storage, files] = await Promise.all([
|
|
113
|
+
listLibraryEntries(workspace),
|
|
114
|
+
listStorageDocuments(workspace),
|
|
115
|
+
listWorkspaceFiles(workspace),
|
|
116
|
+
]);
|
|
117
|
+
return [
|
|
118
|
+
...builtinCatalogEntries().map((entry) => ({ ...entry, source: 'builtin' })),
|
|
119
|
+
...library.map((entry) => ({ ...entry, source: 'library' })),
|
|
120
|
+
...storage,
|
|
121
|
+
...files,
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function findAsset(id, source = null) {
|
|
126
|
+
const assets = await allAssets();
|
|
127
|
+
return assets.find((entry) => entry.id === id && (!source || entry.source === source)) ?? null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function matchesTags(entry, tags) {
|
|
131
|
+
if (!Array.isArray(tags) || tags.length === 0) return true;
|
|
132
|
+
const actual = new Set((entry.tags ?? []).map((tag) => String(tag).toLowerCase()));
|
|
133
|
+
return tags.every((tag) => actual.has(String(tag).toLowerCase()));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function searchAssets(args) {
|
|
137
|
+
const assets = await allAssets();
|
|
138
|
+
const results = assets.filter((entry) => {
|
|
139
|
+
if (args.source && entry.source !== args.source) return false;
|
|
140
|
+
if (args.cluster && entry.cluster !== args.cluster) return false;
|
|
141
|
+
if (args.kind && entry.kind !== args.kind) return false;
|
|
142
|
+
if (!matchesTags(entry, args.tags)) return false;
|
|
143
|
+
return matchesText(entry, args.query);
|
|
144
|
+
}).slice(0, limitValue(args.limit));
|
|
145
|
+
return {
|
|
146
|
+
count: results.length,
|
|
147
|
+
items: results.map((entry) => {
|
|
148
|
+
if (entry.source === 'workspace') return entry;
|
|
149
|
+
if (entry.source === 'workspace-storage') return summarizeStorageDocument(entry);
|
|
150
|
+
return summarizeCatalogEntry(entry);
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function getAsset(args) {
|
|
156
|
+
const asset = await findAsset(args.id, args.source);
|
|
157
|
+
if (!asset) throw new Error(`Asset "${args.id}" was not found.`);
|
|
158
|
+
if (asset.source === 'workspace') return asset;
|
|
159
|
+
if (asset.source === 'workspace-storage') return asset;
|
|
160
|
+
return asset;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function listMyCreations(args) {
|
|
164
|
+
const [library, storage, files] = await Promise.all([
|
|
165
|
+
listLibraryEntries(workspace),
|
|
166
|
+
listStorageDocuments(workspace),
|
|
167
|
+
listWorkspaceFiles(workspace, { roots: ['assets', 'creations', 'exports', 'imports', 'presets'] }),
|
|
168
|
+
]);
|
|
169
|
+
const query = args.query ?? null;
|
|
170
|
+
const items = [
|
|
171
|
+
...library.map((entry) => summarizeCatalogEntry({ ...entry, source: 'library' })),
|
|
172
|
+
...storage.map(summarizeStorageDocument),
|
|
173
|
+
...files,
|
|
174
|
+
].filter((entry) => matchesText(entry, query)).slice(0, limitValue(args.limit, 50, 200));
|
|
175
|
+
return { count: items.length, items, workspace };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function getMyCreation(args) {
|
|
179
|
+
const asset = await findAsset(args.id);
|
|
180
|
+
if (!asset || asset.source === 'builtin') throw new Error(`Workspace creation "${args.id}" was not found.`);
|
|
181
|
+
if (asset.source !== 'workspace') return asset;
|
|
182
|
+
const file = await readWorkspaceFile(workspace, asset.relativePath);
|
|
183
|
+
if (file.data.length > 5 * 1024 * 1024) {
|
|
184
|
+
return { ...asset, note: 'File is larger than 5 MB; use absolutePath to read it directly.' };
|
|
185
|
+
}
|
|
186
|
+
if (file.mimeType.startsWith('text/') || file.mimeType === 'application/json' || ['.gltf'].includes(extname(file.name))) {
|
|
187
|
+
return { ...asset, content: file.data.toString('utf8') };
|
|
188
|
+
}
|
|
189
|
+
return { ...asset, base64: file.data.toString('base64'), encoding: 'base64' };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function generateAsset(args) {
|
|
193
|
+
const entry = builtinCatalogEntries().find((item) => item.id === args.catalog_id);
|
|
194
|
+
if (!entry) throw new Error(`Unknown built-in catalog asset "${args.catalog_id}".`);
|
|
195
|
+
const seed = Number.isFinite(Number(args.seed)) ? Number(args.seed) : Math.floor(Math.random() * 1_000_000);
|
|
196
|
+
const name = String(args.name ?? `${entry.label} ${seed}`);
|
|
197
|
+
const document = {
|
|
198
|
+
catalog: {
|
|
199
|
+
cluster: entry.cluster,
|
|
200
|
+
id: entry.id,
|
|
201
|
+
kind: entry.kind,
|
|
202
|
+
label: entry.label,
|
|
203
|
+
},
|
|
204
|
+
generatedAt: new Date().toISOString(),
|
|
205
|
+
recipe: withSeed(entry.recipe, seed),
|
|
206
|
+
schema: 'toonlab/generated-asset',
|
|
207
|
+
seed,
|
|
208
|
+
spawn: entry.spawn,
|
|
209
|
+
version: 1,
|
|
210
|
+
};
|
|
211
|
+
const file = args.save === false ? null : await saveCreation(workspace, {
|
|
212
|
+
document,
|
|
213
|
+
kind: entry.cluster,
|
|
214
|
+
name,
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
document,
|
|
218
|
+
file,
|
|
219
|
+
next: entry.cluster === 'water' || entry.cluster === 'sky' || entry.cluster === 'post' || entry.cluster === 'toon'
|
|
220
|
+
? 'Use the spawn snippet in your project; this is a settings preset.'
|
|
221
|
+
: 'Open the matching ToonLab lab to preview or export this recipe as GLB.',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function compactCc0Ref(ref) {
|
|
226
|
+
return {
|
|
227
|
+
attribution: ref.attribution,
|
|
228
|
+
authors: ref.authors,
|
|
229
|
+
categories: ref.categories,
|
|
230
|
+
id: ref.id,
|
|
231
|
+
kind: ref.kind,
|
|
232
|
+
name: ref.name,
|
|
233
|
+
pageUrl: ref.pageUrl,
|
|
234
|
+
source: ref.source,
|
|
235
|
+
tags: ref.tags,
|
|
236
|
+
thumbnailUrl: ref.thumbnailUrl,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function polyhavenType(kind) {
|
|
241
|
+
return { hdri: 'hdris', model: 'models', texture: 'textures' }[kind] ?? 'models';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function ambientcgType(kind) {
|
|
245
|
+
return { hdri: 'HDRI', model: 'Model', texture: 'Material' }[kind] ?? 'Model';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function searchCc0Assets(args) {
|
|
249
|
+
const provider = args.provider ?? 'all';
|
|
250
|
+
const kind = args.kind ?? 'model';
|
|
251
|
+
const limit = limitValue(args.limit, 20, 50);
|
|
252
|
+
const searches = [];
|
|
253
|
+
if (provider === 'all' || provider === 'polyhaven') {
|
|
254
|
+
searches.push((async () => {
|
|
255
|
+
const refs = await fetchPolyhavenIndex({ type: polyhavenType(kind) });
|
|
256
|
+
return filterAssetRefs(refs, { kind, text: args.query ?? null }).slice(0, limit);
|
|
257
|
+
})());
|
|
258
|
+
}
|
|
259
|
+
if (provider === 'all' || provider === 'ambientcg') {
|
|
260
|
+
searches.push(searchAmbientcg({
|
|
261
|
+
limit,
|
|
262
|
+
query: args.query ?? '',
|
|
263
|
+
type: ambientcgType(kind),
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
if (searches.length === 0) throw new Error('provider must be all, polyhaven, or ambientcg.');
|
|
267
|
+
const settled = await Promise.allSettled(searches);
|
|
268
|
+
const results = settled.flatMap((result) => result.status === 'fulfilled' ? result.value : []);
|
|
269
|
+
const errors = settled.flatMap((result) => result.status === 'rejected' ? [result.reason?.message ?? String(result.reason)] : []);
|
|
270
|
+
return { count: Math.min(results.length, limit), errors, items: results.slice(0, limit).map(compactCc0Ref) };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function getCc0AssetDetails({ id, kind = 'model', provider, resolution = '1k' }) {
|
|
274
|
+
if (provider === 'ambientcg') {
|
|
275
|
+
const [ref] = await searchAmbientcg({ id });
|
|
276
|
+
if (!ref) throw new Error(`ambientCG asset "${id}" was not found.`);
|
|
277
|
+
const download = resolveAmbientcgDownload(ref, { resolution: resolution.toUpperCase() });
|
|
278
|
+
return { download, ref: compactCc0Ref(ref) };
|
|
279
|
+
}
|
|
280
|
+
if (provider === 'polyhaven') {
|
|
281
|
+
const refs = await fetchPolyhavenIndex({ type: polyhavenType(kind) });
|
|
282
|
+
const ref = refs.find((item) => item.id === id);
|
|
283
|
+
if (!ref) throw new Error(`Poly Haven asset "${id}" was not found.`);
|
|
284
|
+
const files = await fetchPolyhavenFiles(id);
|
|
285
|
+
const download = kind === 'model'
|
|
286
|
+
? resolvePolyhavenModelDownload(files, { resolution })
|
|
287
|
+
: kind === 'texture'
|
|
288
|
+
? resolvePolyhavenTextureDownload(files, { resolution })
|
|
289
|
+
: null;
|
|
290
|
+
return { download, ref: compactCc0Ref(ref) };
|
|
291
|
+
}
|
|
292
|
+
throw new Error('provider must be polyhaven or ambientcg.');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function fetchBytes(url) {
|
|
296
|
+
const response = await fetch(url);
|
|
297
|
+
if (!response.ok) throw new Error(`Download failed with ${response.status}: ${url}`);
|
|
298
|
+
return Buffer.from(await response.arrayBuffer());
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function filenameFromUrl(url, fallback) {
|
|
302
|
+
try {
|
|
303
|
+
return basename(new URL(url).pathname) || fallback;
|
|
304
|
+
} catch {
|
|
305
|
+
return fallback;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function importCc0Asset(args) {
|
|
310
|
+
const details = await getCc0AssetDetails(args);
|
|
311
|
+
if (!details.download) {
|
|
312
|
+
throw new Error('This asset kind does not yet expose a direct downloadable bundle. Use get_cc0_asset for its source page.');
|
|
313
|
+
}
|
|
314
|
+
const folder = `imports/${args.provider}/${String(args.id).replace(/[^a-z0-9._-]+/gi, '-')}`;
|
|
315
|
+
const files = [];
|
|
316
|
+
if (args.provider === 'ambientcg') {
|
|
317
|
+
const name = filenameFromUrl(details.download.url, `${args.id}.zip`);
|
|
318
|
+
files.push(await writeWorkspaceFile(workspace, `${folder}/${name}`, await fetchBytes(details.download.url)));
|
|
319
|
+
} else if (details.download.url) {
|
|
320
|
+
const name = filenameFromUrl(details.download.url, `${args.id}.gltf`);
|
|
321
|
+
files.push(await writeWorkspaceFile(workspace, `${folder}/${name}`, await fetchBytes(details.download.url)));
|
|
322
|
+
for (const [relativePath, url] of Object.entries(details.download.resources ?? {})) {
|
|
323
|
+
files.push(await writeWorkspaceFile(workspace, `${folder}/${relativePath}`, await fetchBytes(url)));
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
for (const [slot, file] of Object.entries(details.download.maps ?? {})) {
|
|
327
|
+
const name = filenameFromUrl(file.url, `${slot}.jpg`);
|
|
328
|
+
files.push(await writeWorkspaceFile(workspace, `${folder}/${name}`, await fetchBytes(file.url)));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const manifest = await writeWorkspaceFile(workspace, `${folder}/toonlab-import.json`, `${JSON.stringify({
|
|
332
|
+
importedAt: new Date().toISOString(),
|
|
333
|
+
...details,
|
|
334
|
+
}, null, 2)}\n`);
|
|
335
|
+
return { ...details, files: [...files, manifest], workspace };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const TOOLS = [
|
|
339
|
+
{
|
|
340
|
+
annotations: { readOnlyHint: true },
|
|
341
|
+
description: 'Show the local .toonlab workspace path, migration status, and item counts.',
|
|
342
|
+
inputSchema: { additionalProperties: false, properties: {}, type: 'object' },
|
|
343
|
+
name: 'get_workspace_info',
|
|
344
|
+
title: 'Get ToonLab workspace info',
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
annotations: { readOnlyHint: true },
|
|
348
|
+
description: 'Search built-in procedural assets, saved library entries, lab presets, and files on disk.',
|
|
349
|
+
inputSchema: {
|
|
350
|
+
additionalProperties: false,
|
|
351
|
+
properties: {
|
|
352
|
+
cluster: { type: 'string' },
|
|
353
|
+
kind: { type: 'string' },
|
|
354
|
+
limit: { maximum: 100, minimum: 1, type: 'integer' },
|
|
355
|
+
query: { type: 'string' },
|
|
356
|
+
source: { enum: ['builtin', 'library', 'workspace', 'workspace-storage'], type: 'string' },
|
|
357
|
+
tags: { items: { type: 'string' }, type: 'array' },
|
|
358
|
+
},
|
|
359
|
+
type: 'object',
|
|
360
|
+
},
|
|
361
|
+
name: 'search_assets',
|
|
362
|
+
title: 'Search ToonLab assets',
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
annotations: { readOnlyHint: true },
|
|
366
|
+
description: 'Get a complete asset, recipe, preset, library entry, or workspace file descriptor by id.',
|
|
367
|
+
inputSchema: {
|
|
368
|
+
additionalProperties: false,
|
|
369
|
+
properties: { id: { type: 'string' }, source: { type: 'string' } },
|
|
370
|
+
required: ['id'],
|
|
371
|
+
type: 'object',
|
|
372
|
+
},
|
|
373
|
+
name: 'get_asset',
|
|
374
|
+
title: 'Get a ToonLab asset',
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
annotations: { readOnlyHint: true },
|
|
378
|
+
description: 'List the developer\'s disk-backed library, saved lab documents, presets, imports, and exports.',
|
|
379
|
+
inputSchema: {
|
|
380
|
+
additionalProperties: false,
|
|
381
|
+
properties: { limit: { type: 'integer' }, query: { type: 'string' } },
|
|
382
|
+
type: 'object',
|
|
383
|
+
},
|
|
384
|
+
name: 'list_my_creations',
|
|
385
|
+
title: 'List my ToonLab creations',
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
annotations: { readOnlyHint: true },
|
|
389
|
+
description: 'Read a saved creation or small workspace file. Binary files are returned as base64 and always include a direct absolute path.',
|
|
390
|
+
inputSchema: { additionalProperties: false, properties: { id: { type: 'string' } }, required: ['id'], type: 'object' },
|
|
391
|
+
name: 'get_my_creation',
|
|
392
|
+
title: 'Get my ToonLab creation',
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
annotations: { destructiveHint: false, idempotentHint: true },
|
|
396
|
+
description: 'Save a JSON or text creation into .toonlab/creations so labs and coding agents can share it.',
|
|
397
|
+
inputSchema: {
|
|
398
|
+
additionalProperties: false,
|
|
399
|
+
properties: {
|
|
400
|
+
document: {},
|
|
401
|
+
filename: { type: 'string' },
|
|
402
|
+
kind: { type: 'string' },
|
|
403
|
+
name: { type: 'string' },
|
|
404
|
+
},
|
|
405
|
+
required: ['document', 'name'],
|
|
406
|
+
type: 'object',
|
|
407
|
+
},
|
|
408
|
+
name: 'save_creation',
|
|
409
|
+
title: 'Save a ToonLab creation',
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
annotations: { destructiveHint: false, idempotentHint: false },
|
|
413
|
+
description: 'Generate a deterministic editable asset recipe from a built-in ToonLab catalog entry and optionally save it to disk.',
|
|
414
|
+
inputSchema: {
|
|
415
|
+
additionalProperties: false,
|
|
416
|
+
properties: {
|
|
417
|
+
catalog_id: { type: 'string' },
|
|
418
|
+
name: { type: 'string' },
|
|
419
|
+
save: { default: true, type: 'boolean' },
|
|
420
|
+
seed: { type: 'integer' },
|
|
421
|
+
},
|
|
422
|
+
required: ['catalog_id'],
|
|
423
|
+
type: 'object',
|
|
424
|
+
},
|
|
425
|
+
name: 'generate_asset',
|
|
426
|
+
title: 'Generate a procedural ToonLab asset',
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
annotations: { openWorldHint: true, readOnlyHint: true },
|
|
430
|
+
description: 'Search CC0 models, textures, or HDRIs from Poly Haven and ambientCG.',
|
|
431
|
+
inputSchema: {
|
|
432
|
+
additionalProperties: false,
|
|
433
|
+
properties: {
|
|
434
|
+
kind: { enum: ['model', 'texture', 'hdri'], type: 'string' },
|
|
435
|
+
limit: { maximum: 50, minimum: 1, type: 'integer' },
|
|
436
|
+
provider: { enum: ['all', 'polyhaven', 'ambientcg'], type: 'string' },
|
|
437
|
+
query: { type: 'string' },
|
|
438
|
+
},
|
|
439
|
+
type: 'object',
|
|
440
|
+
},
|
|
441
|
+
name: 'search_cc0_assets',
|
|
442
|
+
title: 'Search public CC0 assets',
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
annotations: { openWorldHint: true, readOnlyHint: true },
|
|
446
|
+
description: 'Resolve source, attribution, and download metadata for one CC0 asset.',
|
|
447
|
+
inputSchema: {
|
|
448
|
+
additionalProperties: false,
|
|
449
|
+
properties: {
|
|
450
|
+
id: { type: 'string' },
|
|
451
|
+
kind: { enum: ['model', 'texture', 'hdri'], type: 'string' },
|
|
452
|
+
provider: { enum: ['polyhaven', 'ambientcg'], type: 'string' },
|
|
453
|
+
resolution: { default: '1k', type: 'string' },
|
|
454
|
+
},
|
|
455
|
+
required: ['provider', 'id'],
|
|
456
|
+
type: 'object',
|
|
457
|
+
},
|
|
458
|
+
name: 'get_cc0_asset',
|
|
459
|
+
title: 'Get a public CC0 asset',
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
annotations: { destructiveHint: false, openWorldHint: true },
|
|
463
|
+
description: 'Download a CC0 asset and attribution manifest into .toonlab/imports for direct use by the project.',
|
|
464
|
+
inputSchema: {
|
|
465
|
+
additionalProperties: false,
|
|
466
|
+
properties: {
|
|
467
|
+
id: { type: 'string' },
|
|
468
|
+
kind: { enum: ['model', 'texture', 'hdri'], type: 'string' },
|
|
469
|
+
provider: { enum: ['polyhaven', 'ambientcg'], type: 'string' },
|
|
470
|
+
resolution: { default: '1k', type: 'string' },
|
|
471
|
+
},
|
|
472
|
+
required: ['provider', 'id'],
|
|
473
|
+
type: 'object',
|
|
474
|
+
},
|
|
475
|
+
name: 'import_cc0_asset',
|
|
476
|
+
title: 'Import a public CC0 asset',
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
annotations: { readOnlyHint: true },
|
|
480
|
+
description: 'Explain what asset generation is available in the open-source local server.',
|
|
481
|
+
inputSchema: { additionalProperties: false, properties: {}, type: 'object' },
|
|
482
|
+
name: 'get_generation_capabilities',
|
|
483
|
+
title: 'Get local generation capabilities',
|
|
484
|
+
},
|
|
485
|
+
...STYLE_LAB_TOOLS,
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
async function callTool(name, args = {}) {
|
|
489
|
+
switch (name) {
|
|
490
|
+
case 'get_workspace_info': return getWorkspaceInfo(workspace);
|
|
491
|
+
case 'search_assets': return searchAssets(args);
|
|
492
|
+
case 'get_asset': return getAsset(args);
|
|
493
|
+
case 'list_my_creations': return listMyCreations(args);
|
|
494
|
+
case 'get_my_creation': return getMyCreation(args);
|
|
495
|
+
case 'save_creation': return saveCreation(workspace, args);
|
|
496
|
+
case 'generate_asset': return generateAsset(args);
|
|
497
|
+
case 'search_cc0_assets': return searchCc0Assets(args);
|
|
498
|
+
case 'get_cc0_asset': return getCc0AssetDetails(args);
|
|
499
|
+
case 'import_cc0_asset': return importCc0Asset(args);
|
|
500
|
+
case 'get_generation_capabilities': return {
|
|
501
|
+
available: [
|
|
502
|
+
'Generate deterministic recipes from every built-in ToonLab catalog entry.',
|
|
503
|
+
'Author, validate, and batch-resolve open-domain recipes for the generative style domains (post, camera, game feel, lighting styles, light fixtures).',
|
|
504
|
+
'Save and retrieve editable lab documents through the shared .toonlab workspace.',
|
|
505
|
+
'Search and import CC0 assets from Poly Haven and ambientCG.',
|
|
506
|
+
'Use each recipe\'s spawn snippet to integrate it into a Three.js project.',
|
|
507
|
+
],
|
|
508
|
+
hostedProAdds: [
|
|
509
|
+
'Managed image and 3D model generation providers',
|
|
510
|
+
'Cloud library and cross-device sync',
|
|
511
|
+
'Remote Streamable HTTP transport with OAuth',
|
|
512
|
+
],
|
|
513
|
+
mode: 'open-source-local',
|
|
514
|
+
};
|
|
515
|
+
default:
|
|
516
|
+
if (isStyleLabTool(name)) {
|
|
517
|
+
return callStyleLabTool(name, args, { saveCreation, workspace });
|
|
518
|
+
}
|
|
519
|
+
throw new Error(`Unknown tool "${name}".`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function listResources() {
|
|
524
|
+
const assets = await allAssets();
|
|
525
|
+
return assets.slice(0, 500).map((entry) => ({
|
|
526
|
+
description: entry.description ?? `${entry.source} ToonLab asset`,
|
|
527
|
+
mimeType: entry.mimeType ?? 'application/json',
|
|
528
|
+
name: entry.label ?? entry.name ?? entry.id,
|
|
529
|
+
uri: `toonlab://asset/${encodeURIComponent(entry.id)}`,
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function readResource(uri) {
|
|
534
|
+
const parsed = new URL(uri);
|
|
535
|
+
if (parsed.protocol !== 'toonlab:' || parsed.hostname !== 'asset') throw new Error(`Unsupported resource URI: ${uri}`);
|
|
536
|
+
const id = decodeURIComponent(parsed.pathname.replace(/^\//, ''));
|
|
537
|
+
const asset = await getAsset({ id });
|
|
538
|
+
if (asset.source === 'workspace') {
|
|
539
|
+
const file = await readWorkspaceFile(workspace, asset.relativePath);
|
|
540
|
+
if (file.data.length > 5 * 1024 * 1024) throw new Error('Resource is larger than the 5 MB MCP inline limit. Use its absolutePath.');
|
|
541
|
+
if (file.mimeType === 'application/json' || file.mimeType.startsWith('text/')) {
|
|
542
|
+
return { contents: [{ mimeType: file.mimeType, text: file.data.toString('utf8'), uri }] };
|
|
543
|
+
}
|
|
544
|
+
return { contents: [{ blob: file.data.toString('base64'), mimeType: file.mimeType, uri }] };
|
|
545
|
+
}
|
|
546
|
+
return { contents: [{ mimeType: 'application/json', text: JSON.stringify(asset, null, 2), uri }] };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function handleRequest(message) {
|
|
550
|
+
const { id, method, params = {} } = message;
|
|
551
|
+
if (method === 'initialize') {
|
|
552
|
+
const requested = params.protocolVersion;
|
|
553
|
+
return {
|
|
554
|
+
id,
|
|
555
|
+
jsonrpc: '2.0',
|
|
556
|
+
result: {
|
|
557
|
+
capabilities: { resources: {}, tools: {} },
|
|
558
|
+
instructions: 'Use ToonLab to search public and private assets, create deterministic recipes, import CC0 files, and work with the local .toonlab workspace.',
|
|
559
|
+
protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.has(requested) ? requested : LATEST_PROTOCOL_VERSION,
|
|
560
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
if (method === 'ping') return { id, jsonrpc: '2.0', result: {} };
|
|
565
|
+
if (method === 'tools/list') return { id, jsonrpc: '2.0', result: { tools: TOOLS } };
|
|
566
|
+
if (method === 'tools/call') {
|
|
567
|
+
try {
|
|
568
|
+
return { id, jsonrpc: '2.0', result: jsonContent(await callTool(params.name, params.arguments ?? {})) };
|
|
569
|
+
} catch (error) {
|
|
570
|
+
return { id, jsonrpc: '2.0', result: errorContent(error) };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (method === 'resources/list') return { id, jsonrpc: '2.0', result: { resources: await listResources() } };
|
|
574
|
+
if (method === 'resources/read') {
|
|
575
|
+
try {
|
|
576
|
+
return { id, jsonrpc: '2.0', result: await readResource(params.uri) };
|
|
577
|
+
} catch (error) {
|
|
578
|
+
return { error: { code: -32002, message: error?.message ?? String(error) }, id, jsonrpc: '2.0' };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return { error: { code: -32601, message: `Method not found: ${method}` }, id, jsonrpc: '2.0' };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
let input = '';
|
|
585
|
+
process.stdin.setEncoding('utf8');
|
|
586
|
+
process.stdin.on('data', async (chunk) => {
|
|
587
|
+
input += chunk;
|
|
588
|
+
let newline = input.indexOf('\n');
|
|
589
|
+
while (newline >= 0) {
|
|
590
|
+
const line = input.slice(0, newline).trim();
|
|
591
|
+
input = input.slice(newline + 1);
|
|
592
|
+
if (line) {
|
|
593
|
+
try {
|
|
594
|
+
const message = JSON.parse(line);
|
|
595
|
+
if (message.id !== undefined && message.id !== null) {
|
|
596
|
+
process.stdout.write(`${JSON.stringify(await handleRequest(message))}\n`);
|
|
597
|
+
}
|
|
598
|
+
} catch (error) {
|
|
599
|
+
process.stdout.write(`${JSON.stringify({
|
|
600
|
+
error: { code: -32700, message: error?.message ?? 'Parse error' },
|
|
601
|
+
id: null,
|
|
602
|
+
jsonrpc: '2.0',
|
|
603
|
+
})}\n`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
newline = input.indexOf('\n');
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
process.stdin.on('end', () => process.exit(0));
|