@labelgrid/mcp 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 (55) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +299 -0
  4. package/dist/api/content-types.d.ts +33 -0
  5. package/dist/api/content-types.js +87 -0
  6. package/dist/api/http.d.ts +62 -0
  7. package/dist/api/http.js +345 -0
  8. package/dist/api/upload.d.ts +26 -0
  9. package/dist/api/upload.js +104 -0
  10. package/dist/config.d.ts +29 -0
  11. package/dist/config.js +82 -0
  12. package/dist/coverage.d.ts +19 -0
  13. package/dist/coverage.js +150 -0
  14. package/dist/gating.d.ts +13 -0
  15. package/dist/gating.js +22 -0
  16. package/dist/index.d.ts +9 -0
  17. package/dist/index.js +86 -0
  18. package/dist/legal.d.ts +12 -0
  19. package/dist/legal.js +13 -0
  20. package/dist/log.d.ts +16 -0
  21. package/dist/log.js +35 -0
  22. package/dist/server.d.ts +15 -0
  23. package/dist/server.js +83 -0
  24. package/dist/tools/accounting.d.ts +12 -0
  25. package/dist/tools/accounting.js +386 -0
  26. package/dist/tools/analytics.d.ts +3 -0
  27. package/dist/tools/analytics.js +62 -0
  28. package/dist/tools/catalog-read.d.ts +10 -0
  29. package/dist/tools/catalog-read.js +145 -0
  30. package/dist/tools/catalog-write.d.ts +12 -0
  31. package/dist/tools/catalog-write.js +206 -0
  32. package/dist/tools/delivery.d.ts +6 -0
  33. package/dist/tools/delivery.js +40 -0
  34. package/dist/tools/files-read.d.ts +7 -0
  35. package/dist/tools/files-read.js +86 -0
  36. package/dist/tools/full-writes.d.ts +12 -0
  37. package/dist/tools/full-writes.js +248 -0
  38. package/dist/tools/identity.d.ts +3 -0
  39. package/dist/tools/identity.js +28 -0
  40. package/dist/tools/reference.d.ts +3 -0
  41. package/dist/tools/reference.js +36 -0
  42. package/dist/tools/release-write.d.ts +12 -0
  43. package/dist/tools/release-write.js +184 -0
  44. package/dist/tools/review-read.d.ts +7 -0
  45. package/dist/tools/review-read.js +78 -0
  46. package/dist/tools/setup.d.ts +11 -0
  47. package/dist/tools/setup.js +56 -0
  48. package/dist/tools/types.d.ts +41 -0
  49. package/dist/tools/types.js +52 -0
  50. package/dist/tools/webhooks.d.ts +7 -0
  51. package/dist/tools/webhooks.js +124 -0
  52. package/dist/version.d.ts +6 -0
  53. package/dist/version.js +17 -0
  54. package/package.json +34 -0
  55. package/server.json +59 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@labelgrid/mcp` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-07-15
9
+
10
+ First release.
11
+
12
+ ### Added
13
+
14
+ - 83 tools over the LabelGrid public API, in ten toolsets: identity, reference
15
+ data, catalog (labels, artists, writers, publishers, releases, tracks),
16
+ release drafting, review & quality reads, analytics, accounting & royalties,
17
+ delivery status, webhooks, and distribution.
18
+ - Three-tier safety model, fail-closed at registration and at call time:
19
+ reads always on; safe writes on by default (`LABELGRID_ENABLE_WRITES=false`
20
+ or `LABELGRID_READ_ONLY=true` to disable); full writes (distribution,
21
+ takedowns, immutable uploads) off by default, requiring
22
+ `LABELGRID_ENABLE_FULL_WRITES=true` plus an explicit acknowledgment sentence.
23
+ - Typed HTTP client with structured error normalization, byte-bounded
24
+ responses, and optional caller-supplied idempotency keys on release/track
25
+ creation and distribution.
26
+ - File-handling guardrails: per-tool extension allow-lists with symlink
27
+ resolution, no-overwrite statement downloads, presigned uploads that never
28
+ carry the API token.
29
+ - Legal and data-handling disclosures in the README, the MCP `instructions`
30
+ field, and stderr at startup.
31
+ - CI (lint, typecheck, unit tests on Node 20/22, repository hygiene scan,
32
+ API-coverage drift check), secret-gated sandbox contract suite, and
33
+ release-triggered npm trusted publishing with provenance.
34
+ - MCP registry manifest (`server.json`).
35
+ - Setup mode: starting without a token launches a single `setup` tool that walks the user through creating and configuring their API token in chat.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LabelGrid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,299 @@
1
+ # LabelGrid MCP Server
2
+
3
+ `@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
+
5
+ ## Quickstart
6
+
7
+ You need a LabelGrid API token (see [Getting a token](#getting-a-token)) and Node.js 20+. The server runs on demand via `npx` — nothing to install globally.
8
+
9
+ ### Claude Desktop
10
+
11
+ Add this to your `claude_desktop_config.json` (Settings → Developer → Edit Config):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "labelgrid": {
17
+ "command": "npx",
18
+ "args": ["-y", "@labelgrid/mcp"],
19
+ "env": {
20
+ "LABELGRID_API_TOKEN": "your-token-here"
21
+ }
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ Restart Claude Desktop. You should see the LabelGrid tools appear.
28
+
29
+ ### Claude Code
30
+
31
+ ```bash
32
+ claude mcp add labelgrid -e LABELGRID_API_TOKEN=your-token-here -- npx -y @labelgrid/mcp
33
+ ```
34
+
35
+ ### Cursor
36
+
37
+ Add this to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per project):
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "labelgrid": {
43
+ "command": "npx",
44
+ "args": ["-y", "@labelgrid/mcp"],
45
+ "env": {
46
+ "LABELGRID_API_TOKEN": "your-token-here"
47
+ }
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ ### First run / setup mode
54
+
55
+ If you start the server without `LABELGRID_API_TOKEN`, it does not fail — it launches in **setup mode** and exposes a single `setup` helper. 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 full toolset loads automatically.
56
+
57
+ ## Getting a token
58
+
59
+ API access is part of LabelGrid's [API plans](https://help.labelgrid.com/en/integrations/api-overview) — see the [API Overview and Quickstart](https://help.labelgrid.com/en/integrations/api-overview) for what the API offers and how to activate it.
60
+
61
+ 1. Sign in to your LabelGrid dashboard.
62
+ 2. Go to **Profile → API Tokens**. (If you don't see this option, your account doesn't have API access yet — the [API overview](https://help.labelgrid.com/en/integrations/api-overview) explains how to get it, or contact support.)
63
+ 3. Create a token and copy it into your client config as `LABELGRID_API_TOKEN`.
64
+
65
+ Treat the token like a password: it grants access to your catalog. Never commit it or paste it into a shared chat. Revoke a token any time from the same screen (or with the `revoke_api_token` tool).
66
+
67
+ ## Configuration
68
+
69
+ All configuration is via environment variables in your client config.
70
+
71
+ | Variable | Default | Purpose |
72
+ | --- | --- | --- |
73
+ | `LABELGRID_API_TOKEN` | — | **Required.** Your API token. |
74
+ | `LABELGRID_API_URL` | production API | Override the API base URL. |
75
+ | `LABELGRID_ENABLE_WRITES` | `true` | Safe writes (create/update drafts, labels, artists, …). Set `false` for reads only. |
76
+ | `LABELGRID_ENABLE_FULL_WRITES` | `false` | Arm full writes — see [Safety model](#safety-model). Also requires the acknowledgment below. |
77
+ | `LABELGRID_FULL_WRITES_ACK` | — | Must equal the exact acknowledgment sentence to arm full writes. |
78
+ | `LABELGRID_READ_ONLY` | `false` | Force reads only; overrides both write flags. |
79
+ | `LABELGRID_TOOLSETS` | all | Comma-separated subset of toolsets to expose. |
80
+
81
+ Valid toolsets: `identity`, `reference`, `catalog`, `releases`, `review`, `analytics`, `accounting`, `delivery`, `webhooks`, `distribution`.
82
+
83
+ ## Tool reference
84
+
85
+ <!-- TOOLS:BEGIN -->
86
+
87
+ _83 tools across 10 toolsets. This table is generated from the
88
+ tool definitions by `npm run gen-docs` — do not edit it by hand._
89
+
90
+ ### Identity `identity`
91
+
92
+ | Tool | Gate | Description |
93
+ | --- | --- | --- |
94
+ | `get_me` | read | Return the authenticated LabelGrid account profile, including the release submission limit/quota and terms-acceptance status. Use this to confirm which account your API token belongs to before making other calls. |
95
+ | `revoke_api_token` | write | Revoke a LabelGrid API token. Pass token_id to revoke a specific token; omit it to revoke the token currently in use. WARNING: revoking the current token immediately ends this session — the server loses access and stops working until you configure a new token. |
96
+
97
+ ### Reference data `reference`
98
+
99
+ | Tool | Gate | Description |
100
+ | --- | --- | --- |
101
+ | `list_reference_data` | read | Fetch a LabelGrid reference dataset used to resolve the IDs and codes that catalog and release tools expect. Pick ONE dataset with `type`: `genres` and `genre_categories` (values for primary/secondary/tertiary genre IDs), `languages` (audio and metadata language codes), `contributor_roles` (valid role names for track contributors), `instruments`, `distro_outlets` (the distribution outlets/stores available to your account), or `territories` (country/territory codes). Call this before creating or updating a release or track when you need a valid ID or code. |
102
+
103
+ ### Catalog (labels, artists, writers, publishers, releases, tracks, files) `catalog`
104
+
105
+ | Tool | Gate | Description |
106
+ | --- | --- | --- |
107
+ | `list_labels` | read | List the labels in your account, paginated. A label groups your releases and carries default copyright, website and outlet settings. Use get_label for the full detail of one label. |
108
+ | `get_label` | read | Retrieve one label by id, including its settings and defaults. |
109
+ | `list_artists` | read | List the artists in your account, paginated. Filter by `artist_name` to find a specific artist. Use get_artist for one artist’s full profile and links. |
110
+ | `get_artist` | read | Retrieve one artist by id, including bio, identifiers and platform links. |
111
+ | `list_writers` | read | List the songwriters in your account, paginated. Filter by `name` or `ipi`. Writers are attached to tracks for composition credits and royalty splits. |
112
+ | `get_writer` | read | Retrieve one writer by id, including PRO/IPI identifiers and publisher link. |
113
+ | `list_publishers` | read | List the publishers in your account, paginated. Filter by `name` or `ipi`. Publishers are linked to writers for publishing administration. |
114
+ | `get_publisher` | read | Retrieve one publisher by id. |
115
+ | `list_releases` | read | List releases in your account, paginated. Filter by `label_id`, `is_live` (1 = live/distributed), `barcode_number` (UPC/EAN), or `cat` (catalog number). Use get_release for one release’s full metadata and track listing. |
116
+ | `get_release` | read | Retrieve one release by id, including its metadata, artwork state and track listing. |
117
+ | `list_tracks` | read | List tracks in your account, paginated. Filter by `release_id` to list a release’s tracks, or by `isrc`. Use get_track for one track’s full metadata, credits and splits. |
118
+ | `get_track` | read | Retrieve one track by id, including titles, contributors, writers, publishers and royalty splits. |
119
+ | `get_track_file` | read | Retrieve metadata about one of a track’s asset files (its stereo audio, Dolby Atmos audio, or lyrics file), including its processing state. This returns file information, not the bytes — use get_track_audio_download_url for a downloadable link. |
120
+ | `get_track_audio_download_url` | read | Return a time-limited, signed URL to download one of a track’s audio assets. `asset_type` selects the asset: audio_16, audio_24, and audio_32 are the WAV master at that bit depth; audio_preview_full and audio_preview_clip are the generated MP3 preview (full-length / clip). Returns { download_url, expires_in }; the URL expires roughly 10 minutes after it is issued, so request a fresh one when it lapses. Fetch the URL directly — do not send your API token to it. |
121
+ | `list_track_licenses` | read | List the licenses attached to a track (e.g. cover/mechanical or sample clearances), paginated. |
122
+ | `get_track_license` | read | Retrieve one license attached to a track by its license id. |
123
+ | `get_release_file` | read | Retrieve metadata about a release animated cover (motion artwork) video asset — the square or the tall/portrait cover video — including its processing state. |
124
+ | `create_label` | write | Create a new label. Pass its attributes in `fields`. |
125
+ | `update_label` | write | Update a label. Supply only the fields you want to change in `fields`. |
126
+ | `delete_label` | write | Delete a label. The API refuses to delete a label that still has releases — remove or reassign its releases first. |
127
+ | `upload_label_image` | write | Upload a label image from a local file. `image_type` selects which asset: logo, logo-dark (a dark-mode variant), or background. `file_path` must be a local image file. |
128
+ | `create_artist` | write | Create a new artist. Pass its attributes in `fields`. |
129
+ | `update_artist` | write | Update an artist. Supply only the fields you want to change in `fields`. |
130
+ | `delete_artist` | write | Delete an artist. The API refuses deletion when the artist is still referenced by releases or tracks. |
131
+ | `upload_artist_photo` | write | Upload an artist photo from a local file. `file_path` must be a local image file. |
132
+ | `create_writer` | write | Create a new songwriter. Pass its attributes in `fields`. |
133
+ | `update_writer` | write | Update a writer. Supply only the fields you want to change in `fields`. |
134
+ | `delete_writer` | write | Delete a writer. The API refuses deletion when the writer is still referenced by tracks. |
135
+ | `create_publisher` | write | Create a new publisher. Pass its attributes in `fields`. |
136
+ | `update_publisher` | write | Update a publisher. Supply only the fields you want to change in `fields`. |
137
+ | `delete_publisher` | write | Delete a publisher. The API refuses deletion when the publisher is still referenced by writers. |
138
+
139
+ ### Releases & tracks (draft lifecycle) `releases`
140
+
141
+ | Tool | Gate | Description |
142
+ | --- | --- | --- |
143
+ | `create_release` | write | Create a new release in DRAFT state. Pass its metadata in `fields`. Required: content_type, label_id, artists, titles, cat (catalog number), artwork_ai_usage, primary_genre_id. Pass idempotency_key and reuse the SAME value if you retry a call whose outcome you did not observe — the server deduplicates by it for 24h; without a key each call is a new operation. Add tracks with create_track, then validate_release before distributing. |
144
+ | `update_release` | write | Update a release’s metadata. Supply only the fields you want to change in `fields`. Once a release has been submitted or distributed, some fields are locked: attempting to change a locked field returns a 403 with code RELEASE_LOCKED_FIELDS, surfaced verbatim so you can see exactly which fields cannot be changed. |
145
+ | `delete_release` | write | Delete a release. The API only allows deleting a draft that has never been submitted; it refuses to delete a release that has been submitted or distributed. |
146
+ | `create_track` | write | Create a track on a release. Pass its metadata in `fields`. Required: release_id, disc, track_num, composition_type, artists, audio_ai_usage, composition_ai_usage, commercial_samples, audio_language, contributors, and recording_country (a required ISO 3166-1 alpha-2 country code, e.g. "US"). Pass idempotency_key and reuse the SAME value if you retry a call whose outcome you did not observe — the server deduplicates by it for 24h; without a key each call is a new operation. |
147
+ | `update_track` | write | Update a track’s metadata. Supply only the fields you want to change in `fields`. As with releases, some fields lock once the parent release is submitted or distributed. |
148
+ | `delete_track` | write | Delete a track. Allowed while the parent release is an editable draft; the API refuses once the release is submitted or distributed. |
149
+ | `validate_release` | write | Run validation on a release and return any problems that would block distribution, as both a human-readable `errors` list and a machine-readable `errors_structured` list. This is a near-read check: it changes nothing and is safe to repeat. Run it before distributing. |
150
+ | `refresh_quality_report` | write | Re-run the Preflight QC automated checks and refresh the release’s quality report. Read the results with get_quality_report. The server applies an hourly refresh budget, so frequent calls may be rate-limited. Preflight QC is an optional add-on. |
151
+ | `update_landing_config` | write | Set the smart-link landing-page configuration for a release. `actions` uses the current (v2) action-list contract — each entry describes one call-to-action on the page. You can also set links_page_enabled, config_mode, page_style, custom_cta_text, custom_description, and pre_order_links. This replaces the landing configuration. |
152
+ | `create_release_short_url` | write | Create (or return the existing) short URL for a release’s smart-link landing page. Safe to repeat. |
153
+ | `add_review_issue_note` | write | Add a note to a release review issue — for example to explain a fix or add context for the reviewer. `review_issue_id` is the id of the issue (from list_review_issues). |
154
+
155
+ ### Review & quality `review`
156
+
157
+ | Tool | Gate | Description |
158
+ | --- | --- | --- |
159
+ | `list_review_issues` | read | List the review issues raised against a release during its automated quality checks. `release_id` is required. Each issue carries a code (see list_issue_definitions for what each code means), severity, and whether it blocks distribution. Use this to see what a customer must fix before a release can go out. |
160
+ | `list_issue_definitions` | read | Retrieve the catalog of review issue definitions: each code’s human-readable title, description, severity and whether it blocks distribution. Use it to interpret the codes returned by list_review_issues and the quality report. Issue codes are string slugs. |
161
+ | `get_quality_report` | read | Retrieve the Preflight QC quality report for a release: the customer-facing issues found by the automated checks so you can review them before confirming the release into distribution. Preflight QC is an optional add-on — if your account does not have it enabled the API returns a 403, which is surfaced verbatim. |
162
+ | `list_stream_radar_flags` | read | List Stream Radar flags for your releases, paginated — early-warning flags from streaming-integrity monitoring that surface possible artificial-streaming activity so you can act early. Filter by status, severity, dsp, isrc, release_id, and the last-detected date range (detected_from/detected_to). Stream Radar is an optional add-on; without it the API returns a 403, surfaced verbatim. |
163
+ | `get_stream_radar_flag` | read | Retrieve one Stream Radar flag by id, with its full detail. Stream Radar is an optional add-on; without it the API returns a 403, surfaced verbatim. |
164
+
165
+ ### Analytics `analytics`
166
+
167
+ | Tool | Gate | Description |
168
+ | --- | --- | --- |
169
+ | `get_analytics` | read | Retrieve a streaming analytics summary for your catalog in a single call. `start_date` and `end_date` (both YYYY-MM-DD) are required and the window is capped at 30 days by the server. Optionally narrow the result by `platform` (SPOTIFY, ITUNES, APPLE_MUSIC), `release_id`, `isrc`, `upc`, or `artist_names`. By default all 15 metric sections are returned; pass `metrics` to request only a subset. Available metrics: streams, listeners, saves, skips, shares, completion-rate, lyrics-view-rate, canvas-view-rate, device-split, source-split, saves-by-tier, streams-by-country, streams-by-gender, streams-by-age, shares-by-country. This endpoint is rate-limited (about 60 requests per minute); a 429 response carries retry_after_seconds. |
170
+
171
+ ### Accounting `accounting`
172
+
173
+ | Tool | Gate | Description |
174
+ | --- | --- | --- |
175
+ | `list_statements` | read | List your royalty statements, paginated. Filter by label_id, release_id, isrc, upc, and a start_date/end_date range. Pass group_by="release" to roll the totals up per release. Use get_statement for one statement, or download_statement_csv for its line items. |
176
+ | `get_statement` | read | Retrieve one royalty statement by its invoice number. |
177
+ | `download_statement_csv` | read | Download statement line items as CSV. Pass invoice_number for a single statement, OR a start_date/end_date range to export across statements. If save_to_path (an absolute path whose parent directory exists) is given, the CSV is written there — an existing file is never overwritten (returns FILE_EXISTS) — and the tool returns the byte count. Otherwise the CSV is returned inline, truncated at 100KB (with truncated: true) — use save_to_path for large exports. |
178
+ | `download_statement_invoice` | read | Download the invoice PDF for a statement. save_to_path is REQUIRED (the PDF is binary) and must be an absolute path whose parent directory exists; the PDF is written there — an existing file is never overwritten (returns FILE_EXISTS) — and the tool returns the byte count. |
179
+ | `list_transactions` | read | List account transactions, paginated. Filter by label_id, release_id, isrc, upc, and a start_date/end_date range; sort with `sort`; pass group_by="release" to roll up per release. |
180
+ | `get_royalties_breakdown` | read | Get a cursor-paginated royalty breakdown grouped by one or more dimensions. group_by is REQUIRED and is a comma-separated, ordered subset of: track, dsp, release, territory, period (e.g. "release,dsp"). Filter by label_id, release_id, isrc, upc, and a start_date/end_date range. |
181
+ | `list_artificial_streams` | read | List the artificial-streaming records reported for your catalog, cursor-paginated — the per-record detail behind any artificial-streaming fee. Filter by dsp (spotify or apple), a start_date/end_date range, release_id, or isrc. |
182
+ | `get_artificial_fee_breakdown` | read | Retrieve the per-release breakdown of an artificial-streaming fee for one billing period. `period` is the month in YYYY-MM format. |
183
+ | `get_account_summary` | read | Retrieve your accounting summary — current balance and related account-level financial totals. |
184
+
185
+ ### Delivery `delivery`
186
+
187
+ | Tool | Gate | Description |
188
+ | --- | --- | --- |
189
+ | `get_delivery_queue` | read | List the distribution queue entries for your account, paginated — one entry per (release, outlet) delivery with its current status (e.g. pending review, processing, scheduled, complete, error). Filter by `release_id`, `outlet_id`, or `status`. Use this to see where a release is in the delivery pipeline to each store. |
190
+ | `get_landing_config` | read | Retrieve the smart-link landing-page configuration for a release: whether the links page is enabled, its style/mode, custom copy, the action list and any pre-order links. Pair with update_landing_config to change it. |
191
+
192
+ ### Webhooks `webhooks`
193
+
194
+ | Tool | Gate | Description |
195
+ | --- | --- | --- |
196
+ | `list_webhooks` | read | List the webhook subscriptions configured on your account, each with its URL, subscribed events and active state. |
197
+ | `get_webhook` | read | Retrieve one webhook subscription by id. |
198
+ | `get_webhook_logs` | read | Retrieve the recent delivery log for a webhook — the attempts, response codes and outcomes — to debug why events did or did not reach your endpoint. |
199
+ | `list_webhook_event_types` | read | List every available webhook event type, each with the schema of the payload it delivers. Use it to decide which events to subscribe a webhook to. |
200
+ | `create_webhook` | write | Create a webhook subscription. `name` and `url` (the HTTPS endpoint that will receive events) are required, along with `events` selecting which event types to deliver. The API returns a signing secret once on creation — store it to verify incoming payloads. |
201
+ | `update_webhook` | write | Update a webhook subscription. Supply only the fields you want to change: `name`, `url`, `events`, or `is_active` (set false to pause deliveries). |
202
+ | `delete_webhook` | write | Delete a webhook subscription permanently. It will stop receiving events. |
203
+ | `test_webhook` | write | Send a test event to a webhook’s endpoint so you can confirm it is reachable and your signature verification works. Safe to repeat. |
204
+ | `rotate_webhook_secret` | write | Generate a new signing secret for a webhook and return it. WARNING: the old secret stops working immediately — update your endpoint’s signature verification with the new secret right away or deliveries will fail verification. |
205
+
206
+ ### Distribution (full writes) `distribution`
207
+
208
+ | Tool | Gate | Description |
209
+ | --- | --- | --- |
210
+ | `upload_track_audio` | full-write | Upload a finalized audio file (stereo WAV/FLAC or Dolby Atmos) or lyrics (LRC) file for a track. The file is uploaded directly to storage and then processed asynchronously; check its state with get_track_file. Once a release is distributed the file is immutable — upload the correct master before distributing. |
211
+ | `delete_track_audio` | full-write | Delete one of a track’s asset files (stereo, Dolby Atmos, or lyrics). Allowed only while the parent release is still an editable draft; the API refuses once the release is locked or distributed. |
212
+ | `upload_release_asset` | full-write | Upload a finalized animated cover (motion artwork) video for a release — the square or the tall/portrait cover video. The video is uploaded directly to storage and then processed; check its state with get_release_file. Upload the correct video before distributing — it is immutable once the release is live. For the static cover art image use upload_release_artwork. |
213
+ | `delete_release_asset` | full-write | Delete a release animated cover (motion artwork) video — the square or the tall/portrait cover video. Allowed only while the release is still an editable draft. |
214
+ | `upload_release_artwork` | full-write | Upload or replace the release’s static cover art image from a local file. Cover art is immutable once the release is distributed — upload the final artwork before distributing. |
215
+ | `upload_track_license` | full-write | Attach a license document to a track (for a cover or a cleared sample). `file_path` is the local license file and `type` is "cover" or "sample". Optionally record license_id, license_provider, license_provider_name, and original_track_link. Licenses are immutability-governed once the release is live. |
216
+ | `update_track_license` | full-write | Replace the file and/or metadata of an existing track license. `track_license_id` is the id of the license (from list_track_licenses); `file_path` is the license file to submit. Optionally update license_id, license_provider, license_provider_name, and original_track_link. |
217
+ | `delete_track_license` | full-write | Permanently delete a track license and its file. `track_license_id` is the license id (from list_track_licenses). This cannot be undone. |
218
+ | `distribute_release` | full-write | Submit a release for distribution to the stores/outlets — this is the FINAL, consequential action that sends the release out; validate_release should pass first. The server enforces your account’s weekly submission limit and returns a structured error if it is exceeded. Pass idempotency_key and reuse the SAME value if you retry a call whose outcome you did not observe — the server deduplicates by it for 24h; without a key each call is a new submission. |
219
+ | `takedown_release` | full-write | Take a release down from ALL outlets/stores — a final, consequential action that removes it everywhere it was delivered. Re-distribution afterward is a fresh submission. |
220
+ | `confirm_review` | full-write | Confirm a release that Preflight QC placed on hold, moving it into distribution review. Use after you have reviewed the quality report and accept the release as-is. Safe to repeat. |
221
+ | `enable_beatport` | full-write | Request Beatport onboarding for a label. This is a one-time action that cannot be un-requested once submitted, so confirm the label is correct first. |
222
+
223
+ <!-- TOOLS:END -->
224
+
225
+ ## Safety model
226
+
227
+ The server has three gates. Each is fail-closed: a tool is only registered — and only callable — when its gate is armed.
228
+
229
+ 1. **Reads** — always on. Listing and fetching your catalog, analytics, statements, and so on.
230
+ 2. **Safe writes** (`LABELGRID_ENABLE_WRITES`, on by default) — reversible, draft-stage changes: creating and editing draft releases and tracks, labels, artists, writers, publishers, webhooks, landing pages, and notes. Set `LABELGRID_ENABLE_WRITES=false` (or `LABELGRID_READ_ONLY=true`) to turn these off.
231
+ 3. **Full writes** (`LABELGRID_ENABLE_FULL_WRITES`, off by default) — consequential, hard-to-reverse actions. To arm them you must set **both**:
232
+
233
+ ```bash
234
+ LABELGRID_ENABLE_FULL_WRITES=true
235
+ LABELGRID_FULL_WRITES_ACK=I accept responsibility for AI-driven distribution actions
236
+ ```
237
+
238
+ The acknowledgment string must match exactly, or full writes stay off. When armed, the `distribution` toolset becomes available. These tools can:
239
+
240
+ - upload finalized (immutable) track audio and release artwork,
241
+ - upload, update, and delete track licenses,
242
+ - **distribute a release to stores** — a final submission subject to your account's weekly limit,
243
+ - **take a release down from all stores**,
244
+ - confirm a held release into review,
245
+ - request one-time Beatport onboarding for a label.
246
+
247
+ Leaving `LABELGRID_ENABLE_FULL_WRITES` unset is the safe default: your AI assistant can prepare and validate everything, but the irreversible submission stays a deliberate, opt-in step.
248
+
249
+ ## Rate limits & errors
250
+
251
+ Every tool returns either the API's JSON payload or a **structured error** — never a raw protocol failure — so your assistant can reason about what went wrong. The error shape is:
252
+
253
+ ```json
254
+ {
255
+ "error": {
256
+ "code": "VALIDATION_FAILED",
257
+ "message": "The submitted data was invalid.",
258
+ "status": 422,
259
+ "errors": { "title": ["The title field is required."] }
260
+ }
261
+ }
262
+ ```
263
+
264
+ Common codes: `TOKEN_INVALID` (401 — check your token), `FORBIDDEN` (403 — plan/permission or a locked field, with the server's code passed through), `NOT_FOUND` (404), `VALIDATION_FAILED` (422, with `errors`), `RATE_LIMITED` (429), `SERVER_ERROR` (5xx), `NETWORK_ERROR`, and `FILE_NOT_FOUND` / `UPLOAD_FAILED` for local file operations.
265
+
266
+ **Rate limits.** A `429` is surfaced with a `retry_after_seconds` field (from the API's `Retry-After` header). The server does **not** auto-retry — your client decides when to try again. Analytics is limited to roughly 60 requests per minute.
267
+
268
+ ## Contributing
269
+
270
+ Issues and pull requests are welcome. An API-coverage drift check runs in CI against a committed snapshot of the public API document and fails when the snapshot gains an endpoint this server does not expose (refresh the snapshot with `node scripts/fetch-openapi.mjs`). This repo uses:
271
+
272
+ - **TypeScript** (strict ESM), Node 20+, with `@modelcontextprotocol/sdk` and `zod` as the only runtime dependencies.
273
+ - **[Biome](https://biomejs.dev)** for lint + format (`npm run lint`).
274
+ - **[Vitest](https://vitest.dev)** for tests (`npm test`).
275
+
276
+ Local workflow:
277
+
278
+ ```bash
279
+ npm ci
280
+ npm run build # tsc
281
+ npm test # unit tests
282
+ npm run lint # biome
283
+ npm run leak-guard # repository hygiene scan
284
+ npm run gen-docs # regenerate the tool table above (after build)
285
+ ```
286
+
287
+ Every tool is a thin declaration — one HTTP call plus response shaping, no client-side business logic. Please keep it that way: validation and rules belong on the server. The tool table in this README is generated (`npm run gen-docs`); edit tool descriptions in `src/tools/`, not the table.
288
+
289
+ ## Legal notices
290
+
291
+ These disclosures are also surfaced at runtime: in the MCP `instructions` field your client receives on initialize, and on stderr at startup. The text below mirrors the runtime constants in `src/legal.ts`.
292
+
293
+ - **Summary.** This software is provided AS-IS, without warranty of any kind, express or implied. By using it you accept sole responsibility for your use of the LabelGrid API and for every action taken by any AI client or agent you connect to this server, including write operations against your LabelGrid account. Your use of the API through this server is governed by the LabelGrid API Terms of Service and Acceptable Use Policy. This server does not bypass server-side protections such as rate limits, plan entitlements, or terms enforcement. See [LICENSE](./LICENSE) (MIT).
294
+ - **Full writes.** When full writes are armed: distribution submissions, takedowns, and immutable file uploads initiated by an AI agent have real, potentially irreversible consequences for your releases on streaming platforms and stores. By setting the `LABELGRID_FULL_WRITES_ACK` acknowledgment variable you accepted that all such actions are your sole responsibility.
295
+ - **Data handling.** This server transmits your LabelGrid catalogue and account data to the AI client you configure. Choosing that client, and disclosing that data flow where required, is your responsibility.
296
+
297
+ ## License
298
+
299
+ [MIT](./LICENSE) © LabelGrid
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared file content-type inference and the upload extension allow-list guard.
3
+ *
4
+ * CONTENT_TYPES (moved here from upload.ts) infers a best-effort MIME type from
5
+ * a file extension — used for the presigned PUT and the multipart Blob.
6
+ * assertAllowedExtension is the per-tool guard: each file-accepting tool
7
+ * declares exactly which extensions it accepts, and the guard rejects anything
8
+ * else BEFORE the file is read or any HTTP call is made, so an upload tool can
9
+ * never be pointed at an arbitrary local file.
10
+ */
11
+ import type { ApiError } from './http.js';
12
+ /** Best-effort Content-Type inferred from a file extension. */
13
+ export declare const CONTENT_TYPES: Record<string, string>;
14
+ /** Best-effort Content-Type for a file path (default application/octet-stream). */
15
+ export declare function contentType(filePath: string): string;
16
+ /**
17
+ * Rejects a file whose extension is not in `allowed` (case-insensitive), before
18
+ * any read or HTTP call, and resolves the path to its real target. The supplied
19
+ * path's extension is checked first (the fast path); then the path is resolved
20
+ * with realpathSync and the REAL target's extension is checked too, so a symlink
21
+ * named `cover.jpg` that points at an arbitrary local file cannot slip past the
22
+ * guard. On success it returns `{ realPath }` — the resolved canonical path,
23
+ * which the caller MUST use as the path it reads/uploads (never the original
24
+ * argument), so a symlink retargeted after validation cannot redirect the read
25
+ * (the resolved target is what gets uploaded). On failure it returns `{ error }`
26
+ * — a structured FILE_TYPE_NOT_ALLOWED, or FILE_NOT_FOUND if the path does not
27
+ * resolve.
28
+ */
29
+ export declare function assertAllowedExtension(filePath: string, allowed: string[]): {
30
+ error: ApiError;
31
+ } | {
32
+ realPath: string;
33
+ };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Shared file content-type inference and the upload extension allow-list guard.
3
+ *
4
+ * CONTENT_TYPES (moved here from upload.ts) infers a best-effort MIME type from
5
+ * a file extension — used for the presigned PUT and the multipart Blob.
6
+ * assertAllowedExtension is the per-tool guard: each file-accepting tool
7
+ * declares exactly which extensions it accepts, and the guard rejects anything
8
+ * else BEFORE the file is read or any HTTP call is made, so an upload tool can
9
+ * never be pointed at an arbitrary local file.
10
+ */
11
+ import { realpathSync } from 'node:fs';
12
+ import { extname } from 'node:path';
13
+ /** Best-effort Content-Type inferred from a file extension. */
14
+ export const CONTENT_TYPES = {
15
+ '.wav': 'audio/wav',
16
+ '.flac': 'audio/flac',
17
+ '.aif': 'audio/aiff',
18
+ '.aiff': 'audio/aiff',
19
+ '.mp3': 'audio/mpeg',
20
+ '.lrc': 'text/plain',
21
+ '.txt': 'text/plain',
22
+ '.jpg': 'image/jpeg',
23
+ '.jpeg': 'image/jpeg',
24
+ '.png': 'image/png',
25
+ '.webp': 'image/webp',
26
+ '.tif': 'image/tiff',
27
+ '.tiff': 'image/tiff',
28
+ '.pdf': 'application/pdf',
29
+ '.mp4': 'video/mp4',
30
+ '.mov': 'video/quicktime',
31
+ };
32
+ /** Best-effort Content-Type for a file path (default application/octet-stream). */
33
+ export function contentType(filePath) {
34
+ return CONTENT_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
35
+ }
36
+ /**
37
+ * Rejects a file whose extension is not in `allowed` (case-insensitive), before
38
+ * any read or HTTP call, and resolves the path to its real target. The supplied
39
+ * path's extension is checked first (the fast path); then the path is resolved
40
+ * with realpathSync and the REAL target's extension is checked too, so a symlink
41
+ * named `cover.jpg` that points at an arbitrary local file cannot slip past the
42
+ * guard. On success it returns `{ realPath }` — the resolved canonical path,
43
+ * which the caller MUST use as the path it reads/uploads (never the original
44
+ * argument), so a symlink retargeted after validation cannot redirect the read
45
+ * (the resolved target is what gets uploaded). On failure it returns `{ error }`
46
+ * — a structured FILE_TYPE_NOT_ALLOWED, or FILE_NOT_FOUND if the path does not
47
+ * resolve.
48
+ */
49
+ export function assertAllowedExtension(filePath, allowed) {
50
+ const isAllowed = (candidate) => allowed.some((a) => a.toLowerCase() === candidate);
51
+ const ext = extname(filePath).toLowerCase();
52
+ // Fast path: reject a plainly-disallowed extension before touching the disk.
53
+ if (!isAllowed(ext)) {
54
+ return {
55
+ error: {
56
+ code: 'FILE_TYPE_NOT_ALLOWED',
57
+ message: `This tool only accepts ${allowed.join(', ')} files (got "${ext || 'no extension'}").`,
58
+ status: 0,
59
+ },
60
+ };
61
+ }
62
+ // The supplied name is allowed; resolve symlinks and re-check the real target.
63
+ let realPath;
64
+ try {
65
+ realPath = realpathSync(filePath);
66
+ }
67
+ catch {
68
+ return {
69
+ error: {
70
+ code: 'FILE_NOT_FOUND',
71
+ message: `No readable file at ${filePath}.`,
72
+ status: 0,
73
+ },
74
+ };
75
+ }
76
+ const realExt = extname(realPath).toLowerCase();
77
+ if (!isAllowed(realExt)) {
78
+ return {
79
+ error: {
80
+ code: 'FILE_TYPE_NOT_ALLOWED',
81
+ message: `The file resolves to a "${realExt || 'no extension'}" file; this tool only accepts ${allowed.join(', ')}.`,
82
+ status: 0,
83
+ },
84
+ };
85
+ }
86
+ return { realPath };
87
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The single typed HTTP client for the LabelGrid public API.
3
+ *
4
+ * Every tool goes through this client. It owns transport, header injection,
5
+ * query serialization, optional idempotency keys and — critically — error
6
+ * normalization: HTTP failures are turned into a structured {@link ApiError}
7
+ * and returned, never thrown. Business rules live server-side; this file is
8
+ * transport only (no retries, no queues).
9
+ */
10
+ export type ApiError = {
11
+ code: string;
12
+ message: string;
13
+ status: number;
14
+ field?: string;
15
+ suggestion?: string;
16
+ retry_after_seconds?: number;
17
+ errors?: unknown;
18
+ /** Structured validation detail passed through verbatim from the API (422). */
19
+ errors_structured?: unknown;
20
+ };
21
+ export type ApiResult<T = unknown> = {
22
+ data: T;
23
+ } | {
24
+ error: ApiError;
25
+ };
26
+ export declare class LabelGridClient {
27
+ private readonly baseUrl;
28
+ private readonly token;
29
+ private readonly fetchFn;
30
+ private readonly version;
31
+ constructor(opts: {
32
+ baseUrl: string;
33
+ token: string;
34
+ fetchFn?: typeof fetch;
35
+ version: string;
36
+ });
37
+ private authHeaders;
38
+ private send;
39
+ get<T>(path: string, query?: Record<string, unknown>): Promise<ApiResult<T>>;
40
+ post<T>(path: string, body?: unknown, opts?: {
41
+ idempotency?: boolean;
42
+ idempotencyKey?: string;
43
+ }): Promise<ApiResult<T>>;
44
+ patch<T>(path: string, body?: unknown): Promise<ApiResult<T>>;
45
+ put<T>(path: string, body?: unknown, opts?: {
46
+ idempotency?: boolean;
47
+ idempotencyKey?: string;
48
+ }): Promise<ApiResult<T>>;
49
+ delete<T>(path: string): Promise<ApiResult<T>>;
50
+ /**
51
+ * Sends a multipart/form-data POST with a single file field plus optional
52
+ * extra string fields. A missing/unreadable file yields a FILE_NOT_FOUND
53
+ * error result rather than throwing.
54
+ */
55
+ postMultipart<T>(path: string, filePath: string, fieldName: string, extra?: Record<string, string>): Promise<ApiResult<T>>;
56
+ /**
57
+ * Performs a raw request with NO Authorization header — used for presigned
58
+ * upload PUTs, where the signed URL is already the credential and an extra
59
+ * Bearer token would break the signature.
60
+ */
61
+ raw(url: string, init: RequestInit): Promise<Response>;
62
+ }