@labelgrid/mcp 0.2.2 → 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.
- package/CHANGELOG.md +29 -0
- package/README.md +132 -100
- package/dist/config.d.ts +15 -0
- package/dist/config.js +37 -5
- package/dist/coverage.js +79 -79
- package/dist/entities.d.ts +25 -0
- package/dist/entities.js +51 -0
- package/dist/gating.d.ts +1 -1
- package/dist/gating.js +5 -1
- package/dist/projection.d.ts +40 -0
- package/dist/projection.js +145 -0
- package/dist/resources.d.ts +29 -0
- package/dist/resources.js +107 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +23 -3
- package/dist/tools/account.d.ts +3 -0
- package/dist/tools/account.js +32 -0
- package/dist/tools/all.js +12 -20
- package/dist/tools/catalog.d.ts +14 -0
- package/dist/tools/catalog.js +267 -0
- package/dist/tools/distribution.d.ts +13 -0
- package/dist/tools/distribution.js +266 -0
- package/dist/tools/finance.d.ts +12 -0
- package/dist/tools/finance.js +311 -0
- package/dist/tools/insights.d.ts +7 -0
- package/dist/tools/insights.js +139 -0
- package/dist/tools/reference.js +9 -24
- package/dist/tools/releases.d.ts +11 -0
- package/dist/tools/releases.js +177 -0
- package/dist/tools/setup.js +1 -1
- package/dist/tools/webhooks.d.ts +3 -3
- package/dist/tools/webhooks.js +73 -105
- package/package.json +3 -2
- package/server.json +3 -3
- package/dist/tools/accounting.d.ts +0 -12
- package/dist/tools/accounting.js +0 -386
- package/dist/tools/analytics.d.ts +0 -3
- package/dist/tools/analytics.js +0 -62
- package/dist/tools/catalog-read.d.ts +0 -10
- package/dist/tools/catalog-read.js +0 -145
- package/dist/tools/catalog-write.d.ts +0 -12
- package/dist/tools/catalog-write.js +0 -206
- package/dist/tools/delivery.d.ts +0 -6
- package/dist/tools/delivery.js +0 -40
- package/dist/tools/files-read.d.ts +0 -7
- package/dist/tools/files-read.js +0 -86
- package/dist/tools/full-writes.d.ts +0 -12
- package/dist/tools/full-writes.js +0 -248
- package/dist/tools/identity.d.ts +0 -3
- package/dist/tools/identity.js +0 -28
- package/dist/tools/release-write.d.ts +0 -12
- package/dist/tools/release-write.js +0 -184
- package/dist/tools/review-read.d.ts +0 -7
- package/dist/tools/review-read.js +0 -78
|
@@ -0,0 +1,266 @@
|
|
|
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 { z } from 'zod';
|
|
13
|
+
import { assertAllowedExtension } from '../api/content-types.js';
|
|
14
|
+
import { uploadViaPresignedUrl } from '../api/upload.js';
|
|
15
|
+
/** Per-target upload extension allow-lists: an upload tool never reads an arbitrary file. */
|
|
16
|
+
const TRACK_UPLOAD_EXTS = {
|
|
17
|
+
track_stereo: ['.wav', '.flac', '.aif', '.aiff'],
|
|
18
|
+
track_dolby: ['.wav'],
|
|
19
|
+
track_lyrics: ['.lrc', '.txt'],
|
|
20
|
+
};
|
|
21
|
+
const MOTION_EXTS = ['.mp4', '.mov'];
|
|
22
|
+
const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.webp', '.tif', '.tiff'];
|
|
23
|
+
const LICENSE_EXTS = ['.pdf', '.jpg', '.jpeg', '.png'];
|
|
24
|
+
/** target → the API's track fileType path segment. */
|
|
25
|
+
const TRACK_FILE_TYPES = {
|
|
26
|
+
track_stereo: 'stereo',
|
|
27
|
+
track_dolby: 'dolby',
|
|
28
|
+
track_lyrics: 'lyrics',
|
|
29
|
+
};
|
|
30
|
+
/** target → the API's release motion-artwork assetType path segment. */
|
|
31
|
+
const MOTION_ASSET_TYPES = {
|
|
32
|
+
release_motion_square: 'square',
|
|
33
|
+
release_motion_tall: 'tall',
|
|
34
|
+
};
|
|
35
|
+
const releaseId = z.number().int().positive().describe('The release id.');
|
|
36
|
+
/** Optional caller-supplied idempotency key, plumbed to the Idempotency-Key header. */
|
|
37
|
+
const idempotencyKey = z
|
|
38
|
+
.string()
|
|
39
|
+
.min(8)
|
|
40
|
+
.max(128)
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('The server deduplicates by this key for 24h — reuse the SAME key when retrying a call whose outcome you did not observe.');
|
|
43
|
+
/** Optional license metadata shared by the license upload/update actions. */
|
|
44
|
+
const licenseMeta = {
|
|
45
|
+
license_id: z.string().optional().describe('The license/clearance reference number, if any.'),
|
|
46
|
+
license_provider: z
|
|
47
|
+
.enum(['licensing_agency', 'direct_from_publisher'])
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Where the license came from.'),
|
|
50
|
+
license_provider_name: z.string().optional().describe('The name of the license provider.'),
|
|
51
|
+
original_track_link: z.string().optional().describe('URL to the original/source track.'),
|
|
52
|
+
};
|
|
53
|
+
/** Collects the defined license metadata fields into a string map for multipart. */
|
|
54
|
+
function licenseExtra(args) {
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const key of [
|
|
57
|
+
'type',
|
|
58
|
+
'license_id',
|
|
59
|
+
'license_provider',
|
|
60
|
+
'license_provider_name',
|
|
61
|
+
'original_track_link',
|
|
62
|
+
]) {
|
|
63
|
+
const v = args[key];
|
|
64
|
+
if (v !== undefined && v !== null)
|
|
65
|
+
out[key] = String(v);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
const uploadAsset = {
|
|
70
|
+
name: 'upload_asset',
|
|
71
|
+
toolset: 'distribution',
|
|
72
|
+
gate: 'full_write',
|
|
73
|
+
title: 'Upload a release/track asset',
|
|
74
|
+
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_*. ' +
|
|
75
|
+
'`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). ' +
|
|
76
|
+
"`release_cover_art` uploads or replaces the release's static cover art image. " +
|
|
77
|
+
'`release_motion_square` / `release_motion_tall` upload the animated cover (motion artwork) video — square or tall/portrait — also processed asynchronously. ' +
|
|
78
|
+
'ALL of these become immutable once the release is distributed — upload the final files before distributing.',
|
|
79
|
+
inputShape: {
|
|
80
|
+
target: z
|
|
81
|
+
.enum([
|
|
82
|
+
'track_stereo',
|
|
83
|
+
'track_dolby',
|
|
84
|
+
'track_lyrics',
|
|
85
|
+
'release_cover_art',
|
|
86
|
+
'release_motion_square',
|
|
87
|
+
'release_motion_tall',
|
|
88
|
+
])
|
|
89
|
+
.describe('Which asset to upload.'),
|
|
90
|
+
id: z.number().int().positive().describe('The track id (track_*) or release id (release_*).'),
|
|
91
|
+
file_path: z.string().describe('Local path to the file to upload.'),
|
|
92
|
+
},
|
|
93
|
+
annotations: {},
|
|
94
|
+
handler: async (args, { client }) => {
|
|
95
|
+
const target = args.target;
|
|
96
|
+
const filePath = args.file_path;
|
|
97
|
+
if (target in TRACK_FILE_TYPES) {
|
|
98
|
+
const ext = assertAllowedExtension(filePath, TRACK_UPLOAD_EXTS[target]);
|
|
99
|
+
if ('error' in ext)
|
|
100
|
+
return { error: ext.error };
|
|
101
|
+
const fileType = TRACK_FILE_TYPES[target];
|
|
102
|
+
return uploadViaPresignedUrl(client, {
|
|
103
|
+
uploadUrlPath: `/tracks/${args.id}/files/${fileType}/upload-url`,
|
|
104
|
+
commitPath: `/tracks/${args.id}/files/${fileType}`,
|
|
105
|
+
filePath: ext.realPath,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (target === 'release_cover_art') {
|
|
109
|
+
const ext = assertAllowedExtension(filePath, IMAGE_EXTS);
|
|
110
|
+
if ('error' in ext)
|
|
111
|
+
return { error: ext.error };
|
|
112
|
+
return client.postMultipart(`/releases/${args.id}/photo`, ext.realPath, 'file');
|
|
113
|
+
}
|
|
114
|
+
const ext = assertAllowedExtension(filePath, MOTION_EXTS);
|
|
115
|
+
if ('error' in ext)
|
|
116
|
+
return { error: ext.error };
|
|
117
|
+
const assetType = MOTION_ASSET_TYPES[target];
|
|
118
|
+
return uploadViaPresignedUrl(client, {
|
|
119
|
+
uploadUrlPath: `/releases/${args.id}/files/${assetType}/upload-url`,
|
|
120
|
+
commitPath: `/releases/${args.id}/files/${assetType}`,
|
|
121
|
+
filePath: ext.realPath,
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const deleteAsset = {
|
|
126
|
+
name: 'delete_asset',
|
|
127
|
+
toolset: 'distribution',
|
|
128
|
+
gate: 'full_write',
|
|
129
|
+
title: 'Delete a release/track asset',
|
|
130
|
+
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.',
|
|
131
|
+
inputShape: {
|
|
132
|
+
target: z
|
|
133
|
+
.enum([
|
|
134
|
+
'track_stereo',
|
|
135
|
+
'track_dolby',
|
|
136
|
+
'track_lyrics',
|
|
137
|
+
'release_motion_square',
|
|
138
|
+
'release_motion_tall',
|
|
139
|
+
])
|
|
140
|
+
.describe('Which asset to delete.'),
|
|
141
|
+
id: z
|
|
142
|
+
.number()
|
|
143
|
+
.int()
|
|
144
|
+
.positive()
|
|
145
|
+
.describe('The track id (track_*) or release id (release_motion_*).'),
|
|
146
|
+
},
|
|
147
|
+
annotations: { destructiveHint: true },
|
|
148
|
+
handler: (args, { client }) => {
|
|
149
|
+
const target = args.target;
|
|
150
|
+
if (target in TRACK_FILE_TYPES) {
|
|
151
|
+
return client.delete(`/tracks/${args.id}/files/${TRACK_FILE_TYPES[target]}`);
|
|
152
|
+
}
|
|
153
|
+
return client.delete(`/releases/${args.id}/files/${MOTION_ASSET_TYPES[target]}`);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const manageTrackLicense = {
|
|
157
|
+
name: 'manage_track_license',
|
|
158
|
+
toolset: 'distribution',
|
|
159
|
+
gate: 'full_write',
|
|
160
|
+
title: 'Manage a track license',
|
|
161
|
+
description: 'Manage the license documents attached to a track (for a cover or a cleared sample). Pick ONE action with `action`: ' +
|
|
162
|
+
"`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. " +
|
|
163
|
+
'`update` replaces the file and/or metadata of an existing license — `track_license_id` (from list_track_licenses) and `file_path` required. ' +
|
|
164
|
+
'`delete` permanently deletes a license and its file — `track_license_id` required; cannot be undone. ' +
|
|
165
|
+
'Licenses are immutability-governed once the release is live.',
|
|
166
|
+
inputShape: {
|
|
167
|
+
action: z.enum(['upload', 'update', 'delete']).describe('Which license action.'),
|
|
168
|
+
track_id: z.number().int().positive().describe('The track id.'),
|
|
169
|
+
track_license_id: z
|
|
170
|
+
.number()
|
|
171
|
+
.int()
|
|
172
|
+
.positive()
|
|
173
|
+
.optional()
|
|
174
|
+
.describe('From list_track_licenses. Required for update/delete.'),
|
|
175
|
+
file_path: z
|
|
176
|
+
.string()
|
|
177
|
+
.optional()
|
|
178
|
+
.describe('Local path to the license file. Required for upload/update.'),
|
|
179
|
+
type: z.enum(['cover', 'sample']).optional().describe('Kind of license (upload).'),
|
|
180
|
+
...licenseMeta,
|
|
181
|
+
},
|
|
182
|
+
annotations: { destructiveHint: true },
|
|
183
|
+
handler: async (args, { client }) => {
|
|
184
|
+
const action = args.action;
|
|
185
|
+
if ((action === 'update' || action === 'delete') && args.track_license_id === undefined) {
|
|
186
|
+
return {
|
|
187
|
+
error: {
|
|
188
|
+
code: 'INVALID_SELECTOR',
|
|
189
|
+
message: `action '${action}' requires \`track_license_id\` — the license to act on (from list_track_licenses).`,
|
|
190
|
+
status: 0,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (action === 'delete') {
|
|
195
|
+
return client.delete(`/tracks/${args.track_id}/licenses/${args.track_license_id}`);
|
|
196
|
+
}
|
|
197
|
+
if (args.file_path === undefined) {
|
|
198
|
+
return {
|
|
199
|
+
error: {
|
|
200
|
+
code: 'INVALID_SELECTOR',
|
|
201
|
+
message: `action '${action}' requires \`file_path\` — the local license file to submit.`,
|
|
202
|
+
status: 0,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const ext = assertAllowedExtension(args.file_path, LICENSE_EXTS);
|
|
207
|
+
if ('error' in ext)
|
|
208
|
+
return { error: ext.error };
|
|
209
|
+
const path = action === 'upload'
|
|
210
|
+
? `/tracks/${args.track_id}/licenses`
|
|
211
|
+
: `/tracks/${args.track_id}/licenses/${args.track_license_id}`;
|
|
212
|
+
return client.postMultipart(path, ext.realPath, 'file', licenseExtra(args));
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
const distributeRelease = {
|
|
216
|
+
name: 'distribute_release',
|
|
217
|
+
toolset: 'distribution',
|
|
218
|
+
gate: 'full_write',
|
|
219
|
+
title: 'Distribute a release',
|
|
220
|
+
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.',
|
|
221
|
+
inputShape: { release_id: releaseId, idempotency_key: idempotencyKey },
|
|
222
|
+
annotations: { destructiveHint: true },
|
|
223
|
+
handler: (args, { client }) => client.post(`/releases/${args.release_id}/distribute`, undefined, {
|
|
224
|
+
idempotency: true,
|
|
225
|
+
idempotencyKey: args.idempotency_key,
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
const takedownRelease = {
|
|
229
|
+
name: 'takedown_release',
|
|
230
|
+
toolset: 'distribution',
|
|
231
|
+
gate: 'full_write',
|
|
232
|
+
title: 'Take down a release',
|
|
233
|
+
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.',
|
|
234
|
+
inputShape: { release_id: releaseId },
|
|
235
|
+
annotations: { destructiveHint: true },
|
|
236
|
+
handler: (args, { client }) => client.post(`/releases/${args.release_id}/takedown-all`),
|
|
237
|
+
};
|
|
238
|
+
const confirmReview = {
|
|
239
|
+
name: 'confirm_review',
|
|
240
|
+
toolset: 'distribution',
|
|
241
|
+
gate: 'full_write',
|
|
242
|
+
title: 'Confirm a held release into review',
|
|
243
|
+
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.',
|
|
244
|
+
inputShape: { release_id: releaseId },
|
|
245
|
+
annotations: { destructiveHint: true, idempotentHint: true },
|
|
246
|
+
handler: (args, { client }) => client.post(`/releases/${args.release_id}/confirm-review`),
|
|
247
|
+
};
|
|
248
|
+
const enableBeatport = {
|
|
249
|
+
name: 'enable_beatport',
|
|
250
|
+
toolset: 'distribution',
|
|
251
|
+
gate: 'full_write',
|
|
252
|
+
title: 'Request Beatport onboarding for a label',
|
|
253
|
+
description: 'Request Beatport onboarding for a label. A one-time action that cannot be un-requested, so confirm the label is correct first.',
|
|
254
|
+
inputShape: { label_id: z.number().int().positive().describe('The label id.') },
|
|
255
|
+
annotations: { destructiveHint: true },
|
|
256
|
+
handler: (args, { client }) => client.post(`/labels/${args.label_id}/enable-beatport`),
|
|
257
|
+
};
|
|
258
|
+
export const distributionTools = [
|
|
259
|
+
uploadAsset,
|
|
260
|
+
deleteAsset,
|
|
261
|
+
manageTrackLicense,
|
|
262
|
+
distributeRelease,
|
|
263
|
+
takedownRelease,
|
|
264
|
+
confirmReview,
|
|
265
|
+
enableBeatport,
|
|
266
|
+
];
|
|
@@ -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[];
|
|
@@ -0,0 +1,311 @@
|
|
|
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 { realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, isAbsolute } from 'node:path';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { applyProjection } from '../projection.js';
|
|
15
|
+
import { VERSION } from '../version.js';
|
|
16
|
+
const INLINE_CSV_LIMIT = 100 * 1024;
|
|
17
|
+
/**
|
|
18
|
+
* Validates that save_to_path is absolute and its parent resolves (via
|
|
19
|
+
* realpathSync, so a dangling/symlinked parent is rejected) to an existing real
|
|
20
|
+
* directory. Writing itself is exclusive (see writeNewFile), so this never
|
|
21
|
+
* overwrites an existing file.
|
|
22
|
+
*/
|
|
23
|
+
function validateSavePath(p) {
|
|
24
|
+
if (!isAbsolute(p)) {
|
|
25
|
+
return {
|
|
26
|
+
code: 'INVALID_PATH',
|
|
27
|
+
message: `save_to_path must be an absolute path (received: ${p}).`,
|
|
28
|
+
status: 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const dir = dirname(p);
|
|
32
|
+
let realDir;
|
|
33
|
+
try {
|
|
34
|
+
realDir = realpathSync(dir);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return {
|
|
38
|
+
code: 'INVALID_PATH',
|
|
39
|
+
message: `The parent directory of save_to_path does not exist: ${dir}.`,
|
|
40
|
+
status: 0,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
let isDir = false;
|
|
44
|
+
try {
|
|
45
|
+
isDir = statSync(realDir).isDirectory();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
isDir = false;
|
|
49
|
+
}
|
|
50
|
+
if (!isDir) {
|
|
51
|
+
return {
|
|
52
|
+
code: 'INVALID_PATH',
|
|
53
|
+
message: `The parent directory of save_to_path is not a directory: ${dir}.`,
|
|
54
|
+
status: 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Writes a file with exclusive creation ('wx'): an existing path is NEVER
|
|
61
|
+
* overwritten. Returns FILE_EXISTS on collision, or a structured write error,
|
|
62
|
+
* or null on success.
|
|
63
|
+
*/
|
|
64
|
+
function writeNewFile(path, data) {
|
|
65
|
+
try {
|
|
66
|
+
writeFileSync(path, data, { flag: 'wx' });
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err.code === 'EEXIST') {
|
|
71
|
+
return {
|
|
72
|
+
code: 'FILE_EXISTS',
|
|
73
|
+
message: `A file already exists at ${path}. This tool never overwrites — choose a new path.`,
|
|
74
|
+
status: 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
code: 'WRITE_FAILED',
|
|
79
|
+
message: `Could not write to ${path}: ${err instanceof Error ? err.message : 'unknown error'}.`,
|
|
80
|
+
status: 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Maps an error HTTP status from a raw download into a structured code. */
|
|
85
|
+
function statusToCode(status) {
|
|
86
|
+
if (status === 401)
|
|
87
|
+
return 'TOKEN_INVALID';
|
|
88
|
+
if (status === 403)
|
|
89
|
+
return 'FORBIDDEN';
|
|
90
|
+
if (status === 404)
|
|
91
|
+
return 'NOT_FOUND';
|
|
92
|
+
if (status >= 500)
|
|
93
|
+
return 'SERVER_ERROR';
|
|
94
|
+
return 'ERROR';
|
|
95
|
+
}
|
|
96
|
+
/** Authenticated raw GET for file downloads; returns the Response or an error. */
|
|
97
|
+
async function authedGet(ctx, path) {
|
|
98
|
+
const base = ctx.config.baseUrl.replace(/\/+$/, '');
|
|
99
|
+
let res;
|
|
100
|
+
try {
|
|
101
|
+
res = await ctx.client.raw(`${base}${path}`, {
|
|
102
|
+
method: 'GET',
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${ctx.config.token}`,
|
|
105
|
+
Accept: 'application/json',
|
|
106
|
+
'User-Agent': `labelgrid-mcp/${VERSION}`,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
error: {
|
|
114
|
+
code: 'NETWORK_ERROR',
|
|
115
|
+
message: err instanceof Error ? err.message : 'Network request failed.',
|
|
116
|
+
status: 0,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
let message = `Request failed with status ${res.status}.`;
|
|
122
|
+
try {
|
|
123
|
+
const text = await res.text();
|
|
124
|
+
if (text) {
|
|
125
|
+
try {
|
|
126
|
+
const body = JSON.parse(text);
|
|
127
|
+
if (typeof body.message === 'string')
|
|
128
|
+
message = body.message;
|
|
129
|
+
else if (typeof body.error === 'string')
|
|
130
|
+
message = body.error;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
message = text;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// keep the default message
|
|
139
|
+
}
|
|
140
|
+
return { ok: false, error: { code: statusToCode(res.status), message, status: res.status } };
|
|
141
|
+
}
|
|
142
|
+
return { ok: true, res };
|
|
143
|
+
}
|
|
144
|
+
const queryFinancials = {
|
|
145
|
+
name: 'query_financials',
|
|
146
|
+
toolset: 'finance',
|
|
147
|
+
gate: 'read',
|
|
148
|
+
title: 'Query financial data',
|
|
149
|
+
description: 'Query your financial data. Pick ONE view with `view`: ' +
|
|
150
|
+
'`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. ' +
|
|
151
|
+
'`statement_detail` retrieves one statement by `invoice_number` (required). ' +
|
|
152
|
+
'`transactions` lists account transactions, paginated — same `filters`; sort with `sort`; group_by="release" rolls up per release. ' +
|
|
153
|
+
'`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. ' +
|
|
154
|
+
"Use download_statement for statement line items (CSV) or the invoice PDF. response_format:'detailed' returns the verbatim API response.",
|
|
155
|
+
inputShape: {
|
|
156
|
+
view: z
|
|
157
|
+
.enum(['statements', 'statement_detail', 'transactions', 'royalty_breakdown'])
|
|
158
|
+
.describe('Which financial read.'),
|
|
159
|
+
invoice_number: z.string().optional().describe('Required for view statement_detail.'),
|
|
160
|
+
group_by: z
|
|
161
|
+
.string()
|
|
162
|
+
.optional()
|
|
163
|
+
.describe('REQUIRED for royalty_breakdown (ordered subset: track, dsp, release, territory, period); "release" rolls statements/transactions up per release.'),
|
|
164
|
+
sort: z.string().optional().describe('Sort expression (view transactions).'),
|
|
165
|
+
filters: z
|
|
166
|
+
.record(z.string(), z.unknown())
|
|
167
|
+
.optional()
|
|
168
|
+
.describe('label_id, release_id, isrc, upc, start_date, end_date — passed through verbatim.'),
|
|
169
|
+
cursor: z.string().optional().describe('Pagination cursor (view royalty_breakdown).'),
|
|
170
|
+
page: z.number().int().positive().optional().describe('1-based page number.'),
|
|
171
|
+
per_page: z.number().int().positive().optional().describe('Items per page.'),
|
|
172
|
+
response_format: z
|
|
173
|
+
.enum(['concise', 'detailed'])
|
|
174
|
+
.optional()
|
|
175
|
+
.describe("'concise' (default) keeps only the high-signal fields (ids always kept); 'detailed' returns the verbatim API response."),
|
|
176
|
+
},
|
|
177
|
+
annotations: { readOnlyHint: true },
|
|
178
|
+
handler: async (args, { client }) => {
|
|
179
|
+
const view = args.view;
|
|
180
|
+
let result;
|
|
181
|
+
if (view === 'statements') {
|
|
182
|
+
result = await client.get('/statements', {
|
|
183
|
+
group_by: args.group_by,
|
|
184
|
+
page: args.page,
|
|
185
|
+
per_page: args.per_page,
|
|
186
|
+
filter: args.filters,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
else if (view === 'statement_detail') {
|
|
190
|
+
if (args.invoice_number === undefined) {
|
|
191
|
+
return {
|
|
192
|
+
error: {
|
|
193
|
+
code: 'INVALID_SELECTOR',
|
|
194
|
+
message: "view 'statement_detail' requires `invoice_number` — the statement invoice number.",
|
|
195
|
+
status: 0,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
result = await client.get(`/statements/${encodeURIComponent(String(args.invoice_number))}`);
|
|
200
|
+
}
|
|
201
|
+
else if (view === 'transactions') {
|
|
202
|
+
result = await client.get('/transactions', {
|
|
203
|
+
group_by: args.group_by,
|
|
204
|
+
page: args.page,
|
|
205
|
+
per_page: args.per_page,
|
|
206
|
+
sort: args.sort,
|
|
207
|
+
filter: args.filters,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
result = await client.get('/royalties/breakdown', {
|
|
212
|
+
group_by: args.group_by,
|
|
213
|
+
per_page: args.per_page,
|
|
214
|
+
cursor: args.cursor,
|
|
215
|
+
filter: args.filters,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return applyProjection(result, 'query_financials', args.response_format);
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
const downloadStatement = {
|
|
222
|
+
name: 'download_statement',
|
|
223
|
+
toolset: 'finance',
|
|
224
|
+
gate: 'read',
|
|
225
|
+
title: 'Download a statement file',
|
|
226
|
+
description: "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).",
|
|
227
|
+
inputShape: {
|
|
228
|
+
format: z
|
|
229
|
+
.enum(['csv', 'invoice_pdf'])
|
|
230
|
+
.describe('Which file: csv (line items) or invoice_pdf (the invoice PDF).'),
|
|
231
|
+
invoice_number: z
|
|
232
|
+
.string()
|
|
233
|
+
.optional()
|
|
234
|
+
.describe('Single-statement invoice number. Required for format invoice_pdf.'),
|
|
235
|
+
start_date: z.string().optional().describe('CSV export range start, YYYY-MM-DD.'),
|
|
236
|
+
end_date: z.string().optional().describe('CSV export range end, YYYY-MM-DD.'),
|
|
237
|
+
save_to_path: z
|
|
238
|
+
.string()
|
|
239
|
+
.optional()
|
|
240
|
+
.describe('Absolute path (existing parent dir) to write the file to. Optional for csv (otherwise returned inline); required for invoice_pdf.'),
|
|
241
|
+
},
|
|
242
|
+
annotations: { readOnlyHint: true },
|
|
243
|
+
handler: async (args, ctx) => {
|
|
244
|
+
const invoice = args.invoice_number;
|
|
245
|
+
const savePath = args.save_to_path;
|
|
246
|
+
if (args.format === 'invoice_pdf') {
|
|
247
|
+
if (invoice === undefined || invoice === '') {
|
|
248
|
+
return {
|
|
249
|
+
error: {
|
|
250
|
+
code: 'INVALID_SELECTOR',
|
|
251
|
+
message: "format 'invoice_pdf' requires `invoice_number` — the statement invoice number.",
|
|
252
|
+
status: 0,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (savePath === undefined) {
|
|
257
|
+
return {
|
|
258
|
+
error: {
|
|
259
|
+
code: 'INVALID_SELECTOR',
|
|
260
|
+
message: "format 'invoice_pdf' requires `save_to_path` — an absolute path to write the binary PDF to.",
|
|
261
|
+
status: 0,
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const err = validateSavePath(savePath);
|
|
266
|
+
if (err)
|
|
267
|
+
return { error: err };
|
|
268
|
+
const result = await authedGet(ctx, `/statements/${encodeURIComponent(invoice)}/invoice`);
|
|
269
|
+
if (!result.ok)
|
|
270
|
+
return { error: result.error };
|
|
271
|
+
const bytes = Buffer.from(await result.res.arrayBuffer());
|
|
272
|
+
const writeErr = writeNewFile(savePath, bytes);
|
|
273
|
+
if (writeErr)
|
|
274
|
+
return { error: writeErr };
|
|
275
|
+
return { data: { saved_to: savePath, bytes: bytes.length } };
|
|
276
|
+
}
|
|
277
|
+
// format === 'csv'
|
|
278
|
+
if (savePath !== undefined) {
|
|
279
|
+
const err = validateSavePath(savePath);
|
|
280
|
+
if (err)
|
|
281
|
+
return { error: err };
|
|
282
|
+
}
|
|
283
|
+
let path;
|
|
284
|
+
if (invoice !== undefined && invoice !== '') {
|
|
285
|
+
path = `/statements/${encodeURIComponent(invoice)}/csv`;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
const parts = [];
|
|
289
|
+
if (args.start_date !== undefined)
|
|
290
|
+
parts.push(`start_date=${encodeURIComponent(String(args.start_date))}`);
|
|
291
|
+
if (args.end_date !== undefined)
|
|
292
|
+
parts.push(`end_date=${encodeURIComponent(String(args.end_date))}`);
|
|
293
|
+
path = `/statements/export/csv${parts.length > 0 ? `?${parts.join('&')}` : ''}`;
|
|
294
|
+
}
|
|
295
|
+
const result = await authedGet(ctx, path);
|
|
296
|
+
if (!result.ok)
|
|
297
|
+
return { error: result.error };
|
|
298
|
+
const text = await result.res.text();
|
|
299
|
+
const totalBytes = Buffer.byteLength(text);
|
|
300
|
+
if (savePath !== undefined) {
|
|
301
|
+
const writeErr = writeNewFile(savePath, text);
|
|
302
|
+
if (writeErr)
|
|
303
|
+
return { error: writeErr };
|
|
304
|
+
return { data: { saved_to: savePath, bytes: totalBytes } };
|
|
305
|
+
}
|
|
306
|
+
const truncated = text.length > INLINE_CSV_LIMIT;
|
|
307
|
+
const content = truncated ? text.slice(0, INLINE_CSV_LIMIT) : text;
|
|
308
|
+
return { data: { content, truncated, bytes: totalBytes } };
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
export const financeTools = [queryFinancials, downloadStatement];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insights toolset: the streaming analytics summary and the consolidated
|
|
3
|
+
* artificial-streaming query (early-warning flags, reported records, and the
|
|
4
|
+
* fee breakdown). All read-only.
|
|
5
|
+
*/
|
|
6
|
+
import type { ToolDef } from './types.js';
|
|
7
|
+
export declare const insightsTools: ToolDef[];
|