@labelgrid/mcp 0.3.0 → 0.4.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.
@@ -1,26 +0,0 @@
1
- /**
2
- * Presigned-URL upload helper.
3
- *
4
- * A large binary asset is never streamed through the LabelGrid API. Instead the
5
- * flow is three steps:
6
- * 1. POST the upload-url endpoint (with the filename) to mint a short-lived
7
- * presigned storage URL and its object key.
8
- * 2. PUT the file bytes straight to that presigned URL. This request carries
9
- * NO Authorization header — the signature in the URL is the credential, and
10
- * an extra Bearer token would break it.
11
- * 3. PUT the commit endpoint with the returned object key (with an idempotency
12
- * key) so the API records the finalized file.
13
- *
14
- * A failure at step 2 aborts before the commit, so a half-uploaded object is
15
- * never finalized. Business rules (format checks, transcoding) stay server-side.
16
- */
17
- import type { ApiResult, LabelGridClient } from './http.js';
18
- export type UploadOptions = {
19
- /** The endpoint that mints the presigned URL, e.g. /tracks/42/files/stereo/upload-url. */
20
- uploadUrlPath: string;
21
- /** The endpoint that records the finalized file, e.g. /tracks/42/files/stereo. */
22
- commitPath: string;
23
- /** Absolute or relative local path to the file to upload. */
24
- filePath: string;
25
- };
26
- export declare function uploadViaPresignedUrl(client: LabelGridClient, opts: UploadOptions): Promise<ApiResult<unknown>>;
@@ -1,104 +0,0 @@
1
- /**
2
- * Presigned-URL upload helper.
3
- *
4
- * A large binary asset is never streamed through the LabelGrid API. Instead the
5
- * flow is three steps:
6
- * 1. POST the upload-url endpoint (with the filename) to mint a short-lived
7
- * presigned storage URL and its object key.
8
- * 2. PUT the file bytes straight to that presigned URL. This request carries
9
- * NO Authorization header — the signature in the URL is the credential, and
10
- * an extra Bearer token would break it.
11
- * 3. PUT the commit endpoint with the returned object key (with an idempotency
12
- * key) so the API records the finalized file.
13
- *
14
- * A failure at step 2 aborts before the commit, so a half-uploaded object is
15
- * never finalized. Business rules (format checks, transcoding) stay server-side.
16
- */
17
- import { statSync } from 'node:fs';
18
- import { readFile } from 'node:fs/promises';
19
- import { basename } from 'node:path';
20
- import { log } from '../log.js';
21
- import { contentType } from './content-types.js';
22
- /** True only for an existing regular file. */
23
- function isReadableFile(p) {
24
- try {
25
- return statSync(p).isFile();
26
- }
27
- catch {
28
- return false;
29
- }
30
- }
31
- export async function uploadViaPresignedUrl(client, opts) {
32
- // Fail fast and locally: never touch the network for a file we cannot read.
33
- if (!isReadableFile(opts.filePath)) {
34
- const error = {
35
- code: 'FILE_NOT_FOUND',
36
- message: `No readable file at ${opts.filePath}.`,
37
- status: 0,
38
- };
39
- return { error };
40
- }
41
- // Step 1: mint the presigned URL.
42
- const minted = await client.post(opts.uploadUrlPath, {
43
- filename: basename(opts.filePath),
44
- });
45
- if ('error' in minted)
46
- return minted;
47
- const uploadUrl = minted.data?.upload_url;
48
- const key = minted.data?.key;
49
- if (typeof uploadUrl !== 'string' || typeof key !== 'string') {
50
- const error = {
51
- code: 'UPLOAD_URL_INVALID',
52
- message: 'The upload-url response did not contain a usable upload_url and key.',
53
- status: 0,
54
- };
55
- return { error };
56
- }
57
- // Step 2: PUT the bytes directly to storage — NO auth header (the URL is signed).
58
- // The file passed isReadableFile above, but it can vanish before this read
59
- // (a TOCTOU race); a structured FILE_NOT_FOUND is the contract, not a throw.
60
- let bytes;
61
- try {
62
- bytes = await readFile(opts.filePath);
63
- }
64
- catch {
65
- const error = {
66
- code: 'FILE_NOT_FOUND',
67
- message: `The file at ${opts.filePath} could not be read.`,
68
- status: 0,
69
- };
70
- return { error };
71
- }
72
- let putRes;
73
- try {
74
- putRes = await client.raw(uploadUrl, {
75
- method: 'PUT',
76
- headers: { 'Content-Type': contentType(opts.filePath) },
77
- body: new Uint8Array(bytes),
78
- });
79
- }
80
- catch (err) {
81
- // Never surface err.message raw to the log — it can embed the signed URL,
82
- // and `reason` is not a redacted key. Strip any URL before logging.
83
- log('error', 'presigned upload PUT failed', {
84
- reason: err instanceof Error ? err.message.replace(/https?:\/\/\S+/gi, '[url]') : 'network error',
85
- });
86
- const error = {
87
- code: 'UPLOAD_FAILED',
88
- message: 'Uploading the file to storage failed.',
89
- status: 0,
90
- };
91
- return { error };
92
- }
93
- if (!putRes.ok) {
94
- // Abort BEFORE the commit — a half-uploaded object is never finalized.
95
- const error = {
96
- code: 'UPLOAD_FAILED',
97
- message: `Uploading the file to storage failed with status ${putRes.status}.`,
98
- status: putRes.status,
99
- };
100
- return { error };
101
- }
102
- // Step 3: commit the object key (idempotent — a retried commit will not duplicate).
103
- return client.put(opts.commitPath, { s3_key: key }, { idempotency: true });
104
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * The catalog-entity registry: the six entity kinds the consolidated catalog
3
- * tools operate on, each with its endpoint path and the reviewed documentation
4
- * fragments (list filters, create/update fields, delete refusals) the tool
5
- * descriptions are assembled from.
6
- *
7
- * This is data, not behavior — the catalog tools stay thin wrappers and the
8
- * API owns all validation. The wording here carries the caveats from the
9
- * per-entity tool descriptions it replaces (recording_country on track create,
10
- * RELEASE_LOCKED_FIELDS on release update, the delete refusals).
11
- */
12
- export type EntityName = 'label' | 'artist' | 'writer' | 'publisher' | 'release' | 'track';
13
- /** The entity names as a tuple, for zod enum inputs. */
14
- export declare const ENTITY_NAMES: readonly ["label", "artist", "writer", "publisher", "release", "track"];
15
- export type EntitySpec = {
16
- /** The collection endpoint path, e.g. '/labels'. */
17
- path: string;
18
- /** One-line doc of the useful list filters for search_catalog. */
19
- filtersDoc: string;
20
- /** One-line doc of required + common create/update fields. */
21
- fieldsDoc: string;
22
- /** One-line doc of the server-side delete refusals. */
23
- deleteNote: string;
24
- };
25
- export declare const ENTITIES: Record<EntityName, EntitySpec>;
package/dist/entities.js DELETED
@@ -1,51 +0,0 @@
1
- /**
2
- * The catalog-entity registry: the six entity kinds the consolidated catalog
3
- * tools operate on, each with its endpoint path and the reviewed documentation
4
- * fragments (list filters, create/update fields, delete refusals) the tool
5
- * descriptions are assembled from.
6
- *
7
- * This is data, not behavior — the catalog tools stay thin wrappers and the
8
- * API owns all validation. The wording here carries the caveats from the
9
- * per-entity tool descriptions it replaces (recording_country on track create,
10
- * RELEASE_LOCKED_FIELDS on release update, the delete refusals).
11
- */
12
- /** The entity names as a tuple, for zod enum inputs. */
13
- export const ENTITY_NAMES = ['label', 'artist', 'writer', 'publisher', 'release', 'track'];
14
- export const ENTITIES = {
15
- label: {
16
- path: '/labels',
17
- filtersDoc: 'label: no documented filters — paginate with page/per_page.',
18
- fieldsDoc: 'label — required: name, default_email; optional: support email, website/platform URLs, default copyright lines, isrc_base.',
19
- deleteNote: 'label: refused while the label still has releases — remove or reassign its releases first.',
20
- },
21
- artist: {
22
- path: '/artists',
23
- filtersDoc: 'artist: artist_name (filter by artist name).',
24
- fieldsDoc: 'artist — required: artist_name; optional: full_name, email, location, bios, isni, default_language, platform profile URLs.',
25
- deleteNote: 'artist: refused while still referenced by releases or tracks.',
26
- },
27
- writer: {
28
- path: '/writers',
29
- filtersDoc: 'writer: name (writer name), ipi (IPI number).',
30
- fieldsDoc: '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).',
31
- deleteNote: 'writer: refused while still referenced by tracks.',
32
- },
33
- publisher: {
34
- path: '/publishers',
35
- filtersDoc: 'publisher: name (publisher name), ipi (IPI number).',
36
- fieldsDoc: 'publisher — required: name; optional: ipi, pro, isni, controlled_publisher.',
37
- deleteNote: 'publisher: refused while still referenced by writers.',
38
- },
39
- release: {
40
- path: '/releases',
41
- filtersDoc: 'release: label_id (owning label id), is_live (1 = live/distributed only), barcode_number (UPC/EAN), cat (catalog number).',
42
- fieldsDoc: '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.',
43
- deleteNote: 'release: only a never-submitted draft can be deleted.',
44
- },
45
- track: {
46
- path: '/tracks',
47
- filtersDoc: 'track: release_id (one release’s tracks), isrc (filter by ISRC).',
48
- fieldsDoc: '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.',
49
- deleteNote: 'track: allowed while the parent release is an editable draft; refused once submitted or distributed.',
50
- },
51
- };
package/dist/log.d.ts DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * stderr-only structured logging with secret redaction.
3
- *
4
- * stdout is reserved for the MCP protocol stream, so every log line goes to
5
- * stderr. Any structured metadata is passed through {@link redactSecrets}
6
- * first so tokens, passwords and signed URLs never reach the log.
7
- */
8
- /**
9
- * Deep-clones a value, replacing the value of any object key whose name looks
10
- * like a secret with a fixed mask. Non-secret values, arrays and primitives are
11
- * preserved (arrays and nested objects are walked recursively).
12
- */
13
- export declare function redactSecrets(v: unknown): unknown;
14
- export type LogLevel = 'info' | 'warn' | 'error';
15
- /** Writes a single redacted log line to stderr (never stdout). */
16
- export declare function log(level: LogLevel, msg: string, meta?: unknown): void;
package/dist/log.js DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * stderr-only structured logging with secret redaction.
3
- *
4
- * stdout is reserved for the MCP protocol stream, so every log line goes to
5
- * stderr. Any structured metadata is passed through {@link redactSecrets}
6
- * first so tokens, passwords and signed URLs never reach the log.
7
- */
8
- const SECRET_KEY = /token|password|secret|nonce|authorization|key/i;
9
- const MASK = '***REDACTED***';
10
- /**
11
- * Deep-clones a value, replacing the value of any object key whose name looks
12
- * like a secret with a fixed mask. Non-secret values, arrays and primitives are
13
- * preserved (arrays and nested objects are walked recursively).
14
- */
15
- export function redactSecrets(v) {
16
- if (Array.isArray(v)) {
17
- return v.map((item) => redactSecrets(item));
18
- }
19
- if (v !== null && typeof v === 'object') {
20
- const out = {};
21
- for (const [key, value] of Object.entries(v)) {
22
- out[key] = SECRET_KEY.test(key) ? MASK : redactSecrets(value);
23
- }
24
- return out;
25
- }
26
- return v;
27
- }
28
- /** Writes a single redacted log line to stderr (never stdout). */
29
- export function log(level, msg, meta) {
30
- let line = `[${level}] ${msg}`;
31
- if (meta !== undefined) {
32
- line += ` ${JSON.stringify(redactSecrets(meta))}`;
33
- }
34
- process.stderr.write(`${line}\n`);
35
- }