@olo-studio/studio-mcp 1.0.0 → 1.0.1
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/README.md +2 -2
- package/bin/olo-studio-mcp.js +10 -3
- package/package.json +5 -2
- package/src/compiler.js +60 -13
- package/src/director.js +1098 -0
- package/src/renderer.js +256 -35
- package/src/server.js +33 -6
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# `@
|
|
1
|
+
# `@olo-studio/studio-mcp`
|
|
2
2
|
|
|
3
3
|
Local-first MCP server and CLI for **Olomni Studio** — compile `olomni.studio.scene.v1` scenes offline, generate 9:16 video scripts, render deterministic `.mp4` / `.webm` / alpha / chroma videos via headless Chrome (`window.OloStudioBridge.exportSceneBuffer`), and publish `<olo-presence>` 3D brand mascot embeds.
|
|
4
4
|
|
|
@@ -11,7 +11,7 @@ Add to Claude Desktop, Cursor, Windsurf, or any MCP-compatible host:
|
|
|
11
11
|
"mcpServers": {
|
|
12
12
|
"olomni-studio": {
|
|
13
13
|
"command": "npx",
|
|
14
|
-
"args": ["-y", "@
|
|
14
|
+
"args": ["-y", "@olo-studio/studio-mcp"],
|
|
15
15
|
"env": {
|
|
16
16
|
"OLO_STUDIO_API_KEY": "olo_pat_your_token_here"
|
|
17
17
|
}
|
package/bin/olo-studio-mcp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @
|
|
3
|
+
* @olo-studio/studio-mcp CLI & Stdio MCP Server Entrypoint
|
|
4
4
|
* Usage:
|
|
5
5
|
* olo-studio-mcp [serve] Start JSON-RPC 2.0 stdio MCP server
|
|
6
6
|
* olo-studio-mcp compile [opts] Compile SceneSpec v1 JSON offline
|
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
* olo-studio-mcp inspect-glb <src> Inspect .glb header & JSON chunk
|
|
9
9
|
* olo-studio-mcp --help Show help
|
|
10
10
|
*/
|
|
11
|
+
import fs from 'node:fs'
|
|
11
12
|
import fsp from 'node:fs/promises'
|
|
12
13
|
import path from 'node:path'
|
|
13
14
|
import { compileSceneSpec } from '../src/compiler.js'
|
|
14
15
|
import { inspectGlb, renderVideoWithBrowser } from '../src/renderer.js'
|
|
15
16
|
import { startStdioServer } from '../src/server.js'
|
|
16
17
|
|
|
18
|
+
const PKG_VERSION = JSON.parse(
|
|
19
|
+
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
20
|
+
).version
|
|
21
|
+
|
|
17
22
|
function parseFlags(argv) {
|
|
18
23
|
const flags = {}
|
|
19
24
|
const positional = []
|
|
@@ -38,7 +43,7 @@ function parseFlags(argv) {
|
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
function printHelp() {
|
|
41
|
-
console.log(`@
|
|
46
|
+
console.log(`@olo-studio/studio-mcp v${PKG_VERSION} — Local-First MCP Server & CLI for Olomni Studio
|
|
42
47
|
|
|
43
48
|
Usage:
|
|
44
49
|
olo-studio-mcp [serve]
|
|
@@ -47,7 +52,7 @@ Usage:
|
|
|
47
52
|
olo-studio-mcp compile [--script "..."] [--file scene.json] [--title "..."] [--out out.json]
|
|
48
53
|
Compile and validate an olomni.studio.scene.v1 SceneSpec without a browser.
|
|
49
54
|
|
|
50
|
-
olo-studio-mcp render [--script "..."] [--file scene.json] [--out out.mp4] [--format mp4|webm|alpha|chroma] [--glb path-or-url]
|
|
55
|
+
olo-studio-mcp render [--script "..."] [--file scene.json] [--out out.mp4] [--format mp4|webm|alpha|chroma] [--fps 24|30|60] [--quality fast|social|studio] [--glb path-or-url]
|
|
51
56
|
Render a deterministic 3D video via headless Chrome + OloStudioBridge.
|
|
52
57
|
|
|
53
58
|
olo-studio-mcp inspect-glb <path-or-url>
|
|
@@ -113,6 +118,8 @@ async function main() {
|
|
|
113
118
|
sceneSpec: input,
|
|
114
119
|
format: flags.format ? String(flags.format) : 'mp4',
|
|
115
120
|
outputPath: flags.out ? String(flags.out) : './olo-render.mp4',
|
|
121
|
+
...(flags.fps !== undefined ? { fps: Number(flags.fps) } : {}),
|
|
122
|
+
...(flags.quality ? { quality: String(flags.quality) } : {}),
|
|
116
123
|
...(isUrl ? { glbUrl: glbArg } : glbArg ? { glbPath: glbArg } : {}),
|
|
117
124
|
...(flags.studioUrl ? { studioUrl: String(flags.studioUrl) } : {}),
|
|
118
125
|
})
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olo-studio/studio-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Local-first MCP server and CLI for Olomni Studio 3D video rendering, SceneSpec v1 compilation, AI scripting, and <olo-presence> embeds.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"prepack": "node -e \"const fs=require('fs'),p='../../public/studio/engine/director.js';if(fs.existsSync(p))fs.copyFileSync(p,'./src/director.js')\""
|
|
9
|
+
},
|
|
7
10
|
"exports": {
|
|
8
11
|
".": "./src/server.js",
|
|
9
12
|
"./compiler": "./src/compiler.js",
|
|
@@ -32,7 +35,7 @@
|
|
|
32
35
|
},
|
|
33
36
|
"repository": {
|
|
34
37
|
"type": "git",
|
|
35
|
-
"url": "git+https://github.com/
|
|
38
|
+
"url": "git+https://github.com/faustodariopenamondragon-web/OlomniAI.git",
|
|
36
39
|
"directory": "packages/studio-mcp"
|
|
37
40
|
},
|
|
38
41
|
"engines": {
|
package/src/compiler.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'node:fs'
|
|
6
6
|
import path from 'node:path'
|
|
7
|
+
import vm from 'node:vm'
|
|
7
8
|
import { createRequire } from 'node:module'
|
|
8
9
|
import { fileURLToPath } from 'node:url'
|
|
9
10
|
|
|
@@ -205,18 +206,55 @@ export function findPublicStudioDir(overrideDir) {
|
|
|
205
206
|
return null
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
function loadUmdModule(filePath) {
|
|
210
|
+
try {
|
|
211
|
+
const mod = require(filePath)
|
|
212
|
+
if (mod && (typeof mod.compileSceneFromScript === 'function' || typeof mod.compileScene === 'function')) {
|
|
213
|
+
return mod
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// Package "type": "module" prevents require() on .js CJS/UMD files in Node 20
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Node 22+ require(esm) returns {} for UMD scripts under "type": "module"; evaluate as CJS cleanly.
|
|
220
|
+
const code = fs.readFileSync(filePath, 'utf8')
|
|
221
|
+
const mod = { exports: {} }
|
|
222
|
+
const fn = vm.compileFunction(
|
|
223
|
+
code,
|
|
224
|
+
['module', 'exports', 'require', '__filename', '__dirname'],
|
|
225
|
+
{ filename: filePath },
|
|
226
|
+
)
|
|
227
|
+
fn(mod, mod.exports, require, filePath, path.dirname(filePath))
|
|
228
|
+
const exported = mod.exports
|
|
229
|
+
if (exported && (typeof exported.compileSceneFromScript === 'function' || typeof exported.compileScene === 'function')) {
|
|
230
|
+
return exported
|
|
231
|
+
}
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
|
|
208
235
|
export function loadDirector(overridePublicDir) {
|
|
209
236
|
if (cachedDirector) return cachedDirector
|
|
237
|
+
const candidates = []
|
|
210
238
|
const publicDir = findPublicStudioDir(overridePublicDir)
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
239
|
+
if (publicDir) {
|
|
240
|
+
candidates.push(path.join(publicDir, 'studio', 'engine', 'director.js'))
|
|
241
|
+
}
|
|
242
|
+
// Bundled copy for external npx installs outside the repository
|
|
243
|
+
candidates.push(path.join(__dirname, 'director.js'))
|
|
244
|
+
|
|
245
|
+
for (const directorPath of candidates) {
|
|
246
|
+
if (!fs.existsSync(directorPath)) continue
|
|
247
|
+
try {
|
|
248
|
+
const loaded = loadUmdModule(directorPath)
|
|
249
|
+
if (loaded) {
|
|
250
|
+
cachedDirector = loaded
|
|
251
|
+
return cachedDirector
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
// try next candidate
|
|
255
|
+
}
|
|
219
256
|
}
|
|
257
|
+
return null
|
|
220
258
|
}
|
|
221
259
|
|
|
222
260
|
function fnv1aHex(str) {
|
|
@@ -434,10 +472,14 @@ function parseCsvLine(line) {
|
|
|
434
472
|
return cells
|
|
435
473
|
}
|
|
436
474
|
|
|
437
|
-
export function parseBatchInput({ specs, csv }) {
|
|
475
|
+
export function parseBatchInput({ specs, csv, defaults = {} }) {
|
|
476
|
+
const cleanDefaults = Object.fromEntries(
|
|
477
|
+
Object.entries(defaults || {}).filter(([, v]) => v !== undefined),
|
|
478
|
+
)
|
|
479
|
+
|
|
438
480
|
if (Array.isArray(specs) && specs.length > 0) {
|
|
439
481
|
return specs.map((item, idx) => {
|
|
440
|
-
const compiled = compileSceneSpec(item)
|
|
482
|
+
const compiled = compileSceneSpec({ ...cleanDefaults, ...item })
|
|
441
483
|
return {
|
|
442
484
|
index: idx,
|
|
443
485
|
outputName: item.outputName || item.fileName || `variant-${idx + 1}-${compiled.sceneHash}`,
|
|
@@ -461,6 +503,7 @@ export function parseBatchInput({ specs, csv }) {
|
|
|
461
503
|
}
|
|
462
504
|
})
|
|
463
505
|
const compiled = compileSceneSpec({
|
|
506
|
+
...cleanDefaults,
|
|
464
507
|
title: record.title || `Variant ${idx + 1}`,
|
|
465
508
|
script: record.script || record.hook || record.text || '',
|
|
466
509
|
wear: record.wear,
|
|
@@ -468,11 +511,15 @@ export function parseBatchInput({ specs, csv }) {
|
|
|
468
511
|
finish: record.finish,
|
|
469
512
|
scenario: record.scenario,
|
|
470
513
|
vibe: record.vibe,
|
|
471
|
-
voice: record.voice,
|
|
472
|
-
voiceId: record.voiceid || record.voice_id,
|
|
514
|
+
voice: record.voice ?? cleanDefaults.voice,
|
|
515
|
+
voiceId: record.voiceid || record.voice_id || cleanDefaults.voiceId,
|
|
473
516
|
emotion: record.emotion,
|
|
474
517
|
stateOverride: record.state || record.stateoverride,
|
|
475
|
-
captions: record.captions !== undefined
|
|
518
|
+
captions: record.captions !== undefined
|
|
519
|
+
? record.captions !== 'false' && record.captions !== '0'
|
|
520
|
+
: (cleanDefaults.captions !== undefined ? cleanDefaults.captions : true),
|
|
521
|
+
pipPath: record.pippath || record.pip_path || cleanDefaults.pipPath,
|
|
522
|
+
pipUrl: record.pipurl || record.pip_url || cleanDefaults.pipUrl,
|
|
476
523
|
category: record.category || record.language || 'Show 9:16',
|
|
477
524
|
})
|
|
478
525
|
const safeName = (record.outputname || record.output_name || record.id || `variant-${idx + 1}-${compiled.sceneHash}`)
|