@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.
- package/CHANGELOG.md +40 -0
- package/README.md +132 -100
- package/dist/config.d.ts +15 -0
- package/dist/config.js +38 -6
- package/dist/coverage.js +79 -79
- package/dist/gating.d.ts +1 -1
- package/dist/gating.js +5 -1
- package/dist/index.js +1 -2
- 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 +2 -2
- package/dist/server.js +24 -4
- 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 +266 -0
- package/dist/tools/distribution.d.ts +13 -0
- package/dist/tools/distribution.js +265 -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/types.d.ts +1 -1
- package/dist/tools/webhooks.d.ts +3 -3
- package/dist/tools/webhooks.js +73 -105
- package/package.json +5 -14
- package/server.json +3 -3
- package/dist/api/content-types.d.ts +0 -33
- package/dist/api/content-types.js +0 -87
- package/dist/api/http.d.ts +0 -74
- package/dist/api/http.js +0 -392
- package/dist/api/upload.d.ts +0 -26
- package/dist/api/upload.js +0 -104
- package/dist/log.d.ts +0 -16
- package/dist/log.js +0 -35
- 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,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[];
|
|
@@ -0,0 +1,139 @@
|
|
|
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 { z } from 'zod';
|
|
7
|
+
import { applyProjection } from '../projection.js';
|
|
8
|
+
/** The 15 metric sections the summary endpoint can return. */
|
|
9
|
+
const METRICS = [
|
|
10
|
+
'streams',
|
|
11
|
+
'listeners',
|
|
12
|
+
'saves',
|
|
13
|
+
'skips',
|
|
14
|
+
'shares',
|
|
15
|
+
'completion-rate',
|
|
16
|
+
'lyrics-view-rate',
|
|
17
|
+
'canvas-view-rate',
|
|
18
|
+
'device-split',
|
|
19
|
+
'source-split',
|
|
20
|
+
'saves-by-tier',
|
|
21
|
+
'streams-by-country',
|
|
22
|
+
'streams-by-gender',
|
|
23
|
+
'streams-by-age',
|
|
24
|
+
'shares-by-country',
|
|
25
|
+
];
|
|
26
|
+
const getAnalytics = {
|
|
27
|
+
name: 'get_analytics',
|
|
28
|
+
toolset: 'insights',
|
|
29
|
+
gate: 'read',
|
|
30
|
+
title: 'Get streaming analytics',
|
|
31
|
+
description: '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. ' +
|
|
32
|
+
'Optionally narrow the result by `platform` (SPOTIFY, ITUNES, APPLE_MUSIC), `release_id`, `isrc`, `upc`, or `artist_names`. ' +
|
|
33
|
+
'By default all 15 metric sections are returned; pass `metrics` (see its enum) to request only a subset. ' +
|
|
34
|
+
'Rate-limited (about 60 requests per minute); a 429 response carries retry_after_seconds.',
|
|
35
|
+
inputShape: {
|
|
36
|
+
start_date: z.string().describe('Start of the reporting window, YYYY-MM-DD.'),
|
|
37
|
+
end_date: z.string().describe('End of the reporting window, YYYY-MM-DD (max 30-day span).'),
|
|
38
|
+
metrics: z
|
|
39
|
+
.array(z.enum(METRICS))
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('Subset of metric sections to return; omit for all 15.'),
|
|
42
|
+
platform: z.enum(['SPOTIFY', 'ITUNES', 'APPLE_MUSIC']).optional(),
|
|
43
|
+
release_id: z.number().int().positive().optional(),
|
|
44
|
+
isrc: z.string().optional(),
|
|
45
|
+
upc: z.string().optional(),
|
|
46
|
+
artist_names: z.array(z.string()).optional().describe('Filter to one or more artist names.'),
|
|
47
|
+
limit: z.number().int().positive().optional(),
|
|
48
|
+
},
|
|
49
|
+
annotations: { readOnlyHint: true },
|
|
50
|
+
handler: (args, { client }) => client.get('/analytics/summary', {
|
|
51
|
+
filter: {
|
|
52
|
+
start_date: args.start_date,
|
|
53
|
+
end_date: args.end_date,
|
|
54
|
+
platform: args.platform,
|
|
55
|
+
release_id: args.release_id,
|
|
56
|
+
isrc: args.isrc,
|
|
57
|
+
upc: args.upc,
|
|
58
|
+
artist_names: args.artist_names,
|
|
59
|
+
},
|
|
60
|
+
metrics: args.metrics,
|
|
61
|
+
limit: args.limit,
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
const queryArtificialStreaming = {
|
|
65
|
+
name: 'query_artificial_streaming',
|
|
66
|
+
toolset: 'insights',
|
|
67
|
+
gate: 'read',
|
|
68
|
+
title: 'Query artificial-streaming data',
|
|
69
|
+
description: 'Query artificial-streaming (streaming-integrity) data for your catalog. Pick ONE view with `view`: ' +
|
|
70
|
+
'`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). ' +
|
|
71
|
+
'`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. ' +
|
|
72
|
+
'`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. ' +
|
|
73
|
+
'`fee_breakdown` retrieves the per-release breakdown of an artificial-streaming fee for one billing period — `period` (required) is YYYY-MM. ' +
|
|
74
|
+
"response_format:'detailed' returns the verbatim API response.",
|
|
75
|
+
inputShape: {
|
|
76
|
+
view: z
|
|
77
|
+
.enum(['flags', 'flag_detail', 'records', 'fee_breakdown'])
|
|
78
|
+
.describe('Which artificial-streaming read.'),
|
|
79
|
+
flag_id: z.number().int().positive().optional().describe('Required for view flag_detail.'),
|
|
80
|
+
period: z.string().optional().describe('YYYY-MM. Required for view fee_breakdown.'),
|
|
81
|
+
filters: z
|
|
82
|
+
.record(z.string(), z.unknown())
|
|
83
|
+
.optional()
|
|
84
|
+
.describe('Filter names → values, passed through verbatim.'),
|
|
85
|
+
cursor: z.string().optional().describe('Pagination cursor (view records).'),
|
|
86
|
+
page: z.number().int().positive().optional().describe('1-based page number (view flags).'),
|
|
87
|
+
per_page: z.number().int().positive().optional().describe('Items per page.'),
|
|
88
|
+
response_format: z
|
|
89
|
+
.enum(['concise', 'detailed'])
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("'concise' (default) keeps only the high-signal fields (ids always kept); 'detailed' returns the verbatim API response."),
|
|
92
|
+
},
|
|
93
|
+
annotations: { readOnlyHint: true },
|
|
94
|
+
handler: async (args, { client }) => {
|
|
95
|
+
const view = args.view;
|
|
96
|
+
if (view === 'flag_detail' && args.flag_id === undefined) {
|
|
97
|
+
return {
|
|
98
|
+
error: {
|
|
99
|
+
code: 'INVALID_SELECTOR',
|
|
100
|
+
message: "view 'flag_detail' requires `flag_id` — the flag to retrieve.",
|
|
101
|
+
status: 0,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (view === 'fee_breakdown' && args.period === undefined) {
|
|
106
|
+
return {
|
|
107
|
+
error: {
|
|
108
|
+
code: 'INVALID_SELECTOR',
|
|
109
|
+
message: "view 'fee_breakdown' requires `period` — the billing month, YYYY-MM.",
|
|
110
|
+
status: 0,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
let result;
|
|
115
|
+
if (view === 'flags') {
|
|
116
|
+
result = await client.get('/stream-radar/flags', {
|
|
117
|
+
page: args.page,
|
|
118
|
+
per_page: args.per_page,
|
|
119
|
+
filter: args.filters,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else if (view === 'flag_detail') {
|
|
123
|
+
result = await client.get(`/stream-radar/flags/${args.flag_id}`);
|
|
124
|
+
}
|
|
125
|
+
else if (view === 'records') {
|
|
126
|
+
// The records endpoint takes its filters as top-level query params.
|
|
127
|
+
result = await client.get('/royalties/artificial-streams', {
|
|
128
|
+
...(args.filters ?? {}),
|
|
129
|
+
cursor: args.cursor,
|
|
130
|
+
per_page: args.per_page,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
result = await client.get(`/artificial-streaming-fee/${encodeURIComponent(String(args.period))}`);
|
|
135
|
+
}
|
|
136
|
+
return applyProjection(result, 'query_artificial_streaming', args.response_format);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
export const insightsTools = [getAnalytics, queryArtificialStreaming];
|
package/dist/tools/reference.js
CHANGED
|
@@ -1,36 +1,21 @@
|
|
|
1
1
|
/** Reference toolset: one tool serving all read-only lookup datasets. */
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
|
|
4
|
-
const REFERENCE_PATHS = {
|
|
5
|
-
genres: '/genres',
|
|
6
|
-
genre_categories: '/genre-categories',
|
|
7
|
-
languages: '/languages',
|
|
8
|
-
contributor_roles: '/contributor-roles',
|
|
9
|
-
instruments: '/instruments',
|
|
10
|
-
distro_outlets: '/distro-outlets',
|
|
11
|
-
territories: '/territories',
|
|
12
|
-
};
|
|
3
|
+
import { REFERENCE_DATASETS, REFERENCE_TYPES } from '../resources.js';
|
|
13
4
|
const listReferenceData = {
|
|
14
5
|
name: 'list_reference_data',
|
|
15
6
|
toolset: 'reference',
|
|
16
7
|
gate: 'read',
|
|
17
8
|
title: 'List reference data',
|
|
18
|
-
description: 'Fetch a LabelGrid reference dataset used to resolve the IDs and codes
|
|
19
|
-
'`genres` and `genre_categories` (
|
|
20
|
-
'`
|
|
21
|
-
'or `
|
|
9
|
+
description: 'Fetch a LabelGrid reference dataset used to resolve the IDs and codes the catalog and release tools expect. Pick ONE dataset with `type`: ' +
|
|
10
|
+
'`genres` and `genre_categories` (genre IDs), `languages` (audio/metadata language codes), `contributor_roles`, `instruments`, `distro_outlets` (the outlets/stores available to your account), ' +
|
|
11
|
+
'`territories` (country codes), `issue_definitions` (each review issue code’s title, description, severity and whether it blocks distribution; codes are string slugs), ' +
|
|
12
|
+
'or `webhook_event_types` (every webhook event type with its payload schema). ' +
|
|
13
|
+
'Call this when you need a valid ID or code. ' +
|
|
14
|
+
'The same datasets are exposed as MCP resources at labelgrid://reference/{type}; this tool is the fallback for clients that don’t surface resources.',
|
|
22
15
|
inputShape: {
|
|
23
|
-
type: z.enum(
|
|
24
|
-
'genres',
|
|
25
|
-
'genre_categories',
|
|
26
|
-
'languages',
|
|
27
|
-
'contributor_roles',
|
|
28
|
-
'instruments',
|
|
29
|
-
'distro_outlets',
|
|
30
|
-
'territories',
|
|
31
|
-
]),
|
|
16
|
+
type: z.enum(REFERENCE_TYPES),
|
|
32
17
|
},
|
|
33
18
|
annotations: { readOnlyHint: true },
|
|
34
|
-
handler: (args, { client }) => client.get(
|
|
19
|
+
handler: (args, { client }) => client.get(REFERENCE_DATASETS[args.type].path),
|
|
35
20
|
};
|
|
36
21
|
export const referenceTools = [listReferenceData];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Releases toolset: release-level reads (review results, the delivery queue,
|
|
3
|
+
* the smart-link landing config, track licenses) plus the safe-write release
|
|
4
|
+
* checks, landing-page management and review-issue notes.
|
|
5
|
+
*
|
|
6
|
+
* The two [proj] reads (`get_release_review`, `get_delivery_queue`) default to
|
|
7
|
+
* concise-mode projection; `response_format: 'detailed'` returns the verbatim
|
|
8
|
+
* API response.
|
|
9
|
+
*/
|
|
10
|
+
import type { ToolDef } from './types.js';
|
|
11
|
+
export declare const releaseTools: ToolDef[];
|