@labelgrid/mcp 0.2.1 → 0.3.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 (56) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +133 -101
  3. package/dist/api/http.d.ts +12 -0
  4. package/dist/api/http.js +70 -23
  5. package/dist/config.d.ts +15 -0
  6. package/dist/config.js +37 -5
  7. package/dist/coverage.js +79 -79
  8. package/dist/entities.d.ts +25 -0
  9. package/dist/entities.js +51 -0
  10. package/dist/gating.d.ts +1 -1
  11. package/dist/gating.js +5 -1
  12. package/dist/projection.d.ts +40 -0
  13. package/dist/projection.js +145 -0
  14. package/dist/resources.d.ts +29 -0
  15. package/dist/resources.js +107 -0
  16. package/dist/server.d.ts +1 -1
  17. package/dist/server.js +23 -3
  18. package/dist/tools/account.d.ts +3 -0
  19. package/dist/tools/account.js +32 -0
  20. package/dist/tools/all.js +12 -20
  21. package/dist/tools/catalog.d.ts +14 -0
  22. package/dist/tools/catalog.js +267 -0
  23. package/dist/tools/distribution.d.ts +13 -0
  24. package/dist/tools/distribution.js +266 -0
  25. package/dist/tools/finance.d.ts +12 -0
  26. package/dist/tools/finance.js +311 -0
  27. package/dist/tools/insights.d.ts +7 -0
  28. package/dist/tools/insights.js +139 -0
  29. package/dist/tools/reference.js +9 -24
  30. package/dist/tools/releases.d.ts +11 -0
  31. package/dist/tools/releases.js +177 -0
  32. package/dist/tools/setup.js +1 -1
  33. package/dist/tools/webhooks.d.ts +3 -3
  34. package/dist/tools/webhooks.js +73 -105
  35. package/package.json +3 -2
  36. package/server.json +3 -3
  37. package/dist/tools/accounting.d.ts +0 -12
  38. package/dist/tools/accounting.js +0 -386
  39. package/dist/tools/analytics.d.ts +0 -3
  40. package/dist/tools/analytics.js +0 -62
  41. package/dist/tools/catalog-read.d.ts +0 -10
  42. package/dist/tools/catalog-read.js +0 -145
  43. package/dist/tools/catalog-write.d.ts +0 -12
  44. package/dist/tools/catalog-write.js +0 -206
  45. package/dist/tools/delivery.d.ts +0 -6
  46. package/dist/tools/delivery.js +0 -40
  47. package/dist/tools/files-read.d.ts +0 -7
  48. package/dist/tools/files-read.js +0 -86
  49. package/dist/tools/full-writes.d.ts +0 -12
  50. package/dist/tools/full-writes.js +0 -248
  51. package/dist/tools/identity.d.ts +0 -3
  52. package/dist/tools/identity.js +0 -28
  53. package/dist/tools/release-write.d.ts +0 -12
  54. package/dist/tools/release-write.js +0 -184
  55. package/dist/tools/review-read.d.ts +0 -7
  56. package/dist/tools/review-read.js +0 -78
package/CHANGELOG.md CHANGED
@@ -5,6 +5,43 @@ 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.3.0] - 2026-07-16
9
+
10
+ ### Changed
11
+
12
+ - **BREAKING: the 83 per-endpoint tools were consolidated into 30 tools** that
13
+ select their target with an argument (`entity`, `view`, `action`, `check`,
14
+ `format`, `target`, `type`). Every 0.2.x capability is preserved — see the
15
+ full old→new mapping in the README's
16
+ [Migrating from 0.2.x](./README.md#migrating-from-02x) section.
17
+ - **BREAKING: toolsets regrouped into eight sets** — `account`, `reference`,
18
+ `catalog`, `releases`, `insights`, `finance`, `webhooks`, `distribution`.
19
+ Legacy set names (`identity`, `review`, `delivery`, `analytics`,
20
+ `accounting`) are still accepted in `LABELGRID_TOOLSETS` and map silently to
21
+ their current set.
22
+ - **The `webhooks` toolset is now off by default.** Name it explicitly in
23
+ `LABELGRID_TOOLSETS` to enable it. The default connected surface is 21 tools;
24
+ the setup-mode listing applies the same default.
25
+ - Large read responses default to a concise projection: reads marked with
26
+ `response_format` keep only high-signal fields (ids always kept) unless
27
+ `response_format: 'detailed'` is passed, which returns the verbatim API
28
+ response.
29
+
30
+ ### Added
31
+
32
+ - MCP resources: the nine reference datasets are exposed at
33
+ `labelgrid://reference/{type}` alongside the `list_reference_data` tool.
34
+ - A tool-catalog token budget gate in CI (`npm run measure-tokens`) that fails
35
+ when the full catalog's estimated context cost exceeds 8,000 tokens.
36
+
37
+ ## [0.2.2] - 2026-07-16
38
+
39
+ ### Changed
40
+
41
+ - API requests time out after 60 seconds (structured `TIMEOUT` error) and raw
42
+ transfers after 10 minutes — a hung call can no longer hang a tool.
43
+ - CI and publish workflows install dependencies with `--ignore-scripts`.
44
+
8
45
  ## [0.2.1] - 2026-07-15
9
46
 
10
47
  ### Added
package/README.md CHANGED
@@ -1,8 +1,8 @@
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
- `@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.
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 — 30 consolidated tools forming a thin, typed wrapper over the LabelGrid public API, so every rule and validation stays on the server.
6
6
 
7
7
  ## Quickstart
8
8
 
@@ -88,152 +88,184 @@ All configuration is via environment variables in your client config.
88
88
  | `LABELGRID_ENABLE_FULL_WRITES` | `false` | Arm full writes — see [Safety model](#safety-model). Also requires the acknowledgment below. |
89
89
  | `LABELGRID_FULL_WRITES_ACK` | — | Must equal the exact acknowledgment sentence to arm full writes. |
90
90
  | `LABELGRID_READ_ONLY` | `false` | Force reads only; overrides both write flags. |
91
- | `LABELGRID_TOOLSETS` | all | Comma-separated subset of toolsets to expose. |
91
+ | `LABELGRID_TOOLSETS` | all except `webhooks` | Comma-separated subset of toolsets to expose. |
92
92
 
93
- Valid toolsets: `identity`, `reference`, `catalog`, `releases`, `review`, `analytics`, `accounting`, `delivery`, `webhooks`, `distribution`.
93
+ Valid toolsets (8): `account`, `reference`, `catalog`, `releases`, `insights`, `finance`, `webhooks`, `distribution`.
94
+
95
+ - **`webhooks` is opt-in**: it is excluded from the default surface. Name it explicitly in `LABELGRID_TOOLSETS` (e.g. `LABELGRID_TOOLSETS=webhooks` or `catalog,releases,webhooks`) to enable the webhook tools.
96
+ - **Legacy toolset names** from 0.2.x (`identity`, `review`, `delivery`, `analytics`, `accounting`) are still accepted in `LABELGRID_TOOLSETS` and map silently to their current toolset (`account`, `releases`, `releases`, `insights`, `finance`).
97
+
98
+ The nine reference datasets are also exposed as MCP **resources** at `labelgrid://reference/{type}`; the `list_reference_data` tool serves the same data for clients that don't surface resources.
94
99
 
95
100
  ## Tool reference
96
101
 
97
102
  <!-- TOOLS:BEGIN -->
98
103
 
99
- _83 tools across 10 toolsets. This table is generated from the
104
+ _30 tools across 8 toolsets. This table is generated from the
100
105
  tool definitions by `npm run gen-docs` — do not edit it by hand._
101
106
 
102
- ### Identity `identity`
107
+ ### Account `account`
103
108
 
104
109
  | Tool | Gate | Description |
105
110
  | --- | --- | --- |
106
- | `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. |
111
+ | `get_account` | read | Read the authenticated LabelGrid account. Pick ONE view with `view`: `profile` returns the account profile — including the release submission limit/quota and terms-acceptance status — use it to confirm which account your API token belongs to before making other calls; `balance` returns your accounting summary — current balance and related account-level financial totals. |
107
112
  | `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. |
108
113
 
109
114
  ### Reference data `reference`
110
115
 
111
116
  | Tool | Gate | Description |
112
117
  | --- | --- | --- |
113
- | `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. |
118
+ | `list_reference_data` | read | Fetch a LabelGrid reference dataset used to resolve the IDs and codes the catalog and release tools expect. Pick ONE dataset with `type`: `genres` and `genre_categories` (genre IDs), `languages` (audio/metadata language codes), `contributor_roles`, `instruments`, `distro_outlets` (the outlets/stores available to your account), `territories` (country codes), `issue_definitions` (each review issue code’s title, description, severity and whether it blocks distribution; codes are string slugs), or `webhook_event_types` (every webhook event type with its payload schema). Call this when you need a valid ID or code. The same datasets are exposed as MCP resources at labelgrid://reference/{type}; this tool is the fallback for clients that don’t surface resources. |
114
119
 
115
- ### Catalog (labels, artists, writers, publishers, releases, tracks, files) `catalog`
120
+ ### Catalog (labels, artists, writers, publishers, releases, tracks) `catalog`
116
121
 
117
122
  | Tool | Gate | Description |
118
123
  | --- | --- | --- |
119
- | `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. |
120
- | `get_label` | read | Retrieve one label by id, including its settings and defaults. |
121
- | `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. |
122
- | `get_artist` | read | Retrieve one artist by id, including bio, identifiers and platform links. |
123
- | `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. |
124
- | `get_writer` | read | Retrieve one writer by id, including PRO/IPI identifiers and publisher link. |
125
- | `list_publishers` | read | List the publishers in your account, paginated. Filter by `name` or `ipi`. Publishers are linked to writers for publishing administration. |
126
- | `get_publisher` | read | Retrieve one publisher by id. |
127
- | `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. |
128
- | `get_release` | read | Retrieve one release by id, including its metadata, artwork state and track listing. |
129
- | `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. |
130
- | `get_track` | read | Retrieve one track by id, including titles, contributors, writers, publishers and royalty splits. |
131
- | `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. |
132
- | `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. |
133
- | `list_track_licenses` | read | List the licenses attached to a track (e.g. cover/mechanical or sample clearances), paginated. |
134
- | `get_track_license` | read | Retrieve one license attached to a track by its license id. |
135
- | `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. |
136
- | `create_label` | write | Create a new label. Pass its attributes in `fields`. |
137
- | `update_label` | write | Update a label. Supply only the fields you want to change in `fields`. |
138
- | `delete_label` | write | Delete a label. The API refuses to delete a label that still has releases — remove or reassign its releases first. |
139
- | `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. |
140
- | `create_artist` | write | Create a new artist. Pass its attributes in `fields`. |
141
- | `update_artist` | write | Update an artist. Supply only the fields you want to change in `fields`. |
142
- | `delete_artist` | write | Delete an artist. The API refuses deletion when the artist is still referenced by releases or tracks. |
143
- | `upload_artist_photo` | write | Upload an artist photo from a local file. `file_path` must be a local image file. |
144
- | `create_writer` | write | Create a new songwriter. Pass its attributes in `fields`. |
145
- | `update_writer` | write | Update a writer. Supply only the fields you want to change in `fields`. |
146
- | `delete_writer` | write | Delete a writer. The API refuses deletion when the writer is still referenced by tracks. |
147
- | `create_publisher` | write | Create a new publisher. Pass its attributes in `fields`. |
148
- | `update_publisher` | write | Update a publisher. Supply only the fields you want to change in `fields`. |
149
- | `delete_publisher` | write | Delete a publisher. The API refuses deletion when the publisher is still referenced by writers. |
150
-
151
- ### Releases & tracks (draft lifecycle) `releases`
124
+ | `search_catalog` | read | List catalog entities of one kind, paginated. Pick the kind with `entity`: label, artist, writer, publisher, release, or track. `filters` takes the endpoint’s own filter names, passed through verbatim — label: no documented filters — paginate with page/per_page. artist: artist_name (filter by artist name). writer: name (writer name), ipi (IPI number). publisher: name (publisher name), ipi (IPI number). release: label_id (owning label id), is_live (1 = live/distributed only), barcode_number (UPC/EAN), cat (catalog number). track: release_id (one release’s tracks), isrc (filter by ISRC). Use get_catalog_item for one entity's full detail. response_format:'detailed' returns the verbatim API response. |
125
+ | `get_catalog_item` | read | Retrieve one catalog entity by id, with its full detail — a label’s settings, an artist’s identifiers and links, a writer’s PRO/IPI, a release’s metadata and track listing, a track’s contributors and royalty splits. Pick the kind with `entity`: label, artist, writer, publisher, release, or track. response_format:'detailed' returns the verbatim API response. |
126
+ | `create_catalog_item` | write | Create a catalog entity. Pick the kind with `entity` and 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). Once submitted or distributed some fields are locked — changing one returns a 403 with code RELEASE_LOCKED_FIELDS naming exactly which fields cannot change. 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. `idempotency_key` is honored for release and track only. |
127
+ | `update_catalog_item` | write | Update a catalog entity. Pick the kind with `entity`, supply only the fields you want to change in `fields` (same field sets as create_catalog_item). For releases: once submitted or distributed, some fields are locked — changing one returns a 403 with code RELEASE_LOCKED_FIELDS naming exactly which fields cannot change. Track fields lock the same way once the parent release is submitted or distributed. |
128
+ | `delete_catalog_item` | write | Delete a catalog entity by id. 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: allowed while the parent release is an editable draft; refused once submitted or distributed. |
129
+ | `upload_image` | write | Upload a label image or an artist photo from a local file. `target`: label_logo, label_logo_dark (a dark-mode variant), label_background, or artist_photo. `id` is the label id for label_* and the artist id for artist_photo. `file_path` must be a local image file. |
130
+ | `get_asset` | read | Read a track or release asset. Valid selector matrices: (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 — animated cover (motion artwork) video metadata incl. processing state. (3) mode='download_url', parent='track', asset audio_16\|audio_24\|audio_32 (WAV master at that bit depth) or audio_preview_full\|audio_preview_clip (generated 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 has no endpoint and returns a structured error. mode defaults to info. |
152
131
 
153
- | Tool | Gate | Description |
154
- | --- | --- | --- |
155
- | `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. |
156
- | `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. |
157
- | `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. |
158
- | `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. |
159
- | `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. |
160
- | `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. |
161
- | `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. |
162
- | `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. |
163
- | `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. |
164
- | `create_release_short_url` | write | Create (or return the existing) short URL for a release’s smart-link landing page. Safe to repeat. |
165
- | `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). |
166
-
167
- ### Review & quality `review`
132
+ ### Releases (review, delivery, links, licenses, checks) `releases`
168
133
 
169
134
  | Tool | Gate | Description |
170
135
  | --- | --- | --- |
171
- | `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. |
172
- | `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. |
173
- | `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. |
174
- | `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. |
175
- | `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. |
136
+ | `get_release_review` | read | Read a release's automated quality-check results. Pick ONE view with `view`: `issues` lists the review issues raised against the release — each carries a code (see list_reference_data type issue_definitions), severity, and whether it blocks distribution; use it to see what must be fixed before the release can go out. `quality_report` retrieves the Preflight QC quality report — the customer-facing issues found by the automated checks, to review before confirming the release into distribution; Preflight QC is an optional add-on — without it the API returns a 403, surfaced verbatim. response_format:'detailed' returns the verbatim API response. |
137
+ | `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) — where a release is in the delivery pipeline to each store. Filter by `release_id`, `outlet_id`, or `status`. response_format:'detailed' returns the verbatim API response. |
138
+ | `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 manage_release_links (action update_landing_config) to change it. |
139
+ | `list_track_licenses` | read | List the licenses attached to a track (e.g. cover/mechanical or sample clearances), paginated. Pass `license_id` to retrieve one license by its id instead. |
140
+ | `run_release_checks` | write | Run an automated check on a release. Pick ONE with `check`: `validate` returns any problems that would block distribution, as a human-readable `errors` list and a machine-readable `errors_structured` list — it changes nothing and is safe to repeat; run it before distributing. `refresh_quality_report` re-runs the Preflight QC checks and refreshes the quality report (read it with get_release_review view quality_report); the server applies an hourly refresh budget, so frequent calls may be rate-limited. Preflight QC is an optional add-on. |
141
+ | `manage_release_links` | write | Manage a release's smart-link landing page. Pick ONE action with `action`: `update_landing_config` replaces the landing-page configuration with `config` (required for this action) — `config.actions` uses the current (v2) action-list contract (one entry per call-to-action); other keys: links_page_enabled, config_mode, page_style, custom_cta_text, custom_description, pre_order_links. `create_short_url` creates (or returns the existing) short URL for the release's smart-link landing page — safe to repeat. |
142
+ | `add_review_issue_note` | write | Add a note to a release review issue — to explain a fix or add reviewer context. `review_issue_id` comes from get_release_review view issues. |
176
143
 
177
- ### Analytics `analytics`
144
+ ### Insights (analytics & artificial streaming) `insights`
178
145
 
179
146
  | Tool | Gate | Description |
180
147
  | --- | --- | --- |
181
- | `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. |
148
+ | `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` (see its enum) to request only a subset. Rate-limited (about 60 requests per minute); a 429 response carries retry_after_seconds. |
149
+ | `query_artificial_streaming` | read | Query artificial-streaming (streaming-integrity) data for your catalog. Pick ONE view with `view`: `flags` lists Stream Radar early-warning flags surfacing possible artificial-streaming activity so you can act early, paginated — `filters`: status, severity, dsp, isrc, release_id, detected_from/detected_to (YYYY-MM-DD). `flag_detail` retrieves one flag by `flag_id` (required). Stream Radar is an optional add-on; without it the API returns a 403, surfaced verbatim. `records` lists the artificial-streaming records reported for your catalog, cursor-paginated — the per-record detail behind any artificial-streaming fee; `filters`: dsp (spotify or apple), start_date/end_date, release_id, isrc. `fee_breakdown` retrieves the per-release breakdown of an artificial-streaming fee for one billing period — `period` (required) is YYYY-MM. response_format:'detailed' returns the verbatim API response. |
182
150
 
183
- ### Accounting `accounting`
151
+ ### Finance (statements, transactions, royalties) `finance`
184
152
 
185
153
  | Tool | Gate | Description |
186
154
  | --- | --- | --- |
187
- | `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. |
188
- | `get_statement` | read | Retrieve one royalty statement by its invoice number. |
189
- | `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. |
190
- | `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. |
191
- | `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. |
192
- | `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. |
193
- | `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. |
194
- | `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. |
195
- | `get_account_summary` | read | Retrieve your accounting summary — current balance and related account-level financial totals. |
196
-
197
- ### Delivery `delivery`
155
+ | `query_financials` | read | Query your financial data. Pick ONE view with `view`: `statements` lists your royalty statements, paginated — `filters`: label_id, release_id, isrc, upc, start_date/end_date; group_by="release" rolls totals up per release. `statement_detail` retrieves one statement by `invoice_number` (required). `transactions` lists account transactions, paginated — same `filters`; sort with `sort`; group_by="release" rolls up per release. `royalty_breakdown` returns a cursor-paginated royalty breakdown — `group_by` is REQUIRED for this view: a comma-separated, ordered subset of: track, dsp, release, territory, period (e.g. "release,dsp"); same `filters`; pass `cursor` to page. Use download_statement for statement line items (CSV) or the invoice PDF. response_format:'detailed' returns the verbatim API response. |
156
+ | `download_statement` | read | Download statement files. `format: 'csv'` downloads statement line items — pass invoice_number for one statement, OR a start_date/end_date range to export across statements; with save_to_path (an absolute path whose parent directory exists) the CSV is written there and the byte count returned; otherwise it is returned inline, truncated at 100KB (truncated: true) — use save_to_path for large exports. `format: 'invoice_pdf'` downloads the invoice PDF — invoice_number and save_to_path are both REQUIRED (the PDF is binary). An existing file is never overwritten (returns FILE_EXISTS). |
198
157
 
199
- | Tool | Gate | Description |
200
- | --- | --- | --- |
201
- | `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. |
202
- | `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. |
203
-
204
- ### Webhooks `webhooks`
158
+ ### Webhooks (off by default — enable via LABELGRID_TOOLSETS) `webhooks`
205
159
 
206
160
  | Tool | Gate | Description |
207
161
  | --- | --- | --- |
208
- | `list_webhooks` | read | List the webhook subscriptions configured on your account, each with its URL, subscribed events and active state. |
209
- | `get_webhook` | read | Retrieve one webhook subscription by id. |
210
- | `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. |
211
- | `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. |
212
- | `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. |
213
- | `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). |
214
- | `delete_webhook` | write | Delete a webhook subscription permanently. It will stop receiving events. |
215
- | `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. |
216
- | `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. |
162
+ | `list_webhooks` | read | Read your webhook subscriptions. `view: 'config'` (the default) lists the webhook subscriptions configured on your account — each with its URL, subscribed events and active state — or retrieves one subscription when `webhook_id` is given. `view: 'logs'` retrieves the recent delivery log for a webhook (`webhook_id` required) — attempts, response codes and outcomes — to debug why events did or did not reach your endpoint. |
163
+ | `manage_webhook` | write | Manage a webhook subscription. Pick ONE action with `action`: `create` — pass `fields` with `name`, `url` (the HTTPS endpoint receiving deliveries) and `events` (the event subscription object — see list_reference_data type webhook_event_types); the API returns a signing secret ONCE on creation — store it to verify incoming payloads. `update` — supply only the fields to change in `fields`: name, url, events, or is_active (false pauses deliveries). `delete` — permanently removes the subscription; it stops receiving events. `test` — sends a test event to confirm reachability and signature verification; safe to repeat. `rotate_secret` — generates and returns a new signing secret — WARNING: the old secret stops working immediately; update your endpoint right away or deliveries will fail verification. `webhook_id` is required for every action except create. |
217
164
 
218
165
  ### Distribution (full writes) `distribution`
219
166
 
220
167
  | Tool | Gate | Description |
221
168
  | --- | --- | --- |
222
- | `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. |
223
- | `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. |
224
- | `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. |
225
- | `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. |
226
- | `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. |
227
- | `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. |
228
- | `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. |
229
- | `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. |
230
- | `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. |
169
+ | `upload_asset` | full-write | Upload a finalized track or release asset from a local file. `id` is the track id for track_* targets, the release id for release_*. `track_stereo` (stereo audio, WAV/FLAC/AIFF), `track_dolby` (Dolby Atmos, WAV) and `track_lyrics` (LRC) upload directly to storage and process asynchronously — check state with get_asset (mode info). `release_cover_art` uploads or replaces the release's static cover art image. `release_motion_square` / `release_motion_tall` upload the animated cover (motion artwork) video — square or tall/portrait — also processed asynchronously. ALL of these become immutable once the release is distributed — upload the final files before distributing. |
170
+ | `delete_asset` | full-write | Delete a track or release asset file. track_stereo\|track_dolby\|track_lyrics delete a track asset; release_motion_square\|release_motion_tall delete an animated cover (motion artwork) video. Allowed only while the parent release is still an editable draft; the API refuses once the release is locked or distributed. Cover art has no delete endpoint and cannot be deleted here. |
171
+ | `manage_track_license` | full-write | Manage the license documents attached to a track (for a cover or a cleared sample). Pick ONE action with `action`: `upload` attaches a new license — `file_path` required, `type` ('cover' or 'sample') selects the kind; optionally record license_id, license_provider, license_provider_name, original_track_link. `update` replaces the file and/or metadata of an existing license — `track_license_id` (from list_track_licenses) and `file_path` required. `delete` permanently deletes a license and its file — `track_license_id` required; cannot be undone. Licenses are immutability-governed once the release is live. |
172
+ | `distribute_release` | full-write | Submit a release for distribution to the stores/outlets — the FINAL, consequential action that sends the release out; run_release_checks (check validate) should pass first. The server enforces your account’s weekly submission limit and returns a structured error if exceeded. Pass idempotency_key and reuse the SAME value when retrying an unobserved call; without a key each call is a new submission. |
231
173
  | `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. |
232
174
  | `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. |
233
- | `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. |
175
+ | `enable_beatport` | full-write | Request Beatport onboarding for a label. A one-time action that cannot be un-requested, so confirm the label is correct first. |
234
176
 
235
177
  <!-- TOOLS:END -->
236
178
 
179
+ ## Migrating from 0.2.x
180
+
181
+ Version 0.3.0 is a **breaking release**: the 83 per-endpoint tools were consolidated into 30 tools that select their target with an argument (`entity`, `view`, `action`, `check`, `format`, `target`, `type`). Every 0.2.x capability is preserved — the table below maps each old tool to its new call. Toolsets were regrouped into eight sets (legacy set names are still accepted as aliases in `LABELGRID_TOOLSETS`), and the `webhooks` toolset is now off by default.
182
+
183
+ | Old tool (0.2.x) | New call (0.3.0) |
184
+ | --- | --- |
185
+ | `get_me` | `get_account` (`view: 'profile'`) |
186
+ | `get_account_summary` | `get_account` (`view: 'balance'`) |
187
+ | `revoke_api_token` | `revoke_api_token` (unchanged) |
188
+ | `list_reference_data` | `list_reference_data` (now 9 `type` values) |
189
+ | `list_issue_definitions` | `list_reference_data` (`type: 'issue_definitions'`) |
190
+ | `list_webhook_event_types` | `list_reference_data` (`type: 'webhook_event_types'`) |
191
+ | `list_labels` | `search_catalog` (`entity: 'label'`) |
192
+ | `list_artists` | `search_catalog` (`entity: 'artist'`) |
193
+ | `list_writers` | `search_catalog` (`entity: 'writer'`) |
194
+ | `list_publishers` | `search_catalog` (`entity: 'publisher'`) |
195
+ | `list_releases` | `search_catalog` (`entity: 'release'`) |
196
+ | `list_tracks` | `search_catalog` (`entity: 'track'`) |
197
+ | `get_label` | `get_catalog_item` (`entity: 'label'`) |
198
+ | `get_artist` | `get_catalog_item` (`entity: 'artist'`) |
199
+ | `get_writer` | `get_catalog_item` (`entity: 'writer'`) |
200
+ | `get_publisher` | `get_catalog_item` (`entity: 'publisher'`) |
201
+ | `get_release` | `get_catalog_item` (`entity: 'release'`) |
202
+ | `get_track` | `get_catalog_item` (`entity: 'track'`) |
203
+ | `create_label` | `create_catalog_item` (`entity: 'label'`) |
204
+ | `create_artist` | `create_catalog_item` (`entity: 'artist'`) |
205
+ | `create_writer` | `create_catalog_item` (`entity: 'writer'`) |
206
+ | `create_publisher` | `create_catalog_item` (`entity: 'publisher'`) |
207
+ | `create_release` | `create_catalog_item` (`entity: 'release'`) |
208
+ | `create_track` | `create_catalog_item` (`entity: 'track'`) |
209
+ | `update_label` | `update_catalog_item` (`entity: 'label'`) |
210
+ | `update_artist` | `update_catalog_item` (`entity: 'artist'`) |
211
+ | `update_writer` | `update_catalog_item` (`entity: 'writer'`) |
212
+ | `update_publisher` | `update_catalog_item` (`entity: 'publisher'`) |
213
+ | `update_release` | `update_catalog_item` (`entity: 'release'`) |
214
+ | `update_track` | `update_catalog_item` (`entity: 'track'`) |
215
+ | `delete_label` | `delete_catalog_item` (`entity: 'label'`) |
216
+ | `delete_artist` | `delete_catalog_item` (`entity: 'artist'`) |
217
+ | `delete_writer` | `delete_catalog_item` (`entity: 'writer'`) |
218
+ | `delete_publisher` | `delete_catalog_item` (`entity: 'publisher'`) |
219
+ | `delete_release` | `delete_catalog_item` (`entity: 'release'`) |
220
+ | `delete_track` | `delete_catalog_item` (`entity: 'track'`) |
221
+ | `upload_label_image` | `upload_image` (`target: 'label_logo'` \| `'label_logo_dark'` \| `'label_background'`) |
222
+ | `upload_artist_photo` | `upload_image` (`target: 'artist_photo'`) |
223
+ | `get_track_file` | `get_asset` (`parent: 'track'`, `mode: 'info'`, `asset: 'stereo'` \| `'dolby'` \| `'lyrics'`) |
224
+ | `get_release_file` | `get_asset` (`parent: 'release'`, `mode: 'info'`, `asset: 'square'` \| `'tall'`) |
225
+ | `get_track_audio_download_url` | `get_asset` (`parent: 'track'`, `mode: 'download_url'`, `asset: 'audio_16'` \| `'audio_24'` \| `'audio_32'` \| `'audio_preview_full'` \| `'audio_preview_clip'`) |
226
+ | `list_track_licenses` | `list_track_licenses` (unchanged) |
227
+ | `get_track_license` | `list_track_licenses` (`license_id: …`) |
228
+ | `list_review_issues` | `get_release_review` (`view: 'issues'`) |
229
+ | `get_quality_report` | `get_release_review` (`view: 'quality_report'`) |
230
+ | `list_stream_radar_flags` | `query_artificial_streaming` (`view: 'flags'`) |
231
+ | `get_stream_radar_flag` | `query_artificial_streaming` (`view: 'flag_detail'`, `flag_id: …`) |
232
+ | `list_artificial_streams` | `query_artificial_streaming` (`view: 'records'`) |
233
+ | `get_artificial_fee_breakdown` | `query_artificial_streaming` (`view: 'fee_breakdown'`, `period: 'YYYY-MM'`) |
234
+ | `get_analytics` | `get_analytics` (unchanged) |
235
+ | `get_delivery_queue` | `get_delivery_queue` (unchanged) |
236
+ | `get_landing_config` | `get_landing_config` (unchanged) |
237
+ | `list_statements` | `query_financials` (`view: 'statements'`) |
238
+ | `get_statement` | `query_financials` (`view: 'statement_detail'`, `invoice_number: …`) |
239
+ | `list_transactions` | `query_financials` (`view: 'transactions'`) |
240
+ | `get_royalties_breakdown` | `query_financials` (`view: 'royalty_breakdown'`, `group_by: …`) |
241
+ | `download_statement_csv` | `download_statement` (`format: 'csv'`) |
242
+ | `download_statement_invoice` | `download_statement` (`format: 'invoice_pdf'`) |
243
+ | `list_webhooks` | `list_webhooks` (`view: 'config'`, the default) |
244
+ | `get_webhook` | `list_webhooks` (`view: 'config'`, `webhook_id: …`) |
245
+ | `get_webhook_logs` | `list_webhooks` (`view: 'logs'`, `webhook_id: …`) |
246
+ | `create_webhook` | `manage_webhook` (`action: 'create'`) |
247
+ | `update_webhook` | `manage_webhook` (`action: 'update'`) |
248
+ | `delete_webhook` | `manage_webhook` (`action: 'delete'`) |
249
+ | `test_webhook` | `manage_webhook` (`action: 'test'`) |
250
+ | `rotate_webhook_secret` | `manage_webhook` (`action: 'rotate_secret'`) |
251
+ | `validate_release` | `run_release_checks` (`check: 'validate'`) |
252
+ | `refresh_quality_report` | `run_release_checks` (`check: 'refresh_quality_report'`) |
253
+ | `update_landing_config` | `manage_release_links` (`action: 'update_landing_config'`, `config: …`) |
254
+ | `create_release_short_url` | `manage_release_links` (`action: 'create_short_url'`) |
255
+ | `add_review_issue_note` | `add_review_issue_note` (unchanged) |
256
+ | `upload_track_audio` | `upload_asset` (`target: 'track_stereo'` \| `'track_dolby'` \| `'track_lyrics'`) |
257
+ | `upload_release_artwork` | `upload_asset` (`target: 'release_cover_art'`) |
258
+ | `upload_release_asset` | `upload_asset` (`target: 'release_motion_square'` \| `'release_motion_tall'`) |
259
+ | `delete_track_audio` | `delete_asset` (`target: 'track_stereo'` \| `'track_dolby'` \| `'track_lyrics'`) |
260
+ | `delete_release_asset` | `delete_asset` (`target: 'release_motion_square'` \| `'release_motion_tall'`) |
261
+ | `upload_track_license` | `manage_track_license` (`action: 'upload'`) |
262
+ | `update_track_license` | `manage_track_license` (`action: 'update'`, `track_license_id: …`) |
263
+ | `delete_track_license` | `manage_track_license` (`action: 'delete'`, `track_license_id: …`) |
264
+ | `distribute_release` | `distribute_release` (unchanged) |
265
+ | `takedown_release` | `takedown_release` (unchanged) |
266
+ | `confirm_review` | `confirm_review` (unchanged) |
267
+ | `enable_beatport` | `enable_beatport` (unchanged) |
268
+
237
269
  ## Safety model
238
270
 
239
271
  The server has three gates. Each is fail-closed: a tool is only registered — and only callable — when its gate is armed.
@@ -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/dist/config.d.ts CHANGED
@@ -22,6 +22,21 @@ export declare const DEFAULT_BASE_URL = "https://api.labelgrid.com/api/public";
22
22
  export declare const FULL_WRITES_ACK = "I accept responsibility for AI-driven distribution actions";
23
23
  /** The valid toolset names; unknown names in LABELGRID_TOOLSETS warn and are ignored. */
24
24
  export declare const KNOWN_TOOLSETS: ReadonlySet<string>;
25
+ /**
26
+ * Pre-0.3.0 toolset names still accepted in LABELGRID_TOOLSETS, translated to
27
+ * their current toolset. Every legacy name used emits a stderr warning naming
28
+ * the toolset it maps to (see loadConfig). Names that survived the regroup
29
+ * (catalog, reference, releases, webhooks, distribution) map to themselves via
30
+ * KNOWN_TOOLSETS and need no alias entry.
31
+ */
32
+ export declare const LEGACY_TOOLSET_ALIASES: Readonly<Record<string, string>>;
33
+ /**
34
+ * Toolsets excluded from the default surface when LABELGRID_TOOLSETS is unset
35
+ * (`toolsets === null`). Naming one explicitly in LABELGRID_TOOLSETS enables
36
+ * it. Consulted by gating and by the setup-mode listing, so the advertised
37
+ * surface matches reality.
38
+ */
39
+ export declare const defaultExcludedToolsets: ReadonlySet<string>;
25
40
  /** Thrown when the environment cannot produce a usable config. */
26
41
  export declare class ConfigError extends Error {
27
42
  constructor(message: string);