@labelgrid/mcp 0.2.2 → 0.3.1

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 (62) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +132 -100
  3. package/dist/config.d.ts +15 -0
  4. package/dist/config.js +38 -6
  5. package/dist/coverage.js +79 -79
  6. package/dist/gating.d.ts +1 -1
  7. package/dist/gating.js +5 -1
  8. package/dist/index.js +1 -2
  9. package/dist/projection.d.ts +40 -0
  10. package/dist/projection.js +145 -0
  11. package/dist/resources.d.ts +29 -0
  12. package/dist/resources.js +107 -0
  13. package/dist/server.d.ts +2 -2
  14. package/dist/server.js +24 -4
  15. package/dist/tools/account.d.ts +3 -0
  16. package/dist/tools/account.js +32 -0
  17. package/dist/tools/all.js +12 -20
  18. package/dist/tools/catalog.d.ts +14 -0
  19. package/dist/tools/catalog.js +266 -0
  20. package/dist/tools/distribution.d.ts +13 -0
  21. package/dist/tools/distribution.js +265 -0
  22. package/dist/tools/finance.d.ts +12 -0
  23. package/dist/tools/finance.js +311 -0
  24. package/dist/tools/insights.d.ts +7 -0
  25. package/dist/tools/insights.js +139 -0
  26. package/dist/tools/reference.js +9 -24
  27. package/dist/tools/releases.d.ts +11 -0
  28. package/dist/tools/releases.js +177 -0
  29. package/dist/tools/setup.js +1 -1
  30. package/dist/tools/types.d.ts +1 -1
  31. package/dist/tools/webhooks.d.ts +3 -3
  32. package/dist/tools/webhooks.js +73 -105
  33. package/package.json +5 -14
  34. package/server.json +3 -3
  35. package/dist/api/content-types.d.ts +0 -33
  36. package/dist/api/content-types.js +0 -87
  37. package/dist/api/http.d.ts +0 -74
  38. package/dist/api/http.js +0 -392
  39. package/dist/api/upload.d.ts +0 -26
  40. package/dist/api/upload.js +0 -104
  41. package/dist/log.d.ts +0 -16
  42. package/dist/log.js +0 -35
  43. package/dist/tools/accounting.d.ts +0 -12
  44. package/dist/tools/accounting.js +0 -386
  45. package/dist/tools/analytics.d.ts +0 -3
  46. package/dist/tools/analytics.js +0 -62
  47. package/dist/tools/catalog-read.d.ts +0 -10
  48. package/dist/tools/catalog-read.js +0 -145
  49. package/dist/tools/catalog-write.d.ts +0 -12
  50. package/dist/tools/catalog-write.js +0 -206
  51. package/dist/tools/delivery.d.ts +0 -6
  52. package/dist/tools/delivery.js +0 -40
  53. package/dist/tools/files-read.d.ts +0 -7
  54. package/dist/tools/files-read.js +0 -86
  55. package/dist/tools/full-writes.d.ts +0 -12
  56. package/dist/tools/full-writes.js +0 -248
  57. package/dist/tools/identity.d.ts +0 -3
  58. package/dist/tools/identity.js +0 -28
  59. package/dist/tools/release-write.d.ts +0 -12
  60. package/dist/tools/release-write.js +0 -184
  61. package/dist/tools/review-read.d.ts +0 -7
  62. package/dist/tools/review-read.js +0 -78
@@ -0,0 +1,32 @@
1
+ /** Account toolset: read the authenticated account, revoke API tokens. */
2
+ import { z } from 'zod';
3
+ const getAccount = {
4
+ name: 'get_account',
5
+ toolset: 'account',
6
+ gate: 'read',
7
+ title: 'Get account',
8
+ description: 'Read the authenticated LabelGrid account. Pick ONE view with `view`: ' +
9
+ '`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; ' +
10
+ '`balance` returns your accounting summary — current balance and related account-level financial totals.',
11
+ inputShape: {
12
+ view: z.enum(['profile', 'balance']).describe('Which account read.'),
13
+ },
14
+ annotations: { readOnlyHint: true },
15
+ handler: (args, { client }) => client.get(args.view === 'balance' ? '/account' : '/me'),
16
+ };
17
+ const revokeApiToken = {
18
+ name: 'revoke_api_token',
19
+ toolset: 'account',
20
+ gate: 'safe_write',
21
+ title: 'Revoke an API token',
22
+ description: '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.',
23
+ inputShape: { token_id: z.number().int().positive().optional() },
24
+ annotations: { destructiveHint: true, idempotentHint: true },
25
+ handler: (args, { client }) => {
26
+ const tokenId = args.token_id;
27
+ return tokenId === undefined
28
+ ? client.delete('/tokens/current')
29
+ : client.delete(`/tokens/${tokenId}`);
30
+ },
31
+ };
32
+ export const accountTools = [getAccount, revokeApiToken];
package/dist/tools/all.js CHANGED
@@ -1,29 +1,21 @@
1
1
  /** The complete tool catalog, in registration order. */
2
- import { accountingTools } from './accounting.js';
3
- import { analyticsTools } from './analytics.js';
4
- import { catalogReadTools } from './catalog-read.js';
5
- import { catalogWriteTools } from './catalog-write.js';
6
- import { deliveryTools } from './delivery.js';
7
- import { filesReadTools } from './files-read.js';
8
- import { fullWriteTools } from './full-writes.js';
9
- import { identityTools } from './identity.js';
2
+ import { accountTools } from './account.js';
3
+ import { catalogTools } from './catalog.js';
4
+ import { distributionTools } from './distribution.js';
5
+ import { financeTools } from './finance.js';
6
+ import { insightsTools } from './insights.js';
10
7
  import { referenceTools } from './reference.js';
11
- import { releaseWriteTools } from './release-write.js';
12
- import { reviewReadTools } from './review-read.js';
8
+ import { releaseTools } from './releases.js';
13
9
  import { webhookTools } from './webhooks.js';
14
10
  export function allTools() {
15
11
  return [
16
- ...identityTools,
12
+ ...accountTools,
17
13
  ...referenceTools,
18
- ...analyticsTools,
19
- ...catalogReadTools,
20
- ...filesReadTools,
21
- ...reviewReadTools,
22
- ...deliveryTools,
23
- ...accountingTools,
14
+ ...catalogTools,
15
+ ...releaseTools,
16
+ ...insightsTools,
17
+ ...financeTools,
24
18
  ...webhookTools,
25
- ...catalogWriteTools,
26
- ...releaseWriteTools,
27
- ...fullWriteTools,
19
+ ...distributionTools,
28
20
  ];
29
21
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Catalog toolset: the consolidated entity CRUD (search/get/create/update/
3
+ * delete across labels, artists, writers, publishers, releases and tracks),
4
+ * image uploads, and the asset read (`get_asset`).
5
+ *
6
+ * Every tool selects its entity via the entity registry in `@labelgrid/core`.
7
+ * Create/update forward a permissive `fields` object straight to the API,
8
+ * which owns all validation — this keeps each tool a thin wrapper and lets the
9
+ * full documented field set through without re-declaring it here. The two
10
+ * reads are [proj] tools: concise-mode projection by default, with
11
+ * `response_format: 'detailed'` returning the verbatim API response.
12
+ */
13
+ import type { ToolDef } from './types.js';
14
+ export declare const catalogTools: ToolDef[];
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Catalog toolset: the consolidated entity CRUD (search/get/create/update/
3
+ * delete across labels, artists, writers, publishers, releases and tracks),
4
+ * image uploads, and the asset read (`get_asset`).
5
+ *
6
+ * Every tool selects its entity via the entity registry in `@labelgrid/core`.
7
+ * Create/update forward a permissive `fields` object straight to the API,
8
+ * which owns all validation — this keeps each tool a thin wrapper and lets the
9
+ * full documented field set through without re-declaring it here. The two
10
+ * reads are [proj] tools: concise-mode projection by default, with
11
+ * `response_format: 'detailed'` returning the verbatim API response.
12
+ */
13
+ import { statSync } from 'node:fs';
14
+ import { ENTITIES, ENTITY_NAMES, assertAllowedExtension, } from '@labelgrid/core';
15
+ import { z } from 'zod';
16
+ import { applyProjection } from '../projection.js';
17
+ /** Accepted image extensions for the catalog image uploads. */
18
+ const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.webp', '.tif', '.tiff'];
19
+ const entityArg = z.enum(ENTITY_NAMES).describe('The catalog entity kind.');
20
+ const idArg = z.number().int().positive().describe('The entity id.');
21
+ const responseFormat = z
22
+ .enum(['concise', 'detailed'])
23
+ .optional()
24
+ .describe("'concise' (default) keeps only the high-signal fields (ids always kept); 'detailed' returns the verbatim API response.");
25
+ /** A permissive body of API fields, forwarded verbatim to the endpoint. */
26
+ function fieldsBody(desc) {
27
+ return z.record(z.string(), z.unknown()).describe(desc);
28
+ }
29
+ /** Optional caller-supplied idempotency key, plumbed to the Idempotency-Key header. */
30
+ const idempotencyKey = z
31
+ .string()
32
+ .min(8)
33
+ .max(128)
34
+ .optional()
35
+ .describe('Honored for release and track only: the server deduplicates by this key for 24h — reuse the SAME key when retrying an unobserved call. Ignored for other entities.');
36
+ /** Rejects a path that is not an existing regular file, before any HTTP call. */
37
+ function fileError(p) {
38
+ let isFile = false;
39
+ try {
40
+ isFile = statSync(p).isFile();
41
+ }
42
+ catch {
43
+ isFile = false;
44
+ }
45
+ return isFile
46
+ ? null
47
+ : { code: 'FILE_NOT_FOUND', message: `No readable file at ${p}.`, status: 0 };
48
+ }
49
+ function entityDoc(pick) {
50
+ return ENTITY_NAMES.map((name) => pick(ENTITIES[name])).join(' ');
51
+ }
52
+ const searchCatalog = {
53
+ name: 'search_catalog',
54
+ toolset: 'catalog',
55
+ gate: 'read',
56
+ title: 'Search the catalog',
57
+ description: `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 — ${entityDoc((s) => s.filtersDoc)} Use get_catalog_item for one entity's full detail. response_format:'detailed' returns the verbatim API response.`,
58
+ inputShape: {
59
+ entity: entityArg,
60
+ filters: z
61
+ .record(z.string(), z.unknown())
62
+ .optional()
63
+ .describe('Filter names → values, passed through verbatim.'),
64
+ page: z.number().int().positive().optional().describe('1-based page number.'),
65
+ per_page: z.number().int().positive().optional().describe('Items per page.'),
66
+ response_format: responseFormat,
67
+ },
68
+ annotations: { readOnlyHint: true },
69
+ handler: async (args, { client }) => {
70
+ const spec = ENTITIES[args.entity];
71
+ const result = await client.get(spec.path, {
72
+ page: args.page,
73
+ per_page: args.per_page,
74
+ filter: args.filters,
75
+ });
76
+ return applyProjection(result, 'search_catalog', args.response_format);
77
+ },
78
+ };
79
+ const getCatalogItem = {
80
+ name: 'get_catalog_item',
81
+ toolset: 'catalog',
82
+ gate: 'read',
83
+ title: 'Get a catalog item',
84
+ description: '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. ' +
85
+ "Pick the kind with `entity`: label, artist, writer, publisher, release, or track. response_format:'detailed' returns the verbatim API response.",
86
+ inputShape: {
87
+ entity: entityArg,
88
+ id: idArg,
89
+ response_format: responseFormat,
90
+ },
91
+ annotations: { readOnlyHint: true },
92
+ handler: async (args, { client }) => {
93
+ const spec = ENTITIES[args.entity];
94
+ const result = await client.get(`${spec.path}/${args.id}`);
95
+ return applyProjection(result, 'get_catalog_item', args.response_format);
96
+ },
97
+ };
98
+ const createCatalogItem = {
99
+ name: 'create_catalog_item',
100
+ toolset: 'catalog',
101
+ gate: 'safe_write',
102
+ title: 'Create a catalog item',
103
+ description: `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: ${entityDoc((s) => s.fieldsDoc)} 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.`,
104
+ inputShape: {
105
+ entity: entityArg,
106
+ fields: fieldsBody('The entity attributes, forwarded verbatim.'),
107
+ idempotency_key: idempotencyKey,
108
+ },
109
+ annotations: {},
110
+ handler: (args, { client }) => {
111
+ const entity = args.entity;
112
+ const spec = ENTITIES[entity];
113
+ // Only the release and track POST endpoints support idempotency keys — the
114
+ // header is never sent for the other entities (their endpoints lack it).
115
+ if (entity === 'release' || entity === 'track') {
116
+ return client.post(spec.path, args.fields, {
117
+ idempotency: true,
118
+ idempotencyKey: args.idempotency_key,
119
+ });
120
+ }
121
+ return client.post(spec.path, args.fields);
122
+ },
123
+ };
124
+ const updateCatalogItem = {
125
+ name: 'update_catalog_item',
126
+ toolset: 'catalog',
127
+ gate: 'safe_write',
128
+ title: 'Update a catalog item',
129
+ description: '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). ' +
130
+ '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.',
131
+ inputShape: {
132
+ entity: entityArg,
133
+ id: idArg,
134
+ fields: fieldsBody('The fields to change, forwarded verbatim.'),
135
+ },
136
+ annotations: { idempotentHint: true },
137
+ handler: (args, { client }) => {
138
+ const spec = ENTITIES[args.entity];
139
+ return client.patch(`${spec.path}/${args.id}`, args.fields);
140
+ },
141
+ };
142
+ const deleteCatalogItem = {
143
+ name: 'delete_catalog_item',
144
+ toolset: 'catalog',
145
+ gate: 'safe_write',
146
+ title: 'Delete a catalog item',
147
+ description: `Delete a catalog entity by id. The API refuses deletes that would orphan data — ${entityDoc((s) => s.deleteNote)}`,
148
+ inputShape: { entity: entityArg, id: idArg },
149
+ annotations: { destructiveHint: true },
150
+ handler: (args, { client }) => {
151
+ const spec = ENTITIES[args.entity];
152
+ return client.delete(`${spec.path}/${args.id}`);
153
+ },
154
+ };
155
+ /** Maps the label image targets to the API's imageType path segment. */
156
+ const LABEL_IMAGE_TYPES = {
157
+ label_logo: 'logo',
158
+ label_logo_dark: 'logo-dark',
159
+ label_background: 'background',
160
+ };
161
+ const uploadImage = {
162
+ name: 'upload_image',
163
+ toolset: 'catalog',
164
+ gate: 'safe_write',
165
+ title: 'Upload a catalog image',
166
+ description: '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.',
167
+ inputShape: {
168
+ target: z
169
+ .enum(['label_logo', 'label_logo_dark', 'label_background', 'artist_photo'])
170
+ .describe('Which image asset to upload.'),
171
+ id: z.number().int().positive().describe('The label id (label_*) or artist id (artist_photo).'),
172
+ file_path: z.string().describe('Local path to the image file to upload.'),
173
+ },
174
+ annotations: {},
175
+ handler: async (args, { client }) => {
176
+ const err = fileError(args.file_path);
177
+ if (err)
178
+ return { error: err };
179
+ const ext = assertAllowedExtension(args.file_path, IMAGE_EXTS);
180
+ if ('error' in ext)
181
+ return { error: ext.error };
182
+ const target = args.target;
183
+ if (target === 'artist_photo') {
184
+ return client.postMultipart(`/artists/${args.id}/photo`, ext.realPath, 'file');
185
+ }
186
+ return client.postMultipart(`/labels/${args.id}/images/${LABEL_IMAGE_TYPES[target]}`, ext.realPath, 'file');
187
+ },
188
+ };
189
+ /** The (mode, parent) → allowed-assets matrix — the only combinations with an endpoint. */
190
+ const INFO_TRACK_ASSETS = new Set(['stereo', 'dolby', 'lyrics']);
191
+ const INFO_RELEASE_ASSETS = new Set(['square', 'tall']);
192
+ const DOWNLOAD_TRACK_ASSETS = new Set([
193
+ 'audio_16',
194
+ 'audio_24',
195
+ 'audio_32',
196
+ 'audio_preview_full',
197
+ 'audio_preview_clip',
198
+ ]);
199
+ const GET_ASSET_VALID_COMBINATIONS = 'Valid combinations: mode=info + parent=track + asset stereo|dolby|lyrics (file metadata and processing state); ' +
200
+ 'mode=info + parent=release + asset square|tall (animated cover / motion artwork video metadata); ' +
201
+ 'mode=download_url + parent=track + asset audio_16|audio_24|audio_32|audio_preview_full|audio_preview_clip (a signed download URL).';
202
+ const getAsset = {
203
+ name: 'get_asset',
204
+ toolset: 'catalog',
205
+ gate: 'read',
206
+ title: 'Get an asset',
207
+ description: 'Read a track or release asset. Valid selector matrices: ' +
208
+ "(1) mode='info', parent='track', asset stereo|dolby|lyrics — file metadata (not the bytes) incl. processing state. " +
209
+ "(2) mode='info', parent='release', asset square|tall — animated cover (motion artwork) video metadata incl. processing state. " +
210
+ "(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. " +
211
+ 'Any other combination has no endpoint and returns a structured error. mode defaults to info.',
212
+ inputShape: {
213
+ parent: z.enum(['track', 'release']).describe('Whose asset.'),
214
+ id: z.number().int().positive().describe('The track id or release id, per `parent`.'),
215
+ asset: z
216
+ .enum([
217
+ 'stereo',
218
+ 'dolby',
219
+ 'lyrics',
220
+ 'square',
221
+ 'tall',
222
+ 'audio_16',
223
+ 'audio_24',
224
+ 'audio_32',
225
+ 'audio_preview_full',
226
+ 'audio_preview_clip',
227
+ ])
228
+ .describe('Which asset — see the description for the valid parent/mode pairings.'),
229
+ mode: z
230
+ .enum(['info', 'download_url'])
231
+ .optional()
232
+ .describe('info (default) returns file metadata; download_url returns a signed URL.'),
233
+ },
234
+ annotations: { readOnlyHint: true },
235
+ handler: (args, { client }) => {
236
+ const mode = args.mode ?? 'info';
237
+ const parent = args.parent;
238
+ const asset = args.asset;
239
+ if (mode === 'info' && parent === 'track' && INFO_TRACK_ASSETS.has(asset)) {
240
+ return client.get(`/tracks/${args.id}/files/${asset}`);
241
+ }
242
+ if (mode === 'info' && parent === 'release' && INFO_RELEASE_ASSETS.has(asset)) {
243
+ return client.get(`/releases/${args.id}/files/${asset}`);
244
+ }
245
+ if (mode === 'download_url' && parent === 'track' && DOWNLOAD_TRACK_ASSETS.has(asset)) {
246
+ return client.get(`/tracks/${args.id}/files/${asset}/download-url`);
247
+ }
248
+ // No endpoint exists for this combination — there is nothing to send it to.
249
+ return Promise.resolve({
250
+ error: {
251
+ code: 'INVALID_SELECTOR',
252
+ message: `No endpoint exists for mode=${mode}, parent=${parent}, asset=${asset}. ${GET_ASSET_VALID_COMBINATIONS}`,
253
+ status: 0,
254
+ },
255
+ });
256
+ },
257
+ };
258
+ export const catalogTools = [
259
+ searchCatalog,
260
+ getCatalogItem,
261
+ createCatalogItem,
262
+ updateCatalogItem,
263
+ deleteCatalogItem,
264
+ uploadImage,
265
+ getAsset,
266
+ ];
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Distribution toolset (full writes): the consequential actions that put a
3
+ * release into the world or change immutable assets. Every tool here is gated
4
+ * `full_write`, so it is neither registered nor callable unless the operator
5
+ * has explicitly armed full writes (the flag AND the acknowledgment sentence).
6
+ *
7
+ * These wrap: finalized audio/artwork/motion-artwork uploads (via the
8
+ * presigned-URL flow or multipart), license file management, the FINAL
9
+ * distribute/takedown actions, the Preflight-QC confirm-review step, and
10
+ * one-time Beatport onboarding.
11
+ */
12
+ import type { ToolDef } from './types.js';
13
+ export declare const distributionTools: ToolDef[];
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Distribution toolset (full writes): the consequential actions that put a
3
+ * release into the world or change immutable assets. Every tool here is gated
4
+ * `full_write`, so it is neither registered nor callable unless the operator
5
+ * has explicitly armed full writes (the flag AND the acknowledgment sentence).
6
+ *
7
+ * These wrap: finalized audio/artwork/motion-artwork uploads (via the
8
+ * presigned-URL flow or multipart), license file management, the FINAL
9
+ * distribute/takedown actions, the Preflight-QC confirm-review step, and
10
+ * one-time Beatport onboarding.
11
+ */
12
+ import { assertAllowedExtension, uploadViaPresignedUrl } from '@labelgrid/core';
13
+ import { z } from 'zod';
14
+ /** Per-target upload extension allow-lists: an upload tool never reads an arbitrary file. */
15
+ const TRACK_UPLOAD_EXTS = {
16
+ track_stereo: ['.wav', '.flac', '.aif', '.aiff'],
17
+ track_dolby: ['.wav'],
18
+ track_lyrics: ['.lrc', '.txt'],
19
+ };
20
+ const MOTION_EXTS = ['.mp4', '.mov'];
21
+ const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.webp', '.tif', '.tiff'];
22
+ const LICENSE_EXTS = ['.pdf', '.jpg', '.jpeg', '.png'];
23
+ /** target → the API's track fileType path segment. */
24
+ const TRACK_FILE_TYPES = {
25
+ track_stereo: 'stereo',
26
+ track_dolby: 'dolby',
27
+ track_lyrics: 'lyrics',
28
+ };
29
+ /** target → the API's release motion-artwork assetType path segment. */
30
+ const MOTION_ASSET_TYPES = {
31
+ release_motion_square: 'square',
32
+ release_motion_tall: 'tall',
33
+ };
34
+ const releaseId = z.number().int().positive().describe('The release id.');
35
+ /** Optional caller-supplied idempotency key, plumbed to the Idempotency-Key header. */
36
+ const idempotencyKey = z
37
+ .string()
38
+ .min(8)
39
+ .max(128)
40
+ .optional()
41
+ .describe('The server deduplicates by this key for 24h — reuse the SAME key when retrying a call whose outcome you did not observe.');
42
+ /** Optional license metadata shared by the license upload/update actions. */
43
+ const licenseMeta = {
44
+ license_id: z.string().optional().describe('The license/clearance reference number, if any.'),
45
+ license_provider: z
46
+ .enum(['licensing_agency', 'direct_from_publisher'])
47
+ .optional()
48
+ .describe('Where the license came from.'),
49
+ license_provider_name: z.string().optional().describe('The name of the license provider.'),
50
+ original_track_link: z.string().optional().describe('URL to the original/source track.'),
51
+ };
52
+ /** Collects the defined license metadata fields into a string map for multipart. */
53
+ function licenseExtra(args) {
54
+ const out = {};
55
+ for (const key of [
56
+ 'type',
57
+ 'license_id',
58
+ 'license_provider',
59
+ 'license_provider_name',
60
+ 'original_track_link',
61
+ ]) {
62
+ const v = args[key];
63
+ if (v !== undefined && v !== null)
64
+ out[key] = String(v);
65
+ }
66
+ return out;
67
+ }
68
+ const uploadAsset = {
69
+ name: 'upload_asset',
70
+ toolset: 'distribution',
71
+ gate: 'full_write',
72
+ title: 'Upload a release/track asset',
73
+ description: 'Upload a finalized track or release asset from a local file. `id` is the track id for track_* targets, the release id for release_*. ' +
74
+ '`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). ' +
75
+ "`release_cover_art` uploads or replaces the release's static cover art image. " +
76
+ '`release_motion_square` / `release_motion_tall` upload the animated cover (motion artwork) video — square or tall/portrait — also processed asynchronously. ' +
77
+ 'ALL of these become immutable once the release is distributed — upload the final files before distributing.',
78
+ inputShape: {
79
+ target: z
80
+ .enum([
81
+ 'track_stereo',
82
+ 'track_dolby',
83
+ 'track_lyrics',
84
+ 'release_cover_art',
85
+ 'release_motion_square',
86
+ 'release_motion_tall',
87
+ ])
88
+ .describe('Which asset to upload.'),
89
+ id: z.number().int().positive().describe('The track id (track_*) or release id (release_*).'),
90
+ file_path: z.string().describe('Local path to the file to upload.'),
91
+ },
92
+ annotations: {},
93
+ handler: async (args, { client }) => {
94
+ const target = args.target;
95
+ const filePath = args.file_path;
96
+ if (target in TRACK_FILE_TYPES) {
97
+ const ext = assertAllowedExtension(filePath, TRACK_UPLOAD_EXTS[target]);
98
+ if ('error' in ext)
99
+ return { error: ext.error };
100
+ const fileType = TRACK_FILE_TYPES[target];
101
+ return uploadViaPresignedUrl(client, {
102
+ uploadUrlPath: `/tracks/${args.id}/files/${fileType}/upload-url`,
103
+ commitPath: `/tracks/${args.id}/files/${fileType}`,
104
+ filePath: ext.realPath,
105
+ });
106
+ }
107
+ if (target === 'release_cover_art') {
108
+ const ext = assertAllowedExtension(filePath, IMAGE_EXTS);
109
+ if ('error' in ext)
110
+ return { error: ext.error };
111
+ return client.postMultipart(`/releases/${args.id}/photo`, ext.realPath, 'file');
112
+ }
113
+ const ext = assertAllowedExtension(filePath, MOTION_EXTS);
114
+ if ('error' in ext)
115
+ return { error: ext.error };
116
+ const assetType = MOTION_ASSET_TYPES[target];
117
+ return uploadViaPresignedUrl(client, {
118
+ uploadUrlPath: `/releases/${args.id}/files/${assetType}/upload-url`,
119
+ commitPath: `/releases/${args.id}/files/${assetType}`,
120
+ filePath: ext.realPath,
121
+ });
122
+ },
123
+ };
124
+ const deleteAsset = {
125
+ name: 'delete_asset',
126
+ toolset: 'distribution',
127
+ gate: 'full_write',
128
+ title: 'Delete a release/track asset',
129
+ description: '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.',
130
+ inputShape: {
131
+ target: z
132
+ .enum([
133
+ 'track_stereo',
134
+ 'track_dolby',
135
+ 'track_lyrics',
136
+ 'release_motion_square',
137
+ 'release_motion_tall',
138
+ ])
139
+ .describe('Which asset to delete.'),
140
+ id: z
141
+ .number()
142
+ .int()
143
+ .positive()
144
+ .describe('The track id (track_*) or release id (release_motion_*).'),
145
+ },
146
+ annotations: { destructiveHint: true },
147
+ handler: (args, { client }) => {
148
+ const target = args.target;
149
+ if (target in TRACK_FILE_TYPES) {
150
+ return client.delete(`/tracks/${args.id}/files/${TRACK_FILE_TYPES[target]}`);
151
+ }
152
+ return client.delete(`/releases/${args.id}/files/${MOTION_ASSET_TYPES[target]}`);
153
+ },
154
+ };
155
+ const manageTrackLicense = {
156
+ name: 'manage_track_license',
157
+ toolset: 'distribution',
158
+ gate: 'full_write',
159
+ title: 'Manage a track license',
160
+ description: 'Manage the license documents attached to a track (for a cover or a cleared sample). Pick ONE action with `action`: ' +
161
+ "`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. " +
162
+ '`update` replaces the file and/or metadata of an existing license — `track_license_id` (from list_track_licenses) and `file_path` required. ' +
163
+ '`delete` permanently deletes a license and its file — `track_license_id` required; cannot be undone. ' +
164
+ 'Licenses are immutability-governed once the release is live.',
165
+ inputShape: {
166
+ action: z.enum(['upload', 'update', 'delete']).describe('Which license action.'),
167
+ track_id: z.number().int().positive().describe('The track id.'),
168
+ track_license_id: z
169
+ .number()
170
+ .int()
171
+ .positive()
172
+ .optional()
173
+ .describe('From list_track_licenses. Required for update/delete.'),
174
+ file_path: z
175
+ .string()
176
+ .optional()
177
+ .describe('Local path to the license file. Required for upload/update.'),
178
+ type: z.enum(['cover', 'sample']).optional().describe('Kind of license (upload).'),
179
+ ...licenseMeta,
180
+ },
181
+ annotations: { destructiveHint: true },
182
+ handler: async (args, { client }) => {
183
+ const action = args.action;
184
+ if ((action === 'update' || action === 'delete') && args.track_license_id === undefined) {
185
+ return {
186
+ error: {
187
+ code: 'INVALID_SELECTOR',
188
+ message: `action '${action}' requires \`track_license_id\` — the license to act on (from list_track_licenses).`,
189
+ status: 0,
190
+ },
191
+ };
192
+ }
193
+ if (action === 'delete') {
194
+ return client.delete(`/tracks/${args.track_id}/licenses/${args.track_license_id}`);
195
+ }
196
+ if (args.file_path === undefined) {
197
+ return {
198
+ error: {
199
+ code: 'INVALID_SELECTOR',
200
+ message: `action '${action}' requires \`file_path\` — the local license file to submit.`,
201
+ status: 0,
202
+ },
203
+ };
204
+ }
205
+ const ext = assertAllowedExtension(args.file_path, LICENSE_EXTS);
206
+ if ('error' in ext)
207
+ return { error: ext.error };
208
+ const path = action === 'upload'
209
+ ? `/tracks/${args.track_id}/licenses`
210
+ : `/tracks/${args.track_id}/licenses/${args.track_license_id}`;
211
+ return client.postMultipart(path, ext.realPath, 'file', licenseExtra(args));
212
+ },
213
+ };
214
+ const distributeRelease = {
215
+ name: 'distribute_release',
216
+ toolset: 'distribution',
217
+ gate: 'full_write',
218
+ title: 'Distribute a release',
219
+ description: '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.',
220
+ inputShape: { release_id: releaseId, idempotency_key: idempotencyKey },
221
+ annotations: { destructiveHint: true },
222
+ handler: (args, { client }) => client.post(`/releases/${args.release_id}/distribute`, undefined, {
223
+ idempotency: true,
224
+ idempotencyKey: args.idempotency_key,
225
+ }),
226
+ };
227
+ const takedownRelease = {
228
+ name: 'takedown_release',
229
+ toolset: 'distribution',
230
+ gate: 'full_write',
231
+ title: 'Take down a release',
232
+ description: '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.',
233
+ inputShape: { release_id: releaseId },
234
+ annotations: { destructiveHint: true },
235
+ handler: (args, { client }) => client.post(`/releases/${args.release_id}/takedown-all`),
236
+ };
237
+ const confirmReview = {
238
+ name: 'confirm_review',
239
+ toolset: 'distribution',
240
+ gate: 'full_write',
241
+ title: 'Confirm a held release into review',
242
+ description: '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.',
243
+ inputShape: { release_id: releaseId },
244
+ annotations: { destructiveHint: true, idempotentHint: true },
245
+ handler: (args, { client }) => client.post(`/releases/${args.release_id}/confirm-review`),
246
+ };
247
+ const enableBeatport = {
248
+ name: 'enable_beatport',
249
+ toolset: 'distribution',
250
+ gate: 'full_write',
251
+ title: 'Request Beatport onboarding for a label',
252
+ description: 'Request Beatport onboarding for a label. A one-time action that cannot be un-requested, so confirm the label is correct first.',
253
+ inputShape: { label_id: z.number().int().positive().describe('The label id.') },
254
+ annotations: { destructiveHint: true },
255
+ handler: (args, { client }) => client.post(`/labels/${args.label_id}/enable-beatport`),
256
+ };
257
+ export const distributionTools = [
258
+ uploadAsset,
259
+ deleteAsset,
260
+ manageTrackLicense,
261
+ distributeRelease,
262
+ takedownRelease,
263
+ confirmReview,
264
+ enableBeatport,
265
+ ];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Finance toolset: the consolidated financial query (statements, transactions,
3
+ * royalty breakdowns) and statement downloads. All read-only.
4
+ *
5
+ * `download_statement` fetches a file body. It validates the caller-supplied
6
+ * `save_to_path` and writes ONLY there; a CSV without a save path is returned
7
+ * inline, truncated at 100KB. Downloads use an authenticated raw GET (the
8
+ * shared client's JSON path would corrupt binary PDFs), with the same auth
9
+ * headers the client sends.
10
+ */
11
+ import type { ToolDef } from './types.js';
12
+ export declare const financeTools: ToolDef[];