@withpica/mcp-sdk 3.22.0 → 3.23.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/dist/index.js CHANGED
@@ -245,13 +245,20 @@ class BaseResource {
245
245
  * for routes that stream a generated file (e.g. `text/csv`) rather than
246
246
  * `{success, data}`. `request()` unconditionally calls `response.json()`,
247
247
  * which throws a SyntaxError parsing CSV text; every such route needs this
248
- * method instead (see `ImportResource.getTemplate()` for the prior
249
- * precedent this generalises).
248
+ * method instead (`ImportResource.getTemplate()`, `ExportResource.catalogCsv()`).
250
249
  *
251
250
  * `fetchWithRetry` already throws `ApiError` for non-2xx responses (reading
252
251
  * the body as text), so reaching here means `response.ok`. Still guard
253
252
  * against a 2xx JSON error envelope (`{success:false}`) via content-type,
254
253
  * since a route can commit to status 200 before a later validation step.
254
+ *
255
+ * An HTML page is refused. Neither route that uses this
256
+ * (`/admin/import/template`, `/admin/works/export` CSV formats) answers
257
+ * `text/html`, so one means something in front of PICA answered (on a
258
+ * protected Vercel preview, a missing bypass header ends on Vercel's
259
+ * login page with status 200). Returning that page as the file read as a
260
+ * success (2026-10-06, pica_import_template). Locked by
261
+ * `mcp-server/src/__tests__/sdk-request-text-and-bare-fetch.test.ts`.
255
262
  */
256
263
  async requestText(method, path) {
257
264
  const url = `${this.baseUrl}${path}`;
@@ -275,6 +282,9 @@ class BaseResource {
275
282
  }
276
283
  return typeof data === "string" ? data : JSON.stringify(data);
277
284
  }
285
+ if (contentType.includes("text/html")) {
286
+ throw new ApiError(`Expected a file from ${path} but got an HTML page; the request did not reach PICA`, response.status, false);
287
+ }
278
288
  return response.text();
279
289
  }
280
290
  /**
@@ -559,6 +569,25 @@ class PeopleResource extends BaseResource {
559
569
  async bulkDelete(ids) {
560
570
  return this.request("POST", "/admin/people/bulk-delete", { ids });
561
571
  }
572
+ /**
573
+ * GET /admin/people/{id}/split-identity — what giving this person a new
574
+ * global identity would touch: who shares their current one, unattributed
575
+ * split rows naming it, open custody legs naming it, and which of their
576
+ * own identifiers equal it. Read-only.
577
+ */
578
+ async splitIdentityPreview(id) {
579
+ return this.request("GET", `/admin/people/${encodeURIComponent(id)}/split-identity`);
580
+ }
581
+ /**
582
+ * POST /admin/people/{id}/split-identity — give this person a NEW global
583
+ * identity. Body: `{ move_identifiers?, relink_split_ids?,
584
+ * clear_person_identifiers?, undo_external_ids? }`; every list defaults to
585
+ * empty (nothing moves, nothing is relinked). A refusal is a 4xx carrying
586
+ * a `SPLIT_*` code.
587
+ */
588
+ async splitIdentity(id, body) {
589
+ return this.request("POST", `/admin/people/${encodeURIComponent(id)}/split-identity`, body);
590
+ }
562
591
  /**
563
592
  * List a person's artist projects — per-stage-name Spotify/YouTube
564
593
  * identity + enrichment status (ADR-289 Wave B2 item 4, ports the
@@ -830,6 +859,7 @@ class CreditsResource extends BaseResource {
830
859
  work_id: env.work_id ?? workId,
831
860
  credit: env.data,
832
861
  allocation: env.allocation,
862
+ ...(env.person_change && { person_change: env.person_change }),
833
863
  };
834
864
  }
835
865
  /**
@@ -1208,8 +1238,13 @@ class SyncPlacementsResource extends BaseResource {
1208
1238
  : "";
1209
1239
  await this.request("DELETE", `/admin/sync-placements/${id}${query}`);
1210
1240
  }
1241
+ /**
1242
+ * Attach a citation source (POST /api/admin/sync-placements/[id]/cite).
1243
+ * This used to post to `/sources`, which has never existed, so every
1244
+ * pica_sync_placements_cite call answered 404.
1245
+ */
1211
1246
  async addSource(id, source) {
1212
- return this.request("POST", `/admin/sync-placements/${id}/sources`, source);
1247
+ return this.request("POST", `/admin/sync-placements/${id}/cite`, source);
1213
1248
  }
1214
1249
  }
1215
1250
  class WorkspaceResource extends BaseResource {
@@ -1262,9 +1297,24 @@ class AuditResource extends BaseResource {
1262
1297
  }
1263
1298
  }
1264
1299
  class MemoryResource extends BaseResource {
1300
+ /**
1301
+ * The FIRST page of memories only (newest first, the route's default page
1302
+ * size) — since 2026-10-02 the route pages. Kept for existing callers; use
1303
+ * `listPage` to reach the rest.
1304
+ */
1265
1305
  async list() {
1266
1306
  return this.request("GET", "/admin/memory");
1267
1307
  }
1308
+ /** One page of memories, most recently changed first, with the total. */
1309
+ async listPage(params) {
1310
+ const query = new URLSearchParams();
1311
+ if (params?.limit)
1312
+ query.set("limit", String(params.limit));
1313
+ if (params?.offset)
1314
+ query.set("offset", String(params.offset));
1315
+ const qs = query.toString();
1316
+ return this.requestPaginated("GET", `/admin/memory${qs ? `?${qs}` : ""}`);
1317
+ }
1268
1318
  async search(query) {
1269
1319
  return this.request("GET", `/admin/memory/search?q=${encodeURIComponent(query)}`);
1270
1320
  }
@@ -2508,7 +2558,7 @@ class ExportResource extends BaseResource {
2508
2558
  // request() unconditionally calls response.json(), which throws parsing
2509
2559
  // a CSV body — every pro/distributor/nro call 500'd with INTERNAL_ERROR
2510
2560
  // (pica_export_catalog_csv: 5/5 prod errors, ops audit 2026-07-04).
2511
- // requestText() mirrors the ImportResource.getTemplate() precedent for
2561
+ // requestText() serves ImportResource.getTemplate() too, for
2512
2562
  // the same response shape.
2513
2563
  if (params.format === "sync-pdf") {
2514
2564
  return this.request("GET", path);
@@ -2714,8 +2764,12 @@ class AssetsResource extends BaseResource {
2714
2764
  async exportAll() {
2715
2765
  return this.request("GET", "/admin/assets/export");
2716
2766
  }
2767
+ /**
2768
+ * AI-assisted price estimate for the asset's make/model. The route is
2769
+ * POST-only (it spends an AI call each time); this sent GET and got 405.
2770
+ */
2717
2771
  async marketCheck(id) {
2718
- return this.request("GET", `/admin/assets/${id}/market-check`);
2772
+ return this.request("POST", `/admin/assets/${id}/market-check`);
2719
2773
  }
2720
2774
  // --- Production-asset links (ADR-156 operating principle #3) ---
2721
2775
  // Typed edges between physical_assets and works/recordings, with
@@ -2803,14 +2857,28 @@ class ImportResource extends BaseResource {
2803
2857
  });
2804
2858
  }
2805
2859
  async getTemplate(domain) {
2806
- // Template route returns CSV as text/csv — use raw fetch
2807
- const url = `${this.baseUrl}/admin/import/template?domain=${domain}`;
2808
- const response = await fetch(url, {
2809
- headers: { Authorization: `Bearer ${this.apiKey}` },
2810
- });
2811
- if (!response.ok)
2812
- throw new Error(`Template fetch failed: ${response.status}`);
2813
- return response.text();
2860
+ // The route answers text/csv, so requestText(), not request(). It was a
2861
+ // bare fetch() with no preview-protection bypass header, so on a Vercel
2862
+ // preview it never reached the route (2026-10-06).
2863
+ return this.requestText("GET", `/admin/import/template?domain=${encodeURIComponent(domain)}`);
2864
+ }
2865
+ /**
2866
+ * Emails that reached the organisation's forwarding address, newest first,
2867
+ * with what PICA did with each (stored on arrival, waiting for review,
2868
+ * dismissed, failed). Owners and admins only.
2869
+ */
2870
+ async listForwardedEmails(options) {
2871
+ const params = new URLSearchParams();
2872
+ if (options?.limit !== undefined)
2873
+ params.set("limit", String(options.limit));
2874
+ if (options?.offset !== undefined)
2875
+ params.set("offset", String(options.offset));
2876
+ const qs = params.toString();
2877
+ return this.request("GET", `/admin/email-intake/forwarded${qs ? `?${qs}` : ""}`);
2878
+ }
2879
+ /** One forwarded email, with its text body when PICA kept one. */
2880
+ async getForwardedEmail(id) {
2881
+ return this.request("GET", `/admin/email-intake/forwarded/${encodeURIComponent(id)}`);
2814
2882
  }
2815
2883
  }
2816
2884
  // --- Documents Resource ---
@@ -3349,6 +3417,22 @@ class RecordingSplitsResource extends BaseResource {
3349
3417
  async delete(recordingId, splitId) {
3350
3418
  return this.request("DELETE", `/admin/recordings/${recordingId}/splits/${splitId}`);
3351
3419
  }
3420
+ /**
3421
+ * Edit a split and keep the route's whole envelope: `data` is the row, and
3422
+ * `previous` / `changed_fields` / `verification_cleared` sit beside it —
3423
+ * `request()` would drop them. verification_cleared is true when the edit
3424
+ * cleared the holder's confirmation.
3425
+ */
3426
+ async edit(recordingId, splitId, data) {
3427
+ return this.requestWithEnvelope("PATCH", `/admin/recordings/${recordingId}/splits/${splitId}`, data);
3428
+ }
3429
+ /**
3430
+ * End a split: sets end_date (default today) so it stops counting toward
3431
+ * the 100% total; the row is kept. Same envelope as `edit`.
3432
+ */
3433
+ async end(recordingId, splitId, data = {}) {
3434
+ return this.requestWithEnvelope("POST", `/admin/recordings/${recordingId}/splits/${splitId}/end`, data);
3435
+ }
3352
3436
  async verify(recordingId, splitId) {
3353
3437
  return this.request("POST", `/admin/recordings/${recordingId}/splits/${splitId}/verify`);
3354
3438
  }
@@ -3430,6 +3514,36 @@ class PartyClaimsResource extends BaseResource {
3430
3514
  return (res?.data ?? []);
3431
3515
  }
3432
3516
  }
3517
+ /**
3518
+ * Claim your profile: invite people to claim their profile (a preview
3519
+ * unless `confirm`), and read where people stand.
3520
+ */
3521
+ class ProfileClaimsResource extends BaseResource {
3522
+ async invite(params) {
3523
+ const res = await this.request("POST", "/admin/people/claim-invites", params);
3524
+ if (res && typeof res === "object" && "data" in res && res.data) {
3525
+ return res.data;
3526
+ }
3527
+ return (res ?? null);
3528
+ }
3529
+ async status(params) {
3530
+ const q = new URLSearchParams();
3531
+ for (const id of params?.person_ids ?? [])
3532
+ q.append("person_id", id);
3533
+ if (params?.state)
3534
+ q.set("state", params.state);
3535
+ if (params?.limit)
3536
+ q.set("limit", String(params.limit));
3537
+ const qs = q.toString();
3538
+ const res = await this.request("GET", `/admin/profile-claims${qs ? `?${qs}` : ""}`);
3539
+ const body = res;
3540
+ const inner = body?.people ? body : body?.data;
3541
+ return {
3542
+ people: inner?.people ?? [],
3543
+ truncated: inner?.truncated === true,
3544
+ };
3545
+ }
3546
+ }
3433
3547
  class ReleasesResource extends BaseResource {
3434
3548
  async list(params) {
3435
3549
  const parts = [];
@@ -4199,6 +4313,7 @@ export class PicaClient {
4199
4313
  publishers;
4200
4314
  labels;
4201
4315
  partyClaims;
4316
+ profileClaims;
4202
4317
  agreementTemplates;
4203
4318
  producerAgreements;
4204
4319
  workForHire;
@@ -4237,24 +4352,15 @@ export class PicaClient {
4237
4352
  // Registration Integrity Floor (spec 2026-07-09, Task 8).
4238
4353
  integrity;
4239
4354
  async catalogStats(params) {
4240
- const baseUrl = this.works["baseUrl"];
4241
- const apiKey = this.works["apiKey"];
4242
4355
  const qp = new URLSearchParams();
4243
4356
  if (params?.entity)
4244
4357
  qp.set("entity", params.entity);
4245
4358
  if (params?.group_by)
4246
4359
  qp.set("group_by", params.group_by);
4247
4360
  const suffix = qp.toString() ? `?${qp.toString()}` : "";
4248
- const res = await fetch(`${baseUrl}/admin/catalog/stats${suffix}`, {
4249
- headers: {
4250
- Authorization: `Bearer ${apiKey}`,
4251
- "Content-Type": "application/json",
4252
- },
4253
- });
4254
- if (!res.ok)
4255
- throw new Error(`Catalog stats failed: ${res.status}`);
4256
- const json = (await res.json());
4257
- return (json.data || json);
4361
+ // Through the shared request path (bypass header, timeout, retry, typed
4362
+ // errors) rather than a bare fetch() that skipped all four (2026-10-06).
4363
+ return this.works["request"]("GET", `/admin/catalog/stats${suffix}`);
4258
4364
  }
4259
4365
  constructor(config) {
4260
4366
  if (!config.apiKey) {
@@ -4317,6 +4423,7 @@ export class PicaClient {
4317
4423
  this.publishers = new PublishersResource(baseUrl, config.apiKey, debug);
4318
4424
  this.labels = new LabelsResource(baseUrl, config.apiKey, debug);
4319
4425
  this.partyClaims = new PartyClaimsResource(baseUrl, config.apiKey, debug);
4426
+ this.profileClaims = new ProfileClaimsResource(baseUrl, config.apiKey, debug);
4320
4427
  this.agreementTemplates = new AgreementTemplatesResource(baseUrl, config.apiKey, debug);
4321
4428
  this.producerAgreements = new ProducerAgreementsResource(baseUrl, config.apiKey, debug);
4322
4429
  this.workForHire = new WorkForHireResource(baseUrl, config.apiKey, debug);