openutau-mcp 0.0.0-stage → 0.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 (3) hide show
  1. package/README.md +82 -2
  2. package/bin.js +154 -0
  3. package/package.json +30 -4
package/README.md CHANGED
@@ -1,3 +1,83 @@
1
- # Temporary Holding Version
1
+ # openutau-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ MCP server that lets AI assistants create/edit .ustx projects and render WAV with the
4
+ OpenUtau engine — no GUI, no audio device, no .NET SDK required. Works on
5
+ **Windows / macOS / Linux** (x64 & arm64).
6
+
7
+ ## Use with any MCP client
8
+
9
+ ```bash
10
+ npx -y openutau-mcp
11
+ ```
12
+
13
+ Client config example (ZCode / Claude Desktop style):
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "openutau": {
19
+ "command": "npx",
20
+ "args": ["-y", "openutau-mcp"]
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ ## How it works
27
+
28
+ This package is a thin launcher (zero npm dependencies, ~3KB). The platform
29
+ engine ships in per-platform packages (`openutau-mcp-linux-x64`,
30
+ `openutau-mcp-darwin-arm64`, `openutau-mcp-win-x64`, …) that npm installs
31
+ automatically via `optionalDependencies` — only the one matching your OS and
32
+ CPU is downloaded. On first run the engine (~80MB compressed) is extracted once
33
+ to a user cache directory, then speaks MCP over stdio with your client. Later
34
+ runs start instantly from cache.
35
+
36
+ The engine is built from the [OpenUtau source](https://github.com/stakira/OpenUtau)
37
+ (`OpenUtauMcp` project, .NET 10, self-contained): it hosts the real
38
+ `OpenUtau.Core` render pipeline — phonemizers, resamplers (worldline), mixing —
39
+ headlessly, and exposes it over MCP.
40
+
41
+ Environment variables / flags:
42
+
43
+ | Variable | Meaning |
44
+ |---|---|
45
+ | `OPENUTAU_MCP_HOME` | Override cache/install directory |
46
+ | `OPENUTAU_MCP_SINGER_PATHS` | Extra voicebank search directories (path-separator separated) |
47
+ | `OPENUTAU_MCP_DIST_URL` | Download base, only used if the platform package is missing |
48
+ | `--engine-dir=/path` | Skip extraction, use a preinstalled engine copy |
49
+
50
+ ## Voicebanks
51
+
52
+ Voicebanks are not bundled (licensed separately). Install into your OpenUtau
53
+ data directory's `Singers/` folder:
54
+
55
+ | Platform | Data directory |
56
+ |---|---|
57
+ | Linux | `~/.local/share/OpenUtau/` |
58
+ | macOS | `~/Library/OpenUtau/` |
59
+ | Windows (portable) | OpenUtau program directory |
60
+ | Windows (installed) | `Documents/OpenUtau/` |
61
+
62
+ e.g. `ln -s /path/to/voicebank ~/.local/share/OpenUtau/Singers/`
63
+
64
+ ## Tools
65
+
66
+ `list_voicebanks`, `list_phonemizers`, `create_project`, `load_project`, `save_project`,
67
+ `add_track`, `add_notes`, `set_pitch`, `set_note_expression`, `render`, `get_project_info`.
68
+
69
+ Tips: one hanzi per note; pass `phonemizer: "ZH CVVC"` in `add_track` for Chinese lyrics;
70
+ use per-note `vibrato` to avoid robotic renders.
71
+
72
+ ## Building the engines
73
+
74
+ Engine sources live in the OpenUtau tree under `OpenUtauMcp/`. Build with the
75
+ .NET 10 SDK:
76
+
77
+ ```bash
78
+ dotnet publish OpenUtauMcp/OpenUtauMcp.csproj -c Release -r <rid> --self-contained true -o out/<rid>
79
+ ```
80
+
81
+ Cross-publishing from Linux: the CPU ONNX Runtime native libraries must be
82
+ added to the win/osx/linux-arm64 outputs manually (they come from the
83
+ `Microsoft.ML.OnnxRuntime` nupkg — see `repack.sh` in the package repo).
package/bin.js ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ // Thin npm launcher for the OpenUtau MCP engine (self-contained .NET binary,
3
+ // ~90MB compressed per platform). Engine tarballs ship in per-platform sibling
4
+ // packages (openutau-mcp-<platform>-<arch>) selected by npm's optional
5
+ // dependencies; on first run the tarball is extracted once to a user cache
6
+ // directory, then spawned and bridged to MCP stdio. Zero npm dependencies.
7
+ import { spawn, execFileSync } from "node:child_process";
8
+ import { copyFileSync, createWriteStream, existsSync, mkdirSync, readdirSync } from "node:fs";
9
+ import { get as httpsGet } from "node:https";
10
+ import { homedir, tmpdir } from "node:os";
11
+ import * as path from "node:path";
12
+ import { createRequire } from "node:module";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const VERSION = "0.2.0";
16
+ const PKG_DIR = path.dirname(fileURLToPath(import.meta.url));
17
+ const require = createRequire(import.meta.url);
18
+
19
+ // engine/openutau-mcp-<plat>-v<VERSION>.tar.gz inside the platform package
20
+ const PLATFORM = `${process.platform}-${process.arch}`;
21
+ const TARBALL_NAME = `openutau-mcp-${PLATFORM}-v${VERSION}.tar.gz`;
22
+
23
+ // Explicit engine dir wins; otherwise extract-and-cache under a per-OS cache dir.
24
+ const engineDirFlag = process.argv.find(a => a.startsWith("--engine-dir="))?.split("=")[1];
25
+ const cacheRoot = () => {
26
+ if (process.env.OPENUTAU_MCP_HOME) return process.env.OPENUTAU_MCP_HOME;
27
+ if (process.platform === "win32") {
28
+ return path.join(process.env.LOCALAPPDATA || path.join(homedir(), "AppData", "Local"), "openutau-mcp");
29
+ }
30
+ if (process.platform === "darwin") {
31
+ return path.join(homedir(), "Library", "Caches", "openutau-mcp");
32
+ }
33
+ return path.join(process.env.XDG_CACHE_HOME || path.join(homedir(), ".cache"), "openutau-mcp");
34
+ };
35
+ const CACHE = engineDirFlag ? path.resolve(engineDirFlag) : path.join(cacheRoot(), `v${VERSION}`);
36
+ const ENGINE = process.platform === "win32" ? "OpenUtauMcp.exe" : "OpenUtauMcp";
37
+ const EXE = path.join(CACHE, ENGINE);
38
+
39
+ const log = msg => console.error(`[openutau-mcp] ${msg}`);
40
+
41
+ function download(url, dest) {
42
+ if (url.startsWith("file://")) {
43
+ copyFileSync(url.slice("file://".length), dest);
44
+ return Promise.resolve();
45
+ }
46
+ return new Promise((resolve, reject) => {
47
+ const req = httpsGet(url, res => {
48
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
49
+ res.resume();
50
+ return resolve(download(res.headers.location, dest));
51
+ }
52
+ if (res.statusCode !== 200) {
53
+ res.resume();
54
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
55
+ }
56
+ const total = Number(res.headers["content-length"] || 0);
57
+ let got = 0, nextStep = 0.25;
58
+ const out = createWriteStream(dest);
59
+ res.on("data", chunk => {
60
+ got += chunk.length;
61
+ if (total && got / total >= nextStep) {
62
+ log(`${Math.round(got / 1048576)}MB / ${Math.round(total / 1048576)}MB`);
63
+ nextStep += 0.25;
64
+ }
65
+ });
66
+ res.on("error", reject);
67
+ out.on("error", reject);
68
+ out.on("finish", () => resolve());
69
+ res.pipe(out);
70
+ });
71
+ req.on("error", reject);
72
+ });
73
+ }
74
+
75
+ function findBundledTarball() {
76
+ // 1. Next to this package (dev layout): node_modules/<plat-pkg>/engine/.
77
+ try {
78
+ const platPkg = path.join(
79
+ path.dirname(require.resolve(`openutau-mcp-${PLATFORM}/package.json`)),
80
+ "engine", TARBALL_NAME);
81
+ if (existsSync(platPkg)) return platPkg;
82
+ } catch { /* platform package not installed */ }
83
+ // 2. Monorepo layout: sibling directories next to the main package.
84
+ const sibling = path.join(PKG_DIR, "..", `openutau-mcp-${PLATFORM}`, "engine", TARBALL_NAME);
85
+ if (existsSync(sibling)) return sibling;
86
+ // 3. Main package itself (legacy single-package layout).
87
+ const bundled = path.join(PKG_DIR, "engine", TARBALL_NAME);
88
+ if (existsSync(bundled)) return bundled;
89
+ return null;
90
+ }
91
+
92
+ async function ensureEngine() {
93
+ if (existsSync(EXE)) return;
94
+ const bundled = findBundledTarball();
95
+ if (bundled) {
96
+ mkdirSync(CACHE, { recursive: true });
97
+ log(`extracting bundled engine to ${CACHE}`);
98
+ execFileSync("tar", ["xzf", bundled, "-C", CACHE]);
99
+ if (process.platform !== "win32") {
100
+ execFileSync("chmod", ["+x", EXE]);
101
+ }
102
+ log(`engine ready`);
103
+ return;
104
+ }
105
+ const DIST_BASE = process.env.OPENUTAU_MCP_DIST_URL || "";
106
+ if (!DIST_BASE) {
107
+ throw new Error(
108
+ `no engine for ${PLATFORM}: platform package "openutau-mcp-${PLATFORM}" is not installed ` +
109
+ `and OPENUTAU_MCP_DIST_URL is not set`);
110
+ }
111
+ mkdirSync(CACHE, { recursive: true });
112
+ const url = `${DIST_BASE}/openutau-mcp-${PLATFORM}-v${VERSION}.tar.gz`;
113
+ const tarball = path.join(tmpdir(), TARBALL_NAME);
114
+ log(`engine not cached; downloading ${url}`);
115
+ await download(url, tarball);
116
+ log(`extracting to ${CACHE}`);
117
+ execFileSync("tar", ["xzf", tarball, "-C", CACHE]);
118
+ if (process.platform !== "win32") {
119
+ execFileSync("chmod", ["+x", EXE]);
120
+ }
121
+ log(`engine ready`);
122
+ }
123
+
124
+ function warnNoVoicebanks() {
125
+ let singers;
126
+ if (process.platform === "linux") {
127
+ singers = path.join(process.env.XDG_DATA_HOME || path.join(homedir(), ".local", "share"), "OpenUtau", "Singers");
128
+ } else if (process.platform === "darwin") {
129
+ singers = path.join(homedir(), "Library", "OpenUtau", "Singers");
130
+ } else {
131
+ singers = path.join(process.env.OPENUTAU_MCP_HOME || path.join(homedir(), "Documents", "OpenUtau"), "Singers");
132
+ }
133
+ if (existsSync(singers) && readdirSync(singers).some(n => !n.startsWith("."))) return;
134
+ log(`no voicebanks in ${singers}`);
135
+ log(`add one: mkdir -p "${singers}" && ln -s /path/to/voicebank "${singers}/"`);
136
+ log(`or set OPENUTAU_MCP_SINGER_PATHS to a directory containing voicebanks`);
137
+ }
138
+
139
+ await ensureEngine();
140
+ warnNoVoicebanks();
141
+
142
+ const child = spawn(EXE, [], { cwd: CACHE, stdio: "inherit" });
143
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
144
+ process.on(sig, () => child.kill("SIGTERM"));
145
+ }
146
+ child.on("error", err => {
147
+ if (process.platform === "darwin" && (err.code === "EPERM" || err.code === "EACCES")) {
148
+ log(`macOS blocked the engine (Gatekeeper). Run: xattr -rc "${CACHE}"`);
149
+ } else {
150
+ log(`failed to start engine: ${err.message}`);
151
+ }
152
+ process.exit(1);
153
+ });
154
+ child.on("exit", code => process.exit(code ?? 1));
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "openutau-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "MCP server for AI singing: create/edit .ustx projects and render WAV with OpenUtau engine (no GUI needed)",
5
+ "type": "module",
6
+ "bin": {
7
+ "openutau-mcp": "./bin.js"
8
+ },
9
+ "optionalDependencies": {
10
+ "openutau-mcp-linux-x64": "0.2.0",
11
+ "openutau-mcp-linux-arm64": "0.2.0",
12
+ "openutau-mcp-win-x64": "0.2.0",
13
+ "openutau-mcp-win-arm64": "0.2.0",
14
+ "openutau-mcp-darwin-arm64": "0.2.0"
15
+ },
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "license": "MIT",
20
+ "files": [
21
+ "bin.js",
22
+ "README.md"
23
+ ],
24
+ "keywords": [
25
+ "mcp",
26
+ "openutau",
27
+ "utau",
28
+ "singing",
29
+ "voicebank",
30
+ "tts"
31
+ ]
32
+ }