@labelgrid/mcp 0.6.0 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ 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.7.0] - 2026-08-20
9
+
10
+ ### Added
11
+
12
+ - `delete_catalog_item` accepts `replace_with` for writers and publishers. The
13
+ replacement receives every credit before the original is deleted. Other
14
+ entity types reject the parameter without sending a delete request.
15
+ - `query_financials` concise responses include `credit_amount`, preserving
16
+ positive, zero and explicit null values from the API.
17
+
18
+ ### Changed
19
+
20
+ - Writer and publisher deletion guidance now names all references that can
21
+ block deletion and documents the `REPLACEMENT_UNAVAILABLE` response.
22
+
23
+ ## [0.6.1] - 2026-08-05
24
+
25
+ ### Changed
26
+
27
+ - `get_analytics` states how the summary aggregates: every section aggregates
28
+ over the resolved filter scope, so a `upc` filter returns release totals
29
+ rather than a row per track. Per-track output comes from the `track-*-daily`
30
+ sections (which need a `release_id`, `isrc` or `upc` scope) or from
31
+ `get_analytics_rankings`.
32
+ - `get_analytics_rankings` is described as the per-entity breakdown of a scope
33
+ as well as a leaderboard — `type: 'tracks'` with a `upc` or `release_id`
34
+ filter ranks the tracks on that release.
35
+ - Descriptions on the `metrics`, `view` and `type` parameters were removed where
36
+ they restated the parameter's own enum or its tool description. No parameter,
37
+ enum value or validation rule changed.
38
+
8
39
  ## [0.6.0] - 2026-08-05
9
40
 
10
41
  ### Added
package/README.md CHANGED
@@ -129,7 +129,7 @@ tool definitions by `npm run gen-docs` — do not edit it by hand._
129
129
  | `get_catalog_item` | read | Retrieve one catalog entity by id, with full detail (e.g. a release’s metadata and track listing, a track’s contributors and royalty splits, a writer’s PRO/IPI). |
130
130
  | `create_catalog_item` | write | Create a catalog entity: pass its attributes in `fields` — the API owns all validation. Required and common fields per entity: label — required: name, default_email; optional: support email, website/platform URLs, default copyright lines, isrc_base. artist — required: artist_name; optional: full_name, email, location, bios, isni, default_language, platform profile URLs. writer — required: first_name, last_name; optional: middle_name, display_credits, email, country, pro, ipi, isni, publisher_id (or publisher_name/publisher_pro/publisher_ipi). publisher — required: name; optional: ipi, pro, isni, controlled_publisher. release — required on create: content_type, label_id, artists, titles, cat (catalog number), artwork_ai_usage, primary_genre_id; many optional fields (dates, copyright lines, genres, per-outlet URLs). track — required on create: release_id, disc, track_num, composition_type, artists, audio_ai_usage, composition_ai_usage, commercial_samples, audio_language, contributors, and recording_country (ISO 3166-1 alpha-2, e.g. "US"); optional: titles, isrc, iswc, writers, publishers, splits, and more. A release is created in DRAFT state — add tracks, then run the release checks before distributing. |
131
131
  | `update_catalog_item` | write | Update a catalog entity: supply only the fields to change in `fields` (same field sets as create_catalog_item). Once a release is submitted or distributed, some release and track fields are locked — changing one returns a 403 with code RELEASE_LOCKED_FIELDS naming exactly which fields cannot change. |
132
- | `delete_catalog_item` | write | Delete a catalog entity. The API refuses deletes that would orphan data — label: refused while the label still has releases — remove or reassign its releases first. artist: refused while still referenced by releases or tracks. writer: refused while still referenced by tracks. publisher: refused while still referenced by writers. release: only a never-submitted draft can be deleted. track: refused once the release is no longer an editable draft. |
132
+ | `delete_catalog_item` | write | Delete a catalog entity. The API refuses deletes that would orphan data — label: refused while the label still has releases — remove or reassign its releases first. artist: refused while still referenced by releases or tracks. writer: refused while still referenced by tracks or artists, unless replace_with reassigns those credits. publisher: refused while still referenced by tracks or label default publishers, unless replace_with reassigns those credits. release: only a never-submitted draft can be deleted. track: refused once the release is no longer an editable draft. `replace_with` — writer and publisher only — is the id inheriting those credits; a 422 REPLACEMENT_UNAVAILABLE means nothing was deleted. |
133
133
  | `upload_image` | write | Upload a label image (logo, dark-mode logo, or background) or an artist photo from a local image file, per `target`. |
134
134
  | `get_asset` | read | Read a track or release asset. Valid combinations: (1) mode='info' + parent='track' + asset stereo\|dolby\|lyrics — file metadata (not the bytes) incl. processing state. (2) mode='info' + parent='release' + asset square\|tall — motion-artwork (animated cover) video metadata. (3) mode='download_url' + parent='track' + asset audio_16\|audio_24\|audio_32 (WAV master) or audio_preview_full\|audio_preview_clip (MP3 preview) — returns { download_url, expires_in }, a signed URL that expires roughly 10 minutes after issue; fetch it directly — do not send your API token to it. Any other combination is refused. |
135
135
 
@@ -149,9 +149,9 @@ tool definitions by `npm run gen-docs` — do not edit it by hand._
149
149
 
150
150
  | Tool | Gate | Description |
151
151
  | --- | --- | --- |
152
- | `get_analytics` | read | Streaming analytics summary. Window capped at 400 days; `metrics` takes 1-12 section keys per request (split larger selections — responses are cached). KUGOU/KUWO/QQMUSIC report weekly: one point per week carrying the whole week — never average it per day. `meta` carries `platform_cadence`, `section_granularity`, `sections_as_of` and `sections_complete_through` (later dates still filling in). Call get_analytics_availability first for section-per-platform support. The `social-*` / `soundcloud-engagement` sections cover social and UGC usage instead of streaming: their `platform` is a UGC platform; a use, view and play are distinct quantities, never summed with each other or with streams; `ugc_platform` narrows them. Selecting any adds `meta.social_availability` (which UGC platforms report each signal) — the streaming matrix excludes them. The `track-*-daily` sections need a `release_id`, `isrc` or `upc` scope. `track-listeners-daily` sums per-entry daily counts: not distinct people, not summable across dates. Rate-limited ~60/min; windows over 90 days draw a separate lower ~30/min budget — prefer shorter windows for polling. A 429 carries retry_after_seconds. |
152
+ | `get_analytics` | read | Streaming analytics summary. Window capped at 400 days; `metrics` takes 1-12 section keys per request (split larger selections; responses cached). KUGOU/KUWO/QQMUSIC report weekly: one point carries the whole week — never average per day. `meta` carries `platform_cadence`, `section_granularity`, `sections_as_of` and `sections_complete_through` (later dates still filling in). See get_analytics_availability for per-platform support. Sections aggregate over the resolved filter scope: a `upc` filter gives release totals, not per-track rows. For those use `track-*-daily` (needs a `release_id`, `isrc` or `upc` scope) or get_analytics_rankings. `track-listeners-daily` sums per-entry daily counts: not distinct people, not summable across dates. `social-*` / `soundcloud-engagement` cover social/UGC usage, not streaming: `platform` is a UGC platform, `ugc_platform` narrows them; use, view and play are distinct quantities never summed together or with streams. Selecting any adds `meta.social_availability` (which UGC platforms report each signal); the streaming matrix omits them. Rate-limited ~60/min; windows over 90 days draw a separate ~30/min budget — prefer short windows for polling. A 429 carries retry_after_seconds. |
153
153
  | `get_analytics_availability` | read | Static `availability` matrix (per section, per platform) plus `platform_cadence` (daily\|weekly per platform). Account- and date-independent: fetch once, reuse. Read it before get_analytics so an unreported section is treated as unavailable, not an empty chart. |
154
- | `get_analytics_rankings` | read | Top-N rankings for a window, ordered by summed streams. Pick ONE `view`: `leaderboards` — your top artists, tracks or albums (`type` required; `all` returns all three in one request). `placements` — the playlists and radio containers driving streams, summed across storefronts. Same scope filters as get_analytics; `limit` 1-50 (default 10). Under a `platform` filter, an `availability` of `not_available_for_platform` means that platform reports no ranking and `data` is empty. |
154
+ | `get_analytics_rankings` | read | Top-N rankings for a window, ordered by summed streams — the per-entity breakdown of a scope: `type` tracks with a `upc`/`release_id` filter ranks the tracks on that release. Pick ONE `view`: `leaderboards` — your top artists, tracks or albums (`type` required; `all` returns all three in one call). `placements` — playlists and radio containers driving streams, summed across storefronts. Same scope filters as get_analytics; `limit` 1-50 (default 10). Under a `platform` filter, `availability: not_available_for_platform` means no ranking there and `data` is empty. |
155
155
  | `query_artificial_streaming` | read | Artificial-streaming (streaming-integrity) reads. Pick ONE `view`: `flags` — Stream Radar early-warning flags, paginated (`filters`: status, severity, dsp, isrc, release_id, detected_from/detected_to). Stream Radar is an optional add-on; without it the API returns a 403, surfaced verbatim. `flag_detail` — one flag by `flag_id`. `records` — reported artificial-streaming records, cursor-paginated; the detail behind any artificial-streaming fee (`filters`: dsp, start_date/end_date, release_id, isrc). `fee_breakdown` — per-release fee breakdown for one `period` (YYYY-MM). response_format:'detailed' returns the verbatim API response. |
156
156
 
157
157
  ### Finance (statements, transactions, royalties) `finance`
package/dist/coverage.js CHANGED
@@ -145,6 +145,7 @@ export const EXCLUDED = {
145
145
  'GET /resolve/label/{labelSlug}': 'label-website resolution — not exposed in v1',
146
146
  'GET /site-settings/{label}': 'label-website settings — not exposed in v1',
147
147
  'GET /site-settings/links/{label}': 'label-website settings — not exposed in v1',
148
+ 'GET /tracks/{track}/file-upload-attempts/{uploadAttempt}': 'per-upload audio processing status — not exposed in v1',
148
149
  'GET /tracks/{track}/licenses/{trackLicense}/download': 'license file download — not exposed in v1',
149
150
  'GET /transactions/csv': 'transaction CSV export — not exposed in v1',
150
151
  };
@@ -72,6 +72,7 @@ export const CONCISE_ALLOWLISTS = {
72
72
  'status',
73
73
  'currency',
74
74
  'gross_usd',
75
+ 'credit_amount',
75
76
  'labelgrid_fee',
76
77
  'platform_fee_usd',
77
78
  'ugc_fee_usd',
@@ -11,7 +11,7 @@
11
11
  * `response_format: 'detailed'` returning the verbatim API response.
12
12
  */
13
13
  import { statSync } from 'node:fs';
14
- import { ENTITIES, ENTITY_NAMES, assertAllowedExtension, } from '@labelgrid/core';
14
+ import { ENTITIES, ENTITY_NAMES, REPLACEMENT_ENTITIES, assertAllowedExtension, } from '@labelgrid/core';
15
15
  import { z } from 'zod';
16
16
  import { applyProjection } from '../projection.js';
17
17
  /** Accepted image extensions for the catalog image uploads. */
@@ -135,17 +135,47 @@ const updateCatalogItem = {
135
135
  return client.patch(`${spec.path}/${args.id}`, args.fields);
136
136
  },
137
137
  };
138
+ /** 'writer and publisher' — read off the registry, never spelled out by hand. */
139
+ const REPLACEMENT_ENTITY_LIST = REPLACEMENT_ENTITIES.join(' and ');
138
140
  const deleteCatalogItem = {
139
141
  name: 'delete_catalog_item',
140
142
  toolset: 'catalog',
141
143
  gate: 'safe_write',
142
144
  title: 'Delete a catalog item',
143
- description: `Delete a catalog entity. The API refuses deletes that would orphan data — ${entityDoc((s) => s.deleteNote)}`,
144
- inputShape: { entity: entityArg, id: idArg },
145
+ description: `Delete a catalog entity. The API refuses deletes that would orphan data — ${entityDoc((s) => s.deleteNote)}` +
146
+ ` \`replace_with\` — ${REPLACEMENT_ENTITY_LIST} only — is the id inheriting those credits; a 422 REPLACEMENT_UNAVAILABLE means nothing was deleted.`,
147
+ inputShape: {
148
+ entity: entityArg,
149
+ id: idArg,
150
+ replace_with: z
151
+ .number()
152
+ .int()
153
+ .positive()
154
+ .optional()
155
+ .describe(`${REPLACEMENT_ENTITY_LIST} only: the id that takes over every credit held by the one being deleted.`),
156
+ },
145
157
  annotations: { destructiveHint: true },
146
158
  handler: (args, { client }) => {
147
- const spec = ENTITIES[args.entity];
148
- return client.delete(`${spec.path}/${args.id}`);
159
+ const entity = args.entity;
160
+ const spec = ENTITIES[entity];
161
+ const path = `${spec.path}/${args.id}`;
162
+ const replaceWith = args.replace_with;
163
+ if (replaceWith === undefined) {
164
+ return client.delete(path);
165
+ }
166
+ // Only two endpoints take the parameter. Dropping it for the others would
167
+ // delete on the caller's behalf while silently ignoring the reassignment
168
+ // they asked for, so the call is refused before anything is sent.
169
+ if (!spec.acceptsDeleteReplacement) {
170
+ return Promise.resolve({
171
+ error: {
172
+ code: 'INVALID_SELECTOR',
173
+ message: `The ${entity} delete does not accept replace_with — only ${REPLACEMENT_ENTITY_LIST} deletes do. Delete without it, or clear the references first.`,
174
+ status: 0,
175
+ },
176
+ });
177
+ }
178
+ return client.delete(path, { replace_with: replaceWith });
149
179
  },
150
180
  };
151
181
  /** Maps the label image targets to the API's imageType path segment. */
@@ -7,9 +7,11 @@
7
7
  import { z } from 'zod';
8
8
  import { applyProjection } from '../projection.js';
9
9
  /**
10
- * The 47 metric sections the summary endpoint can return, in the server's
11
- * canonical order: the streaming sections first, then the social and UGC
12
- * family, then the per-track daily series.
10
+ * The 47 metric sections the summary endpoint can return. The server's canonical
11
+ * order — the order it projects sections into the response — is the streaming
12
+ * sections, then the per-track daily series, then the social and UGC family.
13
+ * This list groups the social and UGC family last instead; only membership
14
+ * matters here, since the enum validates which keys are legal, not their order.
13
15
  */
14
16
  const METRICS = [
15
17
  'streams',
@@ -96,25 +98,18 @@ const getAnalytics = {
96
98
  toolset: 'insights',
97
99
  gate: 'read',
98
100
  title: 'Get streaming and social analytics',
99
- description: 'Streaming analytics summary. Window capped at 400 days; `metrics` takes 1-12 section keys per request (split larger selections — responses are cached). ' +
100
- 'KUGOU/KUWO/QQMUSIC report weekly: one point per week carrying the whole week — never average it per day. `meta` carries `platform_cadence`, `section_granularity`, `sections_as_of` and `sections_complete_through` (later dates still filling in). ' +
101
- 'Call get_analytics_availability first for section-per-platform support. ' +
102
- 'The `social-*` / `soundcloud-engagement` sections cover social and UGC usage instead of streaming: their `platform` is a UGC platform; a use, view and play are distinct quantities, never summed with each other or with streams; `ugc_platform` narrows them. Selecting any adds `meta.social_availability` (which UGC platforms report each signal) — the streaming matrix excludes them. ' +
103
- 'The `track-*-daily` sections need a `release_id`, `isrc` or `upc` scope. `track-listeners-daily` sums per-entry daily counts: not distinct people, not summable across dates. ' +
104
- 'Rate-limited ~60/min; windows over 90 days draw a separate lower ~30/min budget — prefer shorter windows for polling. A 429 carries retry_after_seconds.',
101
+ description: 'Streaming analytics summary. Window capped at 400 days; `metrics` takes 1-12 section keys per request (split larger selections; responses cached). ' +
102
+ 'KUGOU/KUWO/QQMUSIC report weekly: one point carries the whole week — never average per day. `meta` carries `platform_cadence`, `section_granularity`, `sections_as_of` and `sections_complete_through` (later dates still filling in). ' +
103
+ 'See get_analytics_availability for per-platform support. ' +
104
+ 'Sections aggregate over the resolved filter scope: a `upc` filter gives release totals, not per-track rows. For those use `track-*-daily` (needs a `release_id`, `isrc` or `upc` scope) or get_analytics_rankings. `track-listeners-daily` sums per-entry daily counts: not distinct people, not summable across dates. ' +
105
+ '`social-*` / `soundcloud-engagement` cover social/UGC usage, not streaming: `platform` is a UGC platform, `ugc_platform` narrows them; use, view and play are distinct quantities never summed together or with streams. Selecting any adds `meta.social_availability` (which UGC platforms report each signal); the streaming matrix omits them. ' +
106
+ 'Rate-limited ~60/min; windows over 90 days draw a separate ~30/min budget — prefer short windows for polling. A 429 carries retry_after_seconds.',
105
107
  inputShape: {
106
108
  start_date: z.string().describe('Window start, YYYY-MM-DD.'),
107
109
  end_date: z.string().describe('Window end, YYYY-MM-DD.'),
108
- metrics: z
109
- .array(z.enum(METRICS))
110
- .min(1)
111
- .max(MAX_METRICS_PER_REQUEST)
112
- .describe('Section keys, 1-12 per request.'),
110
+ metrics: z.array(z.enum(METRICS)).min(1).max(MAX_METRICS_PER_REQUEST),
113
111
  platform: z.enum(PLATFORMS).optional(),
114
- ugc_platform: z
115
- .enum(UGC_PLATFORMS)
116
- .optional()
117
- .describe('Narrows the social/UGC sections only.'),
112
+ ugc_platform: z.enum(UGC_PLATFORMS).optional().describe('Social/UGC sections only.'),
118
113
  release_id: z.number().int().positive().optional(),
119
114
  isrc: z.string().optional(),
120
115
  upc: z.string().optional(),
@@ -158,15 +153,15 @@ const getAnalyticsRankings = {
158
153
  toolset: 'insights',
159
154
  gate: 'read',
160
155
  title: 'Get analytics rankings',
161
- description: 'Top-N rankings for a window, ordered by summed streams. Pick ONE `view`: ' +
162
- '`leaderboards` — your top artists, tracks or albums (`type` required; `all` returns all three in one request). ' +
163
- '`placements` — the playlists and radio containers driving streams, summed across storefronts. ' +
164
- 'Same scope filters as get_analytics; `limit` 1-50 (default 10). Under a `platform` filter, an `availability` of `not_available_for_platform` means that platform reports no ranking and `data` is empty.',
156
+ description: 'Top-N rankings for a window, ordered by summed streams — the per-entity breakdown of a scope: `type` tracks with a `upc`/`release_id` filter ranks the tracks on that release. Pick ONE `view`: ' +
157
+ '`leaderboards` — your top artists, tracks or albums (`type` required; `all` returns all three in one call). ' +
158
+ '`placements` — playlists and radio containers driving streams, summed across storefronts. ' +
159
+ 'Same scope filters as get_analytics; `limit` 1-50 (default 10). Under a `platform` filter, `availability: not_available_for_platform` means no ranking there and `data` is empty.',
165
160
  inputShape: {
166
- view: z.enum(RANKING_VIEWS).describe('Which ranking read.'),
161
+ view: z.enum(RANKING_VIEWS),
167
162
  start_date: z.string().describe('Window start, YYYY-MM-DD.'),
168
163
  end_date: z.string().describe('Window end, YYYY-MM-DD.'),
169
- type: z.enum(LEADERBOARD_TYPES).optional().describe('Required for view leaderboards.'),
164
+ type: z.enum(LEADERBOARD_TYPES).optional(),
170
165
  platform: z.enum(PLATFORMS).optional(),
171
166
  ugc_platform: z.enum(UGC_PLATFORMS).optional(),
172
167
  release_id: z.number().int().positive().optional(),
@@ -178,7 +173,7 @@ const getAnalyticsRankings = {
178
173
  .int()
179
174
  .positive()
180
175
  .optional()
181
- .describe('Narrow to one of your own labels; it can never widen scope.'),
176
+ .describe('Narrow to one of your own labels; never widens scope.'),
182
177
  limit: z.number().int().positive().max(MAX_RANKING_LIMIT).optional(),
183
178
  },
184
179
  annotations: { readOnlyHint: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@labelgrid/mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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",
@@ -27,7 +27,7 @@
27
27
  "node": ">=20"
28
28
  },
29
29
  "dependencies": {
30
- "@labelgrid/core": "0.2.1",
30
+ "@labelgrid/core": "0.2.2",
31
31
  "@modelcontextprotocol/sdk": "^1.12.0",
32
32
  "zod": "^3.24.0"
33
33
  }
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.6.0",
5
+ "version": "0.7.0",
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.6.0",
15
+ "version": "0.7.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },