@audiolabtools/mcp-server 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +91 -90
  2. package/hosted-server.mjs +72 -10
  3. package/package.json +47 -43
package/README.md CHANGED
@@ -1,90 +1,91 @@
1
- # @audiolabtools/mcp-server
2
-
3
- MCP ([Model Context Protocol](https://modelcontextprotocol.io)) server that gives any
4
- MCP-capable AI — Claude Desktop, Claude Code, Cursor, and others — eight audio-analysis
5
- tools, backed by the hosted **AudioLab** API. It is a thin HTTP client: **no local audio
6
- engine, no ffmpeg, nothing to compile.** It can analyse a **public URL** or a **local file**
7
- on your machine.
8
-
9
- ## Install
10
-
11
- Point your MCP client at the package via `npx` (nothing to install globally):
12
-
13
- ```json
14
- {
15
- "mcpServers": {
16
- "audiolab": {
17
- "command": "npx",
18
- "args": ["-y", "@audiolabtools/mcp-server"],
19
- "env": { "AUDIOLAB_API_KEY": "al_live_yourkey" }
20
- }
21
- }
22
- }
23
- ```
24
-
25
- Get a key: sign in at **https://audiolab.tools/account** and generate one (free tier available).
26
-
27
- ## Requirements
28
-
29
- - **Node ≥ 18** — uses the built-in global `fetch` + `AbortSignal.timeout`.
30
- - An `AUDIOLAB_API_KEY`. No ffmpeg, no native dependencies.
31
-
32
- ## Tools
33
-
34
- Every tool takes **one audio source** — a public `url` **or** a local `path`:
35
-
36
- - `{ url: "https://…" }` — a public https URL the API fetches server-side.
37
- - `{ path: "./mix.wav" }` — a file on the machine running this server. Files up to **4 MB**
38
- are sent inline; larger files (up to **50 MB**) upload over a one-shot signed URL, are
39
- analysed, and are then deleted. *(Local `path` works only in this stdio server, not the
40
- remote `/mcp` endpoint.)*
41
-
42
- | Tool | Returns |
43
- |---|---|
44
- | `analyze_loudness` | Integrated LUFS (EBU R128 / BS.1770-4), true-peak (dBTP), LRA, crest factor, stereo correlation, mono compatibility, tonal balance |
45
- | `check_target` | Pass/fail vs a delivery target (`spotify` / `apple-music` / `youtube` / `tidal` / `amazon-music` / `podcast` / `ebu-broadcast` / `atsc-broadcast`, or `target:"custom"` + `lufs`+`tp`), with per-metric deltas and an ffmpeg loudnorm fix command |
46
- | `analyze_timeseries` | Short-term LUFS over time + downsampled waveform peaks (`waveformPoints?`) |
47
- | `get_spectrum` | FFT magnitude data + 7-band energies + dominant band |
48
- | `analyze_voice` | Voice QA: speech/silence ratio, speaking rate, SNR, noise floor, room echo, sibilance & clipping risk |
49
- | `get_speech_segments` | Voiced regions with start/end + per-segment RMS (auto-trim, chapters) |
50
- | `index_signal` | Content-type guess, tags, clipping/silence regions, brightness & dynamics buckets |
51
- | `compare_loudness` | A/B on two sources (`urlA`/`pathA` + `urlB`/`pathB`), returns both results |
52
-
53
- Example asks to your AI:
54
-
55
- - *“Analyze the loudness of https://example.com/track.wav”* → `analyze_loudness` with `url`
56
- - *“Run loudness on ./master.wav”* → `analyze_loudness` with `path`
57
- - *“Does ./mix.mp3 pass Spotify?”* → `check_target` with `path` + `target:"spotify"`
58
-
59
- ## Configuration (env)
60
-
61
- | Var | Default | Purpose |
62
- |---|---|---|
63
- | `AUDIOLAB_API_KEY` | — (required) | Your API key. |
64
- | `AUDIOLAB_API_BASE` | `https://audiolab.tools/v1` | Override the API base (must be `https://`). |
65
- | `AUDIOLAB_TIMEOUT_MS` | `60000` | Per-request timeout in milliseconds. |
66
-
67
- ## Privacy
68
-
69
- Analysis happens on the AudioLab API, so the audio **does reach `audiolab.tools`** — a `url`
70
- is fetched server-side, and a local `path` is sent to the API (small files inline; larger
71
- files via a private one-shot signed upload that is deleted right after analysis). The API
72
- returns **numbers only** and does not retain your audio (see https://audiolab.tools/privacy).
73
- This package has no telemetry and writes nothing to disk. If audio must never leave the
74
- machine, don't use a hosted analyser.
75
-
76
- ## Limits
77
-
78
- - Local files: up to **50 MB** (host bigger ones at a public URL).
79
- - One file per call (agents loop for many); one-shot (no streaming/realtime).
80
- - Rate and monthly limits are enforced by the API, per key.
81
-
82
- ## Smoke test
83
-
84
- ```sh
85
- node hosted-server.mjs --selftest # verifies the 8 tools + guards; no network
86
- ```
87
-
88
- ## License
89
-
90
- MIT © Nathan Renting
1
+ # @audiolabtools/mcp-server
2
+
3
+ MCP ([Model Context Protocol](https://modelcontextprotocol.io)) server that gives any
4
+ MCP-capable AI — Claude Desktop, Claude Code, Cursor, and others — nine audio-analysis
5
+ tools, backed by the hosted **AudioLab** API. It is a thin HTTP client: **no local audio
6
+ engine, no ffmpeg, nothing to compile.** It can analyse a **public URL** or a **local file**
7
+ on your machine.
8
+
9
+ ## Install
10
+
11
+ Point your MCP client at the package via `npx` (nothing to install globally):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "audiolab": {
17
+ "command": "npx",
18
+ "args": ["-y", "@audiolabtools/mcp-server"],
19
+ "env": { "AUDIOLAB_API_KEY": "al_live_yourkey" }
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ Get a key: sign in at **https://audiolab.tools/account** and generate one (free tier available).
26
+
27
+ ## Requirements
28
+
29
+ - **Node ≥ 18** — uses the built-in global `fetch` + `AbortSignal.timeout`.
30
+ - An `AUDIOLAB_API_KEY`. No ffmpeg, no native dependencies.
31
+
32
+ ## Tools
33
+
34
+ Every tool takes **one audio source** — a public `url` **or** a local `path`:
35
+
36
+ - `{ url: "https://…" }` — a public https URL the API fetches server-side.
37
+ - `{ path: "./mix.wav" }` — a file on the machine running this server. Files up to **4 MB**
38
+ are sent inline; larger files (up to **50 MB**) upload over a one-shot signed URL, are
39
+ analysed, and are then deleted. *(Local `path` works only in this stdio server, not the
40
+ remote `/mcp` endpoint.)*
41
+
42
+ | Tool | Returns |
43
+ |---|---|
44
+ | `analyze_loudness` | Integrated LUFS (EBU R128 / BS.1770-4), true-peak (dBTP), LRA, crest factor, stereo correlation, mono compatibility, tonal balance |
45
+ | `check_target` | Pass/fail vs a delivery target (`spotify` / `apple-music` / `youtube` / `tidal` / `amazon-music` / `podcast` / `ebu-broadcast` / `atsc-broadcast`, or `target:"custom"` + `lufs`+`tp`), with per-metric deltas and an ffmpeg loudnorm fix command |
46
+ | `analyze_timeseries` | Short-term LUFS over time + downsampled waveform peaks (`waveformPoints?`) |
47
+ | `get_spectrum` | FFT magnitude data + 7-band energies + dominant band |
48
+ | `analyze_voice` | Voice QA: speech/silence ratio, speaking rate, SNR, noise floor, room echo, sibilance & clipping risk |
49
+ | `get_speech_segments` | Voiced regions with start/end + per-segment RMS (auto-trim, chapters) |
50
+ | `index_signal` | Content-type guess, tags, clipping/silence regions, brightness & dynamics buckets |
51
+ | `compare_loudness` | A/B on two sources (`urlA`/`pathA` + `urlB`/`pathB`), returns both results |
52
+ | `analyze_batch` | One route over up to 20 sources in a single call (`urls` and/or `paths`), per-item ok/data/error. For folder QA, library indexing, or checking a whole release against a target. Each item meters as one call |
53
+
54
+ Example asks to your AI:
55
+
56
+ - *“Analyze the loudness of https://example.com/track.wav”* → `analyze_loudness` with `url`
57
+ - *“Run loudness on ./master.wav”* → `analyze_loudness` with `path`
58
+ - *“Does ./mix.mp3 pass Spotify?”* → `check_target` with `path` + `target:"spotify"`
59
+
60
+ ## Configuration (env)
61
+
62
+ | Var | Default | Purpose |
63
+ |---|---|---|
64
+ | `AUDIOLAB_API_KEY` | — (required) | Your API key. |
65
+ | `AUDIOLAB_API_BASE` | `https://audiolab.tools/v1` | Override the API base (must be `https://`). |
66
+ | `AUDIOLAB_TIMEOUT_MS` | `60000` | Per-request timeout in milliseconds. |
67
+
68
+ ## Privacy
69
+
70
+ Analysis happens on the AudioLab API, so the audio **does reach `audiolab.tools`** — a `url`
71
+ is fetched server-side, and a local `path` is sent to the API (small files inline; larger
72
+ files via a private one-shot signed upload that is deleted right after analysis). The API
73
+ returns **numbers only** and does not retain your audio (see https://audiolab.tools/privacy).
74
+ This package has no telemetry and writes nothing to disk. If audio must never leave the
75
+ machine, don't use a hosted analyser.
76
+
77
+ ## Limits
78
+
79
+ - Local files: up to **50 MB** (host bigger ones at a public URL).
80
+ - One file per call (agents loop for many); one-shot (no streaming/realtime).
81
+ - Rate and monthly limits are enforced by the API, per key.
82
+
83
+ ## Smoke test
84
+
85
+ ```sh
86
+ node hosted-server.mjs --selftest # verifies the 9 tools + guards; no network
87
+ ```
88
+
89
+ ## License
90
+
91
+ MIT © Nathan Renting
package/hosted-server.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // AudioLab HOSTED-mode MCP server. Exposes the 8 AudioLab tools to any MCP-capable
2
+ // AudioLab HOSTED-mode MCP server. Exposes the 9 AudioLab tools to any MCP-capable
3
3
  // AI (Claude Desktop / Claude Code, Cursor, etc.) by calling the hosted API at
4
4
  // audiolab.tools/v1/*. It contains NO engine code, just HTTP calls, so it is safe to
5
5
  // distribute publicly without exposing the proprietary engine.
@@ -97,9 +97,9 @@ async function postRaw(route, buf, ct, extra, key) {
97
97
  });
98
98
  }
99
99
 
100
- // Larger local file → mint a signed upload URL, PUT the bytes to storage, then analyse by
101
- // objectPath. The objectPath is namespaced to this key server-side; only this key can read it.
102
- async function postUpload(route, buf, ct, fileName, extra, key) {
100
+ // Mint a signed upload URL and PUT the bytes to storage; returns the objectPath.
101
+ // The objectPath is namespaced to this key server-side; only this key can read it.
102
+ async function uploadToStorage(buf, ct, key) {
103
103
  const k = requireKey(key);
104
104
  const origin = new URL(apiBase()).origin;
105
105
  const mint = await request(`${origin}/api/upload-url`, {
@@ -115,7 +115,46 @@ async function postUpload(route, buf, ct, fileName, extra, key) {
115
115
  throw new Error(`Upload to storage failed: ${String(e?.message || e)}`);
116
116
  }
117
117
  if (!put.ok) throw new Error(`Upload to storage failed (HTTP ${put.status}).`);
118
- return call(route, { objectPath: mint.objectPath, fileName, ...extra }, k);
118
+ return mint.objectPath;
119
+ }
120
+
121
+ // Larger local file → upload to storage, then analyse by objectPath.
122
+ async function postUpload(route, buf, ct, fileName, extra, key) {
123
+ const objectPath = await uploadToStorage(buf, ct, key);
124
+ return call(route, { objectPath, fileName, ...extra }, key);
125
+ }
126
+
127
+ // Read + size-check one local file for upload flows.
128
+ async function readLocal(p) {
129
+ let buf;
130
+ try { buf = await readFile(p); }
131
+ catch (e) { throw new Error(`Could not read local file "${p}": ${e?.code || e?.message || e}`); }
132
+ if (!buf.length) throw new Error(`Local file is empty: "${p}".`);
133
+ if (buf.length > STORAGE_MAX) throw new Error(`"${p}" is ${(buf.length / 1048576).toFixed(1)} MB; max ${STORAGE_MAX / 1048576} MB per file. Host it at a public https URL instead.`);
134
+ return buf;
135
+ }
136
+
137
+ // Batch: one analysis route over many sources in a single /v1/batch call.
138
+ // Local paths are uploaded to storage first (one-shot signed URLs), then the
139
+ // whole set goes out as ONE call with urls + objectPaths. Max 20 combined,
140
+ // matching the server-side cap; each item is metered as one call server-side.
141
+ const BATCH_MAX = 20;
142
+ async function batchSources({ urls = [], paths = [] }, endpoint, extra, key, local = false) {
143
+ if (paths.length && !local) throw new Error('Local file paths are only supported by the local (stdio) MCP server; use public https urls instead.');
144
+ const total = urls.length + paths.length;
145
+ if (!total) throw new Error('Provide urls (public https) and/or paths (local files).');
146
+ if (total > BATCH_MAX) throw new Error(`Batch too large: max ${BATCH_MAX} items per call. Split into smaller batches.`);
147
+ const objectPaths = [];
148
+ for (const p of paths) {
149
+ const buf = await readLocal(p);
150
+ objectPaths.push(await uploadToStorage(buf, ctForPath(p), key));
151
+ }
152
+ const args = Object.fromEntries(Object.entries(extra || {}).filter(([, v]) => v !== undefined && v !== null && v !== ''));
153
+ const body = { endpoint };
154
+ if (urls.length) body.urls = urls;
155
+ if (objectPaths.length) body.objectPaths = objectPaths;
156
+ if (Object.keys(args).length) body.args = args;
157
+ return call('batch', body, key);
119
158
  }
120
159
 
121
160
  // Resolve one audio source (url OR local path) to a route call. `local` gates path reads:
@@ -155,7 +194,7 @@ const sourceShape = (local) => local
155
194
  : { url: z.string().url().describe('Public https URL to the audio file.') };
156
195
 
157
196
  export function buildServer({ apiKey, local = false } = {}) {
158
- const server = new McpServer({ name: 'audiolab', version: '0.2.0' }); // keep in sync with package.json
197
+ const server = new McpServer({ name: 'audiolab', version: '0.3.0' }); // keep in sync with package.json
159
198
  const toolNames = [];
160
199
  const tool = (name, def, handler) => { server.registerTool(name, def, handler); toolNames.push(name); };
161
200
  const src = sourceShape(local);
@@ -227,6 +266,25 @@ export function buildServer({ apiKey, local = false } = {}) {
227
266
  return { a, b };
228
267
  }));
229
268
 
269
+ // analyze_batch — one route over many sources, a single metered-per-item call.
270
+ const batchSrc = local
271
+ ? {
272
+ urls: z.array(z.string().url()).optional().describe('Public https URLs to audio files. Combined max 20 items with paths.'),
273
+ paths: z.array(z.string()).optional().describe('LOCAL audio file paths; each uploads over a one-shot signed URL, is analysed, then deleted. Combined max 20 items with urls.'),
274
+ }
275
+ : { urls: z.array(z.string().url()).min(1).max(20).describe('Public https URLs to audio files (max 20).') };
276
+ tool('analyze_batch', {
277
+ title: 'Batch: one analysis route over many files',
278
+ description: 'Run one analysis route over up to 20 audio sources in a single call. Returns per-item ok/data/error; each item is metered as one call. Routes: mixlab/analyze (default), mixlab/check-target, mixlab/timeseries, mixlab/spectrum, voicelab/qa, voicelab/segments, signallab/index. Use for folder QA, library indexing, or checking a whole release against a loudness target.' + (local ? ' Sources: public https urls and/or local file paths.' : ' Sources: public https urls.'),
279
+ inputSchema: {
280
+ ...batchSrc,
281
+ endpoint: z.string().optional().describe('Analysis route to run for every item (default mixlab/analyze).'),
282
+ target: z.string().optional().describe('For mixlab/check-target: preset id (spotify, ebu-broadcast, podcast, …) or "custom".'),
283
+ lufs: z.number().optional().describe('For target="custom": integrated LUFS target.'),
284
+ tp: z.number().optional().describe('For target="custom": max true-peak (dBTP).'),
285
+ },
286
+ }, wrap((i) => batchSources({ urls: i.urls || [], paths: i.paths || [] }, i.endpoint || 'mixlab/analyze', { target: i.target, lufs: i.lufs, tp: i.tp }, apiKey, local)));
287
+
230
288
  return { server, toolNames };
231
289
  }
232
290
 
@@ -236,9 +294,9 @@ const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv
236
294
 
237
295
  if (isMain && process.argv.includes('--selftest')) {
238
296
  const assert = (await import('node:assert/strict')).default;
239
- const expected = ['analyze_loudness', 'analyze_timeseries', 'analyze_voice', 'check_target', 'compare_loudness', 'get_spectrum', 'get_speech_segments', 'index_signal'];
240
- assert.deepEqual(buildServer().toolNames.slice().sort(), expected, 'eight hosted tools (remote / url-only)');
241
- assert.deepEqual(buildServer({ local: true }).toolNames.slice().sort(), expected, 'eight hosted tools (local / url+path)');
297
+ const expected = ['analyze_batch', 'analyze_loudness', 'analyze_timeseries', 'analyze_voice', 'check_target', 'compare_loudness', 'get_spectrum', 'get_speech_segments', 'index_signal'];
298
+ assert.deepEqual(buildServer().toolNames.slice().sort(), expected, 'nine hosted tools (remote / url-only)');
299
+ assert.deepEqual(buildServer({ local: true }).toolNames.slice().sort(), expected, 'nine hosted tools (local / url+path)');
242
300
 
243
301
  const prev = process.env.AUDIOLAB_API_KEY;
244
302
  delete process.env.AUDIOLAB_API_KEY;
@@ -249,9 +307,13 @@ if (isMain && process.argv.includes('--selftest')) {
249
307
  await assert.rejects(() => analyzeSource('mixlab/analyze', { path: '/etc/hostname' }, {}, 'al_live_selftest', false), /only supported by the local/, 'remote MCP never reads a server-side path');
250
308
  // url + path together is a usage error.
251
309
  await assert.rejects(() => analyzeSource('mixlab/analyze', { url: 'https://x/a.wav', path: '/tmp/a.wav' }, {}, 'al_live_selftest', true), /exactly one/, 'url+path together rejected');
310
+ // Batch guards fire BEFORE any network or filesystem work.
311
+ await assert.rejects(() => batchSources({ paths: ['/tmp/a.wav'] }, 'mixlab/analyze', {}, 'al_live_selftest', false), /only supported by the local/, 'remote batch never reads server-side paths');
312
+ await assert.rejects(() => batchSources({ urls: [], paths: [] }, 'mixlab/analyze', {}, 'al_live_selftest', true), /Provide urls/, 'empty batch rejected');
313
+ await assert.rejects(() => batchSources({ urls: Array.from({ length: 21 }, (_, n) => `https://x/${n}.wav`) }, 'mixlab/analyze', {}, 'al_live_selftest', false), /max 20/, 'oversized batch rejected before network');
252
314
  if (prev) process.env.AUDIOLAB_API_KEY = prev; else delete process.env.AUDIOLAB_API_KEY;
253
315
 
254
- console.log('selftest ok · 8 tools (remote url-only + local url|path) + missing-key guard + remote-path refusal');
316
+ console.log('selftest ok · 9 tools (incl. analyze_batch) + missing-key guard + remote-path refusals + batch guards');
255
317
  process.exit(0);
256
318
  }
257
319
 
package/package.json CHANGED
@@ -1,43 +1,47 @@
1
- {
2
- "name": "@audiolabtools/mcp-server",
3
- "version": "0.2.0",
4
- "description": "MCP server for AudioLab — loudness (EBU R128 / BS.1770-4), true-peak, voice-quality, and signal analysis for AI agents (Claude, Cursor, any MCP client), via the AudioLab hosted API. No local audio engine required.",
5
- "type": "module",
6
- "bin": {
7
- "audiolabtools-mcp-server": "hosted-server.mjs"
8
- },
9
- "files": ["hosted-server.mjs", "README.md"],
10
- "keywords": [
11
- "mcp",
12
- "model-context-protocol",
13
- "claude",
14
- "audio",
15
- "audio-analysis",
16
- "loudness",
17
- "lufs",
18
- "ebu-r128",
19
- "bs-1770",
20
- "true-peak",
21
- "voice-quality",
22
- "audiolab"
23
- ],
24
- "homepage": "https://audiolab.tools/api",
25
- "bugs": { "url": "https://audiolab.tools/api" },
26
- "repository": {
27
- "type": "git",
28
- "url": "git+https://github.com/Audio-Launch/audiolab-tools.git",
29
- "directory": "mcp"
30
- },
31
- "license": "MIT",
32
- "author": "Nathan Renting",
33
- "publishConfig": {
34
- "access": "public"
35
- },
36
- "engines": {
37
- "node": ">=18"
38
- },
39
- "dependencies": {
40
- "@modelcontextprotocol/sdk": "^1.29.0",
41
- "zod": "^4.4.3"
42
- }
43
- }
1
+ {
2
+ "name": "@audiolabtools/mcp-server",
3
+ "version": "0.3.1",
4
+ "description": "MCP server for AudioLab \u2014 loudness (EBU R128 / BS.1770-4), true-peak, voice-quality, and signal analysis for AI agents (Claude, Cursor, any MCP client), via the AudioLab hosted API. No local audio engine required.",
5
+ "type": "module",
6
+ "bin": {
7
+ "audiolabtools-mcp-server": "hosted-server.mjs"
8
+ },
9
+ "files": [
10
+ "hosted-server.mjs",
11
+ "README.md"
12
+ ],
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "claude",
17
+ "audio",
18
+ "audio-analysis",
19
+ "loudness",
20
+ "lufs",
21
+ "ebu-r128",
22
+ "bs-1770",
23
+ "true-peak",
24
+ "voice-quality",
25
+ "audiolab"
26
+ ],
27
+ "homepage": "https://audiolab.tools/api",
28
+ "bugs": {
29
+ "url": "https://github.com/Audio-Launch/audiolab-mcp-server/issues"
30
+ },
31
+ "license": "MIT",
32
+ "author": "Nathan Renting",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.29.0",
41
+ "zod": "^4.4.3"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/Audio-Launch/audiolab-mcp-server.git"
46
+ }
47
+ }