@audiolabtools/mcp-server 0.1.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 +107 -0
  2. package/hosted-server.mjs +142 -0
  3. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # AudioLab MCP server
2
+
3
+ Exposes the proprietary AudioLab analysis engine as [Model Context Protocol](https://modelcontextprotocol.io)
4
+ tools, so an AI agent (Claude Desktop, Claude Code, or any MCP client) can analyze audio.
5
+
6
+ Audio sources accepted:
7
+ - **Public URL** (`{url: "https://..."}`): fetched and decoded server-side
8
+ - **Local file path** (`{path: "./song.wav"}`): read server-side from disk
9
+
10
+ The MCP server runs on stdio in the user's own process, so local-file access is the
11
+ same threat model as a CLI the user installs themselves.
12
+
13
+ Nothing is uploaded by the caller in either case. Responses are **curated** JSON
14
+ (per-sample waveform/spectrum arrays are dropped so the response is useful to an
15
+ LLM, not overwhelming).
16
+
17
+ ## Two modes (pick by who runs it)
18
+
19
+ | | `mcp/server.mjs` (local) | `mcp/hosted-server.mjs` (hosted) |
20
+ |---|---|---|
21
+ | Runs | the proprietary engine locally (bundled ffmpeg) | HTTP calls to the hosted API `audiolab.tools/v1/*` |
22
+ | Audio source | `{url}` **or** `{path}` (on-device, never leaves the machine) | `{url}` only |
23
+ | Needs | the repo + engine (private) | an `AUDIOLAB_API_KEY` |
24
+ | Distribute? | **No** (it contains the proprietary engine; keep it repo-only) | **Yes** (no engine code, safe to hand to Claude & other AIs) |
25
+
26
+ Give **hosted mode** to Claude Desktop / Claude Code / Cursor / any MCP client:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "audiolab": {
32
+ "command": "node",
33
+ "args": ["/abs/path/to/mcp/hosted-server.mjs"],
34
+ "env": { "AUDIOLAB_API_KEY": "al_live_yourkey" }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ Get a key from partners@audiolab.tools (self-serve opens after the first partners).
41
+ Both modes expose the identical eight tools below.
42
+
43
+ ## Tools: eight, all single-shot
44
+
45
+ | Tool | Lab | Input | Returns |
46
+ |---|---|---|---|
47
+ | `analyze_loudness` | MixLab | `{url}` or `{path}` | Integrated LUFS (EBU R128 / BS.1770-4), true-peak, loudness range, crest factor, stereo correlation, tonal balance, harshness/muddiness labels |
48
+ | `check_target` | MixLab | `{url\|path, target, lufs?, tp?}` | Pass/fail vs Spotify -14 / Apple-Music -16 / YouTube -14 / TIDAL -14 / Amazon -14 / podcast -16 / EBU-broadcast -23 / ATSC-broadcast -24 (or `"custom"` + lufs+tp). Includes deltas, issue messages, ffmpeg loudnorm fix command |
49
+ | `analyze_timeseries` | MixLab | `{url\|path, waveformPoints?}` | Short-term LUFS samples + downsampled waveform peaks over time (for graphing/UI) |
50
+ | `get_spectrum` | MixLab | `{url}` or `{path}` | FFT bins (freqs+mags) + 7-band energies + centroid/rolloff/flatness |
51
+ | `analyze_voice` | VoiceLab | `{url}` or `{path}` | Speech/silence ratio, speaking rate, SNR, noise floor, room echo, sibilance & clipping risk |
52
+ | `get_speech_segments` | VoiceLab | `{url}` or `{path}` | Voiced regions with start/end timestamps + per-segment RMS (for auto-trim, chapters, speaker-turn detection) |
53
+ | `index_signal` | SignalLab | `{url}` or `{path}` | Content-type guess, tags, clipping/silence regions, brightness & dynamics buckets, file facts |
54
+ | `compare_loudness` | MixLab A/B | `{a: {url\|path}, b: {url\|path}}` | Run MixLab on both, return both results + per-metric deltas with verdict |
55
+
56
+ Provide **exactly one** of `url` or `path` per source.
57
+
58
+ ## Requirements
59
+
60
+ - Node ≥ 22
61
+ - **ffmpeg on `PATH`**: the engine's Node decoder shells out to it.
62
+
63
+ ## Run
64
+
65
+ ```sh
66
+ npm run mcp # or: node mcp/server.mjs
67
+ node mcp/server.mjs --selftest # smoke test (4 tools), no network/ffmpeg needed
68
+ ```
69
+
70
+ The server speaks MCP over **stdio** (what desktop clients spawn). Streamable HTTP
71
+ for a remote, keyed server is a later transport addition; deploy, auth keys,
72
+ and billing are gated steps (see [`docs/PDD.md`](../docs/PDD.md) §0/§8).
73
+
74
+ ## Add to Claude Desktop / Claude Code
75
+
76
+ ```json
77
+ {
78
+ "mcpServers": {
79
+ "audiolab": {
80
+ "command": "node",
81
+ "args": ["E:/New audiolab.dev/mcp/server.mjs"]
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ (Once the package ships on npm, this becomes
88
+ `{"command": "npx", "args": ["-y", "@audiolab/mcp-server"]}`, gated on publish.)
89
+
90
+ Then ask:
91
+
92
+ - *"Analyze the loudness of https://example.com/track.wav"* → calls `analyze_loudness` with `{url}`
93
+ - *"Run loudness on ./mix.wav"* → calls `analyze_loudness` with `{path}`
94
+ - *"A/B compare ./master.wav against ./reference.wav for loudness"* → calls `compare_loudness`
95
+
96
+ ## Limits
97
+
98
+ - 200 MB per file (size cap)
99
+ - Any sample rate / channel count (engine preserves rate; multi-channel mixed to stereo for analysis)
100
+ - One file per call (no batch; agent loops for many)
101
+ - One-shot only (no streaming / realtime)
102
+
103
+ ## Fastest path to first paying partner
104
+
105
+ Deploy the hosted-API counterpart (`api/v1/[...endpoint].mjs`) + add one key +
106
+ invoice them. No Stripe, no key DB, no metering platform required to start.
107
+ Metering/credits/self-serve come later, behind the billing gate.
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ // AudioLab HOSTED-mode MCP server. Exposes the 8 AudioLab tools to any MCP-capable
3
+ // AI (Claude Desktop / Claude Code, Cursor, etc.) by calling the hosted API at
4
+ // audiolab.tools/v1/*. Unlike mcp/server.mjs it contains NO engine code, just HTTP
5
+ // calls, so it is safe to distribute publicly without exposing the proprietary engine.
6
+ //
7
+ // Two consumers share buildServer():
8
+ // 1. This file run directly = a stdio server (the npm package @audiolab/mcp-server),
9
+ // reading the key from AUDIOLAB_API_KEY.
10
+ // 2. api/mcp.mjs = the remote streamable-HTTP endpoint at /mcp, which passes the
11
+ // per-request Bearer key via buildServer({ apiKey }).
12
+ //
13
+ // Requires an API key (the API is live for design partners; email partners@audiolab.tools).
14
+ // Config in Claude Desktop's claude_desktop_config.json:
15
+ // { "mcpServers": { "audiolab": {
16
+ // "command": "npx", "args": ["-y", "@audiolab/mcp-server"],
17
+ // "env": { "AUDIOLAB_API_KEY": "al_live_yourkey" } } } }
18
+ //
19
+ // Env: AUDIOLAB_API_KEY (required at call time for stdio mode), AUDIOLAB_API_BASE
20
+ // (default https://audiolab.tools/v1). Smoke test (no network): node mcp/hosted-server.mjs --selftest
21
+
22
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
23
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
24
+ import { pathToFileURL } from 'node:url';
25
+ import { z } from 'zod';
26
+
27
+ // Read env at CALL time (not module load) so the key can be injected by the MCP host
28
+ // and so the missing-key guard is testable.
29
+ const apiBase = () => (process.env.AUDIOLAB_API_BASE || 'https://audiolab.tools/v1').replace(/\/$/, '');
30
+
31
+ // key defaults to the env var (stdio mode); the remote endpoint passes a per-request key.
32
+ export async function call(route, body, key = process.env.AUDIOLAB_API_KEY) {
33
+ if (!key) throw new Error('AUDIOLAB_API_KEY is not set. Get a key from partners@audiolab.tools.');
34
+ const res = await fetch(`${apiBase()}/${route}`, {
35
+ method: 'POST',
36
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${key}` },
37
+ body: JSON.stringify(body),
38
+ });
39
+ const text = await res.text();
40
+ let json;
41
+ try { json = JSON.parse(text); } catch { json = { raw: text.slice(0, 500) }; }
42
+ if (!res.ok) throw new Error(String(json?.error?.message || json?.error || `API returned ${res.status}`));
43
+ return json;
44
+ }
45
+
46
+ const asContent = (obj) => ({ content: [{ type: 'text', text: JSON.stringify(obj) }] });
47
+ const wrap = (fn) => async (input) => {
48
+ try { return asContent(await fn(input)); }
49
+ catch (e) { return { isError: true, content: [{ type: 'text', text: String(e?.message || e) }] }; }
50
+ };
51
+
52
+ const URL_IN = { url: z.string().url().describe('Public https URL to the audio file.') };
53
+
54
+ export function buildServer({ apiKey } = {}) {
55
+ const server = new McpServer({ name: 'audiolab', version: '0.2.0' });
56
+ const toolNames = [];
57
+ const tool = (name, def, handler) => { server.registerTool(name, def, handler); toolNames.push(name); };
58
+ // Per-request API key threaded from the transport, so we never race on process.env
59
+ // when several requests share one warm serverless instance.
60
+ const api = (route, body) => call(route, body, apiKey);
61
+
62
+ tool('analyze_loudness', {
63
+ title: 'Analyze loudness (MixLab)',
64
+ description: 'Loudness and dynamics for a track: integrated LUFS (EBU R128 / BS.1770-4), true-peak (dBTP), loudness range (LRA), crest factor, stereo correlation, mono compatibility, and tonal balance. Audio source: public https URL.',
65
+ inputSchema: URL_IN,
66
+ }, wrap(({ url }) => api('mixlab/analyze', { url })));
67
+
68
+ tool('check_target', {
69
+ title: 'Check audio against a loudness target (Spotify, EBU, etc.)',
70
+ description: 'Verify whether audio hits a delivery target (Spotify -14 LUFS / EBU broadcast -23 / podcast -16 / etc.). Returns pass/fail with per-metric deltas and a concrete ffmpeg loudnorm command to fix if failing. Presets: spotify, apple-music, youtube, tidal, amazon-music, podcast, ebu-broadcast, atsc-broadcast; or target="custom" with lufs+tp.',
71
+ inputSchema: { ...URL_IN, target: z.string().describe('Preset id or "custom".'), lufs: z.number().optional(), tp: z.number().optional() },
72
+ }, wrap(({ url, target, lufs, tp }) => api('mixlab/check-target', { url, target, lufs, tp })));
73
+
74
+ tool('analyze_timeseries', {
75
+ title: 'Loudness over time (for graphing / visualization)',
76
+ description: 'Short-term LUFS samples over time (EBU R128, ~3s window) with their time-base, plus downsampled waveform peaks. Arrays suitable for loudness-over-time charts, level meters, and waveform UIs.',
77
+ inputSchema: { ...URL_IN, waveformPoints: z.number().int().positive().optional().describe('Waveform peak count (default 200).') },
78
+ }, wrap(({ url, waveformPoints }) => api('mixlab/timeseries', { url, waveformPoints })));
79
+
80
+ tool('get_spectrum', {
81
+ title: 'FFT spectrum data',
82
+ description: 'Frequency-domain magnitude data (paired frequency/magnitude arrays) plus energies in 7 standard bands (sub/bass/lowMid/mid/highMid/presence/air) and the dominant band. For spectrum-analyzer UIs and tonal-balance analysis.',
83
+ inputSchema: URL_IN,
84
+ }, wrap(({ url }) => api('mixlab/spectrum', { url })));
85
+
86
+ tool('analyze_voice', {
87
+ title: 'Analyze voice quality (VoiceLab)',
88
+ description: 'Speech-quality QA for a voice recording: speech/silence ratio, speaking-rate label, signal-to-noise, noise floor, room-echo label, sibilance risk, clipping severity. Gates a voice take.',
89
+ inputSchema: URL_IN,
90
+ }, wrap(({ url }) => api('voicelab/qa', { url })));
91
+
92
+ tool('get_speech_segments', {
93
+ title: 'Speech segments (VoiceLab)',
94
+ description: 'List voiced speech regions with start/end timestamps and per-segment RMS. For auto-trim, chapter generation, and speaker-turn detection. RMS-based voice-activity detection, not speaker diarization.',
95
+ inputSchema: URL_IN,
96
+ }, wrap(({ url }) => api('voicelab/segments', { url })));
97
+
98
+ tool('index_signal', {
99
+ title: 'Index a signal (SignalLab)',
100
+ description: 'A metadata index for any audio file: content-type guess (voice/music/mixed/noise/silence) with confidence, brightness & dynamics buckets, dominant band, clipping/silence regions, and tag suggestions. For triage or auto-tagging a library.',
101
+ inputSchema: URL_IN,
102
+ }, wrap(({ url }) => api('signallab/index', { url })));
103
+
104
+ tool('compare_loudness', {
105
+ title: 'Compare two tracks (A/B)',
106
+ description: 'Run loudness analysis on TWO URLs and return both results for an A/B comparison: master vs reference, before vs after a fix, two encoders, two cuts.',
107
+ inputSchema: {
108
+ urlA: z.string().url().describe('First source (e.g. master).'),
109
+ urlB: z.string().url().describe('Second source (e.g. reference).'),
110
+ },
111
+ }, wrap(async ({ urlA, urlB }) => {
112
+ const [a, b] = await Promise.all([api('mixlab/analyze', { url: urlA }), api('mixlab/analyze', { url: urlB })]);
113
+ return { a, b };
114
+ }));
115
+
116
+ return { server, toolNames };
117
+ }
118
+
119
+ // Run as a stdio server ONLY when executed directly. Importing this module (api/mcp.mjs)
120
+ // is side-effect-free so it can reuse buildServer() without opening a stdio transport.
121
+ const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
122
+
123
+ if (isMain && process.argv.includes('--selftest')) {
124
+ const assert = (await import('node:assert/strict')).default;
125
+ const { toolNames } = buildServer();
126
+ assert.deepEqual(
127
+ toolNames.slice().sort(),
128
+ ['analyze_loudness', 'analyze_timeseries', 'analyze_voice', 'check_target', 'compare_loudness', 'get_spectrum', 'get_speech_segments', 'index_signal'],
129
+ 'eight hosted tools registered',
130
+ );
131
+ const prev = process.env.AUDIOLAB_API_KEY;
132
+ delete process.env.AUDIOLAB_API_KEY;
133
+ await assert.rejects(() => call('mixlab/analyze', { url: 'https://example.com/a.wav' }), /AUDIOLAB_API_KEY/, 'refuses without a key (before any fetch)');
134
+ if (prev) process.env.AUDIOLAB_API_KEY = prev;
135
+ console.log('selftest ok · 8 hosted tools (API-backed, no engine) + missing-key guard');
136
+ process.exit(0);
137
+ }
138
+
139
+ if (isMain) {
140
+ const { server } = buildServer();
141
+ await server.connect(new StdioServerTransport());
142
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@audiolabtools/mcp-server",
3
+ "version": "0.1.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
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/Audio-Launch/audiolab-tools.git",
28
+ "directory": "mcp"
29
+ },
30
+ "license": "MIT",
31
+ "author": "Nathan Renting",
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.29.0",
40
+ "zod": "^4.4.3"
41
+ }
42
+ }