@labelgrid/mcp 0.2.0 → 0.2.2
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/CHANGELOG.md +20 -0
- package/README.md +3 -1
- package/dist/api/http.d.ts +12 -0
- package/dist/api/http.js +70 -23
- package/dist/index.js +2 -29
- package/dist/server.js +29 -7
- package/dist/tools/all.d.ts +3 -0
- package/dist/tools/all.js +29 -0
- package/package.json +2 -2
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to `@labelgrid/mcp` are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.2] - 2026-07-16
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- API requests time out after 60 seconds (structured `TIMEOUT` error) and raw
|
|
13
|
+
transfers after 10 minutes — a hung call can no longer hang a tool.
|
|
14
|
+
- CI and publish workflows install dependencies with `--ignore-scripts`.
|
|
15
|
+
|
|
16
|
+
## [0.2.1] - 2026-07-15
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Dockerfile (the server runs containerized; boots into setup mode without credentials).
|
|
21
|
+
- `glama.json` metadata and score badges.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Setup mode now lists the full tool catalog for introspection; every catalog
|
|
26
|
+
tool returns setup guidance (`NOT_CONNECTED`) until a token is configured.
|
|
27
|
+
|
|
8
28
|
## [0.2.0] - 2026-07-15
|
|
9
29
|
|
|
10
30
|
### Added
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# LabelGrid MCP Server
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@labelgrid/mcp) [](https://github.com/labelgrid/labelgrid-mcp/actions/workflows/ci.yml) [](https://glama.ai/mcp/servers/@labelgrid/labelgrid-mcp)
|
|
4
|
+
|
|
3
5
|
`@labelgrid/mcp` — the official [Model Context Protocol](https://modelcontextprotocol.io) server for [LabelGrid](https://labelgrid.com), the music distribution platform. Point Claude Desktop, Claude Code, Cursor, or any MCP client at your own LabelGrid account and manage your music catalog, releases, files, analytics, royalty accounting, webhooks, and distribution in natural language — it is a thin, typed wrapper over the LabelGrid public API, so every rule and validation stays on the server.
|
|
4
6
|
|
|
5
7
|
## Quickstart
|
|
@@ -62,7 +64,7 @@ Add this to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per project):
|
|
|
62
64
|
|
|
63
65
|
### First run / setup mode
|
|
64
66
|
|
|
65
|
-
If you start the server without `LABELGRID_API_TOKEN`, it does not fail — it launches in **setup mode
|
|
67
|
+
If you start the server without `LABELGRID_API_TOKEN`, it does not fail — it launches in **setup mode**: the full tool catalog stays listed so you can see what the server offers, and a `setup` helper leads the way. Nothing can run in this state — every catalog tool returns setup guidance instead. Just ask your AI client to "set up LabelGrid" and it will walk you through creating a token and adding it to your config. Once the token is set, restart your client and the tools go live.
|
|
66
68
|
|
|
67
69
|
## Getting a token
|
|
68
70
|
|
package/dist/api/http.d.ts
CHANGED
|
@@ -28,14 +28,26 @@ export declare class LabelGridClient {
|
|
|
28
28
|
private readonly token;
|
|
29
29
|
private readonly fetchFn;
|
|
30
30
|
private readonly version;
|
|
31
|
+
private readonly timeoutMs;
|
|
32
|
+
private readonly rawTimeoutMs;
|
|
31
33
|
constructor(opts: {
|
|
32
34
|
baseUrl: string;
|
|
33
35
|
token: string;
|
|
34
36
|
fetchFn?: typeof fetch;
|
|
35
37
|
version: string;
|
|
38
|
+
/** API request timeout (default 60s) — a hung call must never hang a tool. */
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
/** Timeout for raw transfers like presigned uploads (default 10min). */
|
|
41
|
+
rawTimeoutMs?: number;
|
|
36
42
|
});
|
|
37
43
|
private authHeaders;
|
|
38
44
|
private send;
|
|
45
|
+
/**
|
|
46
|
+
* Reads a response body with the byte ceiling enforced mid-stream. Returns
|
|
47
|
+
* the decoded text, or the supplied too-large error result when the ceiling
|
|
48
|
+
* is crossed. Abort/timeout rejections propagate to the caller for mapping.
|
|
49
|
+
*/
|
|
50
|
+
private readBody;
|
|
39
51
|
get<T>(path: string, query?: Record<string, unknown>): Promise<ApiResult<T>>;
|
|
40
52
|
post<T>(path: string, body?: unknown, opts?: {
|
|
41
53
|
idempotency?: boolean;
|
package/dist/api/http.js
CHANGED
|
@@ -166,11 +166,15 @@ export class LabelGridClient {
|
|
|
166
166
|
token;
|
|
167
167
|
fetchFn;
|
|
168
168
|
version;
|
|
169
|
+
timeoutMs;
|
|
170
|
+
rawTimeoutMs;
|
|
169
171
|
constructor(opts) {
|
|
170
172
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
|
171
173
|
this.token = opts.token;
|
|
172
174
|
this.fetchFn = opts.fetchFn ?? fetch;
|
|
173
175
|
this.version = opts.version;
|
|
176
|
+
this.timeoutMs = opts.timeoutMs ?? 60_000;
|
|
177
|
+
this.rawTimeoutMs = opts.rawTimeoutMs ?? 600_000;
|
|
174
178
|
}
|
|
175
179
|
authHeaders() {
|
|
176
180
|
return {
|
|
@@ -187,7 +191,7 @@ export class LabelGridClient {
|
|
|
187
191
|
// across separate tool calls); otherwise a fresh UUID is generated.
|
|
188
192
|
headers['Idempotency-Key'] = opts.idempotencyKey ?? randomUUID();
|
|
189
193
|
}
|
|
190
|
-
const init = { method, headers };
|
|
194
|
+
const init = { method, headers, signal: AbortSignal.timeout(this.timeoutMs) };
|
|
191
195
|
if (opts.rawBody !== undefined) {
|
|
192
196
|
init.body = opts.rawBody;
|
|
193
197
|
}
|
|
@@ -200,6 +204,16 @@ export class LabelGridClient {
|
|
|
200
204
|
res = await this.fetchFn(url, init);
|
|
201
205
|
}
|
|
202
206
|
catch (err) {
|
|
207
|
+
if (err instanceof DOMException &&
|
|
208
|
+
(err.name === 'TimeoutError' || err.name === 'AbortError')) {
|
|
209
|
+
return {
|
|
210
|
+
error: {
|
|
211
|
+
code: 'TIMEOUT',
|
|
212
|
+
message: `The request timed out after ${Math.round(this.timeoutMs / 1000)} seconds. Try again, or narrow the request.`,
|
|
213
|
+
status: 0,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
203
217
|
return {
|
|
204
218
|
error: {
|
|
205
219
|
code: 'NETWORK_ERROR',
|
|
@@ -229,7 +243,54 @@ export class LabelGridClient {
|
|
|
229
243
|
// A chunked/streamed response carries no Content-Length, so bound it AS we
|
|
230
244
|
// read: accumulate chunks with a running byte counter and abort the moment
|
|
231
245
|
// the counter crosses the ceiling — never buffering the whole oversized body.
|
|
246
|
+
// The request timeout keeps running while the body streams, so a read can
|
|
247
|
+
// also abort here — map that to the same structured TIMEOUT.
|
|
232
248
|
let text;
|
|
249
|
+
try {
|
|
250
|
+
text = await this.readBody(res, tooLarge);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
if (err instanceof DOMException &&
|
|
254
|
+
(err.name === 'TimeoutError' || err.name === 'AbortError')) {
|
|
255
|
+
return {
|
|
256
|
+
error: {
|
|
257
|
+
code: 'TIMEOUT',
|
|
258
|
+
message: `The request timed out after ${Math.round(this.timeoutMs / 1000)} seconds while reading the response. Try again, or narrow the request.`,
|
|
259
|
+
status: 0,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
error: {
|
|
265
|
+
code: 'NETWORK_ERROR',
|
|
266
|
+
message: err instanceof Error ? err.message : 'Reading the response failed.',
|
|
267
|
+
status: 0,
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (typeof text !== 'string') {
|
|
272
|
+
return text; // the bounded reader returned the too-large error result
|
|
273
|
+
}
|
|
274
|
+
let body = null;
|
|
275
|
+
if (text.length > 0) {
|
|
276
|
+
try {
|
|
277
|
+
body = JSON.parse(text);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
body = text;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (res.ok) {
|
|
284
|
+
return { data: body };
|
|
285
|
+
}
|
|
286
|
+
return { error: normalizeError(res, body) };
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Reads a response body with the byte ceiling enforced mid-stream. Returns
|
|
290
|
+
* the decoded text, or the supplied too-large error result when the ceiling
|
|
291
|
+
* is crossed. Abort/timeout rejections propagate to the caller for mapping.
|
|
292
|
+
*/
|
|
293
|
+
async readBody(res, tooLarge) {
|
|
233
294
|
if (res.body) {
|
|
234
295
|
const reader = res.body.getReader();
|
|
235
296
|
const chunks = [];
|
|
@@ -260,29 +321,15 @@ export class LabelGridClient {
|
|
|
260
321
|
merged.set(chunk, offset);
|
|
261
322
|
offset += chunk.byteLength;
|
|
262
323
|
}
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
// No readable stream (some test stubs) — fall back to text() and measure
|
|
267
|
-
// the true byte length as a backstop (multi-byte chars exceed char count).
|
|
268
|
-
text = await res.text();
|
|
269
|
-
if (Buffer.byteLength(text, 'utf8') > MAX_RESPONSE_BYTES) {
|
|
270
|
-
return tooLarge;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
let body = null;
|
|
274
|
-
if (text.length > 0) {
|
|
275
|
-
try {
|
|
276
|
-
body = JSON.parse(text);
|
|
277
|
-
}
|
|
278
|
-
catch {
|
|
279
|
-
body = text;
|
|
280
|
-
}
|
|
324
|
+
return new TextDecoder('utf-8').decode(merged);
|
|
281
325
|
}
|
|
282
|
-
|
|
283
|
-
|
|
326
|
+
// No readable stream (some test stubs) — fall back to text() and measure
|
|
327
|
+
// the true byte length as a backstop (multi-byte chars exceed char count).
|
|
328
|
+
const text = await res.text();
|
|
329
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_RESPONSE_BYTES) {
|
|
330
|
+
return tooLarge;
|
|
284
331
|
}
|
|
285
|
-
return
|
|
332
|
+
return text;
|
|
286
333
|
}
|
|
287
334
|
get(path, query) {
|
|
288
335
|
return this.send('GET', path, { query });
|
|
@@ -340,6 +387,6 @@ export class LabelGridClient {
|
|
|
340
387
|
* Bearer token would break the signature.
|
|
341
388
|
*/
|
|
342
389
|
raw(url, init) {
|
|
343
|
-
return this.fetchFn(url, init);
|
|
390
|
+
return this.fetchFn(url, { signal: AbortSignal.timeout(this.rawTimeoutMs), ...init });
|
|
344
391
|
}
|
|
345
392
|
}
|
package/dist/index.js
CHANGED
|
@@ -13,35 +13,8 @@ import { isToolEnabled } from './gating.js';
|
|
|
13
13
|
import { FULL_WRITES_NOTICE, LEGAL_SUMMARY } from './legal.js';
|
|
14
14
|
import { log } from './log.js';
|
|
15
15
|
import { buildServer } from './server.js';
|
|
16
|
-
import {
|
|
17
|
-
import { analyticsTools } from './tools/analytics.js';
|
|
18
|
-
import { catalogReadTools } from './tools/catalog-read.js';
|
|
19
|
-
import { catalogWriteTools } from './tools/catalog-write.js';
|
|
20
|
-
import { deliveryTools } from './tools/delivery.js';
|
|
21
|
-
import { filesReadTools } from './tools/files-read.js';
|
|
22
|
-
import { fullWriteTools } from './tools/full-writes.js';
|
|
23
|
-
import { identityTools } from './tools/identity.js';
|
|
24
|
-
import { referenceTools } from './tools/reference.js';
|
|
25
|
-
import { releaseWriteTools } from './tools/release-write.js';
|
|
26
|
-
import { reviewReadTools } from './tools/review-read.js';
|
|
27
|
-
import { webhookTools } from './tools/webhooks.js';
|
|
16
|
+
import { allTools } from './tools/all.js';
|
|
28
17
|
import { VERSION } from './version.js';
|
|
29
|
-
function allTools() {
|
|
30
|
-
return [
|
|
31
|
-
...identityTools,
|
|
32
|
-
...referenceTools,
|
|
33
|
-
...analyticsTools,
|
|
34
|
-
...catalogReadTools,
|
|
35
|
-
...filesReadTools,
|
|
36
|
-
...reviewReadTools,
|
|
37
|
-
...deliveryTools,
|
|
38
|
-
...accountingTools,
|
|
39
|
-
...webhookTools,
|
|
40
|
-
...catalogWriteTools,
|
|
41
|
-
...releaseWriteTools,
|
|
42
|
-
...fullWriteTools,
|
|
43
|
-
];
|
|
44
|
-
}
|
|
45
18
|
async function main() {
|
|
46
19
|
let config;
|
|
47
20
|
try {
|
|
@@ -62,7 +35,7 @@ async function main() {
|
|
|
62
35
|
version: VERSION,
|
|
63
36
|
});
|
|
64
37
|
if (config.setupMode) {
|
|
65
|
-
const server = buildServer(config, client,
|
|
38
|
+
const server = buildServer(config, client, allTools());
|
|
66
39
|
log('info', `labelgrid-mcp v${VERSION} — setup mode (no API token configured); call the "setup" tool for guided setup`);
|
|
67
40
|
log('info', LEGAL_SUMMARY);
|
|
68
41
|
await server.connect(new StdioServerTransport());
|
package/dist/server.js
CHANGED
|
@@ -24,8 +24,10 @@ function buildInstructions(config) {
|
|
|
24
24
|
if (config.setupMode) {
|
|
25
25
|
return [
|
|
26
26
|
'No LabelGrid API token is configured, so the account is not connected yet. ' +
|
|
27
|
-
'Call the `setup` tool for step-by-step instructions to connect.
|
|
28
|
-
'
|
|
27
|
+
'Call the `setup` tool for step-by-step instructions to connect. The full ' +
|
|
28
|
+
'tool catalog is listed so you can see what the server offers, but no ' +
|
|
29
|
+
'account data can be accessed in this state — every tool returns setup ' +
|
|
30
|
+
'guidance until a token is configured.',
|
|
29
31
|
LEGAL_SUMMARY,
|
|
30
32
|
].join('\n\n');
|
|
31
33
|
}
|
|
@@ -41,11 +43,20 @@ function buildInstructions(config) {
|
|
|
41
43
|
}
|
|
42
44
|
export function buildServer(config, client, tools) {
|
|
43
45
|
const server = new McpServer({ name: 'labelgrid-mcp', version: VERSION }, { instructions: buildInstructions(config) });
|
|
44
|
-
// In setup mode
|
|
45
|
-
//
|
|
46
|
-
|
|
46
|
+
// In setup mode the setup helper leads, and the full catalog stays LISTED so
|
|
47
|
+
// introspection shows what the server offers — but every catalog tool is
|
|
48
|
+
// inert: without a token no API call is possible, so invoking one returns
|
|
49
|
+
// setup guidance instead of executing.
|
|
50
|
+
const registered = config.setupMode ? [...setupTools, ...tools] : tools;
|
|
47
51
|
for (const tool of registered) {
|
|
48
|
-
|
|
52
|
+
const isSetupHelper = tool.toolset === 'setup';
|
|
53
|
+
// Listing rule: connected mode applies the full gate matrix; setup mode
|
|
54
|
+
// lists the whole catalog (only honoring an explicit toolset narrowing),
|
|
55
|
+
// because nothing can execute without a token anyway.
|
|
56
|
+
const listable = config.setupMode
|
|
57
|
+
? config.toolsets === null || config.toolsets.has(tool.toolset)
|
|
58
|
+
: isToolEnabled(tool, config);
|
|
59
|
+
if (!isSetupHelper && !listable)
|
|
49
60
|
continue;
|
|
50
61
|
server.registerTool(tool.name, {
|
|
51
62
|
title: tool.title,
|
|
@@ -53,8 +64,19 @@ export function buildServer(config, client, tools) {
|
|
|
53
64
|
inputSchema: tool.inputShape,
|
|
54
65
|
annotations: { title: tool.title, ...tool.annotations },
|
|
55
66
|
}, async (args) => {
|
|
67
|
+
// Not connected: every catalog tool refuses with setup guidance.
|
|
68
|
+
if (config.setupMode && !isSetupHelper) {
|
|
69
|
+
return toToolResult({
|
|
70
|
+
error: {
|
|
71
|
+
code: 'NOT_CONNECTED',
|
|
72
|
+
message: 'No LabelGrid API token is configured, so this tool cannot run yet. ' +
|
|
73
|
+
'Call the `setup` tool for step-by-step instructions to connect your account.',
|
|
74
|
+
status: 0,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
56
78
|
// Defense in depth: even a registered tool re-verifies its gate.
|
|
57
|
-
if (!isToolEnabled(tool, config)) {
|
|
79
|
+
if (!isSetupHelper && !isToolEnabled(tool, config)) {
|
|
58
80
|
return toToolResult({
|
|
59
81
|
error: {
|
|
60
82
|
code: 'TOOL_DISABLED',
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** The complete tool catalog, in registration order. */
|
|
2
|
+
import { accountingTools } from './accounting.js';
|
|
3
|
+
import { analyticsTools } from './analytics.js';
|
|
4
|
+
import { catalogReadTools } from './catalog-read.js';
|
|
5
|
+
import { catalogWriteTools } from './catalog-write.js';
|
|
6
|
+
import { deliveryTools } from './delivery.js';
|
|
7
|
+
import { filesReadTools } from './files-read.js';
|
|
8
|
+
import { fullWriteTools } from './full-writes.js';
|
|
9
|
+
import { identityTools } from './identity.js';
|
|
10
|
+
import { referenceTools } from './reference.js';
|
|
11
|
+
import { releaseWriteTools } from './release-write.js';
|
|
12
|
+
import { reviewReadTools } from './review-read.js';
|
|
13
|
+
import { webhookTools } from './webhooks.js';
|
|
14
|
+
export function allTools() {
|
|
15
|
+
return [
|
|
16
|
+
...identityTools,
|
|
17
|
+
...referenceTools,
|
|
18
|
+
...analyticsTools,
|
|
19
|
+
...catalogReadTools,
|
|
20
|
+
...filesReadTools,
|
|
21
|
+
...reviewReadTools,
|
|
22
|
+
...deliveryTools,
|
|
23
|
+
...accountingTools,
|
|
24
|
+
...webhookTools,
|
|
25
|
+
...catalogWriteTools,
|
|
26
|
+
...releaseWriteTools,
|
|
27
|
+
...fullWriteTools,
|
|
28
|
+
];
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@labelgrid/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"mcpName": "io.github.labelgrid/labelgrid-mcp",
|
|
5
|
-
"description": "Official LabelGrid MCP server
|
|
5
|
+
"description": "Official LabelGrid MCP server — connect your AI client to your LabelGrid account",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"keywords": ["mcp", "model-context-protocol", "labelgrid", "music-distribution", "ai", "claude"],
|
|
8
8
|
"main": "dist/index.js",
|
package/server.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.labelgrid/labelgrid-mcp",
|
|
4
4
|
"description": "Official LabelGrid MCP server — manage your music catalog, releases, analytics and distribution.",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.2",
|
|
6
6
|
"websiteUrl": "https://labelgrid.com",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/labelgrid/labelgrid-mcp",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@labelgrid/mcp",
|
|
15
|
-
"version": "0.2.
|
|
15
|
+
"version": "0.2.2",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|