@labelgrid/mcp 0.2.1 → 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 CHANGED
@@ -5,6 +5,14 @@ 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
+
8
16
  ## [0.2.1] - 2026-07-15
9
17
 
10
18
  ### Added
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # LabelGrid MCP Server
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/%40labelgrid%2Fmcp)](https://www.npmjs.com/package/@labelgrid/mcp) [![LabelGrid MCP server](https://glama.ai/mcp/servers/@labelgrid/labelgrid-mcp/badges/score.svg)](https://glama.ai/mcp/servers/@labelgrid/labelgrid-mcp)
3
+ [![npm version](https://img.shields.io/npm/v/%40labelgrid%2Fmcp)](https://www.npmjs.com/package/@labelgrid/mcp) [![CI](https://github.com/labelgrid/labelgrid-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/labelgrid/labelgrid-mcp/actions/workflows/ci.yml) [![LabelGrid MCP server](https://glama.ai/mcp/servers/@labelgrid/labelgrid-mcp/badges/score.svg)](https://glama.ai/mcp/servers/@labelgrid/labelgrid-mcp)
4
4
 
5
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.
6
6
 
@@ -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
- text = new TextDecoder('utf-8').decode(merged);
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
- if (res.ok) {
283
- return { data: body };
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 { error: normalizeError(res, body) };
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@labelgrid/mcp",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "mcpName": "io.github.labelgrid/labelgrid-mcp",
5
5
  "description": "Official LabelGrid MCP server — connect your AI client to your LabelGrid account",
6
6
  "type": "module",
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.1",
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.1",
15
+ "version": "0.2.2",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },