@withpica/mcp-sdk 3.28.0 → 4.1.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/dist/index.js CHANGED
@@ -147,6 +147,35 @@ export class EntityNotFoundError extends ApiError {
147
147
  function isRetryableStatus(status) {
148
148
  return status === 429 || status === 502 || status === 503 || status === 504;
149
149
  }
150
+ /**
151
+ * A write (POST, PATCH) is resent only when the server said it did nothing:
152
+ * 429 and 503 are refusals before the route runs. A timeout, a dropped
153
+ * connection, a 502 or a 504 can each follow a write that DID happen, and
154
+ * resending it repeats the write. On prod 2026-10-29 a 94 s works create
155
+ * timed out at 30 s; the resend found the song the first attempt had just
156
+ * made and answered WORK_ALREADY_EXISTS naming it, and the agent then made by
157
+ * hand the recording the first attempt was still about to make (ops 198deea1).
158
+ * GET, PUT and DELETE are idempotent and keep every retry.
159
+ */
160
+ function isWriteMethod(method) {
161
+ const m = (method ?? "GET").toUpperCase();
162
+ return m === "POST" || m === "PATCH";
163
+ }
164
+ function isRefusedBeforeRunning(status) {
165
+ return status === 429 || status === 503;
166
+ }
167
+ /**
168
+ * A write whose outcome is unknown: it timed out, lost its connection, or got
169
+ * a 502/504, and was not resent because it may have been applied. Look before
170
+ * trying again.
171
+ */
172
+ export class WriteOutcomeUnknownError extends ApiError {
173
+ code = "WRITE_OUTCOME_UNKNOWN";
174
+ constructor(method, path, cause, status = 0) {
175
+ super(`${method} ${path} did not answer (${cause}). It may still have been applied, so it was not sent again: check whether it took effect (for example, query for what it creates) before retrying.`, status, false);
176
+ this.name = "WriteOutcomeUnknownError";
177
+ }
178
+ }
150
179
  function getTimeoutForPath(path) {
151
180
  return LONG_TIMEOUT_PATTERNS.some((p) => path.includes(p))
152
181
  ? LONG_TIMEOUT_MS
@@ -255,6 +284,15 @@ class BaseResource {
255
284
  }
256
285
  async fetchWithRetry(url, init, timeoutMs) {
257
286
  let lastError;
287
+ const write = isWriteMethod(init.method);
288
+ const pathOf = () => {
289
+ try {
290
+ return new URL(url).pathname;
291
+ }
292
+ catch {
293
+ return url;
294
+ }
295
+ };
258
296
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
259
297
  try {
260
298
  const response = await this.fetchWithTimeout(url, init, timeoutMs);
@@ -266,6 +304,12 @@ class BaseResource {
266
304
  if (response.status === 401 || response.status === 403) {
267
305
  throw new ApiError(`Authentication failed: ${response.status} ${errorText}`, response.status, false);
268
306
  }
307
+ // A write that may have run is never resent (isWriteMethod).
308
+ if (write &&
309
+ isRetryableStatus(response.status) &&
310
+ !isRefusedBeforeRunning(response.status)) {
311
+ throw new WriteOutcomeUnknownError(init.method ?? "POST", pathOf(), `${response.status} ${errorText.slice(0, 120)}`, response.status);
312
+ }
269
313
  // Retryable errors — retry with backoff
270
314
  if (isRetryableStatus(response.status) && attempt < MAX_RETRIES) {
271
315
  const retryAfter = response.headers.get("retry-after");
@@ -291,6 +335,10 @@ class BaseResource {
291
335
  catch (err) {
292
336
  if (err instanceof ApiError && !err.retryable)
293
337
  throw err;
338
+ // Timed out or lost the connection: a write may have run.
339
+ if (write && !(err instanceof CatalogueNotHonouredError)) {
340
+ throw new WriteOutcomeUnknownError(init.method ?? "POST", pathOf(), err.message);
341
+ }
294
342
  lastError = err;
295
343
  if (attempt < MAX_RETRIES) {
296
344
  const delayMs = RETRY_BASE_MS * Math.pow(2, attempt);
@@ -577,6 +625,14 @@ class WorksResource extends BaseResource {
577
625
  async delete(id) {
578
626
  return this.request("DELETE", `/admin/works/${id}`);
579
627
  }
628
+ /**
629
+ * Bring back a removed song (removing never deletes one). 409 when a live
630
+ * song now holds its title and artist or its tunecode, or when it was
631
+ * merged into another song.
632
+ */
633
+ async restore(id) {
634
+ return this.request("POST", `/admin/works/${id}/restore`);
635
+ }
580
636
  /**
581
637
  * @deprecated `works` has no `is_verified` column, so PATCH /admin/works/[id]
582
638
  * refuses this body (VALIDATION_ERROR "unknown field 'is_verified'") on every
@@ -718,8 +774,18 @@ class PeopleResource extends BaseResource {
718
774
  async update(id, updates) {
719
775
  return this.request("PATCH", `/admin/people/${id}`, updates);
720
776
  }
721
- async delete(id) {
722
- await this.request("DELETE", `/admin/people/${id}`);
777
+ /**
778
+ * Remove a person. Someone who still has credits is refused (409
779
+ * PERSON_HOLDS_CREDITS, with what they hold) unless `moveTo` names the live
780
+ * person their credits move to; the removal is then a person merge.
781
+ */
782
+ async delete(id, opts = {}) {
783
+ const qs = opts.moveTo ? `?move_to=${encodeURIComponent(opts.moveTo)}` : "";
784
+ return this.request("DELETE", `/admin/people/${id}${qs}`);
785
+ }
786
+ /** Bring back a removed person. 409 when they were merged into another. */
787
+ async restore(id) {
788
+ return this.request("POST", `/admin/people/${id}/restore`);
723
789
  }
724
790
  async enrichFromISNI(id, isni) {
725
791
  return this.request("POST", `/admin/people/${id}/enrich/isni`, {
@@ -1125,6 +1191,8 @@ class AudioFilesResource extends BaseResource {
1125
1191
  queryParams.set("limit", String(params.limit));
1126
1192
  if (params?.offset)
1127
1193
  queryParams.set("offset", String(params.offset));
1194
+ if (params?.cursor)
1195
+ queryParams.set("cursor", params.cursor);
1128
1196
  const query = queryParams.toString();
1129
1197
  return this.request("GET", `/admin/audio-files${query ? `?${query}` : ""}`);
1130
1198
  }
@@ -1471,9 +1539,21 @@ class WorkspaceResource extends BaseResource {
1471
1539
  return this.request("GET", `/admin/events?limit=${limit || 20}`);
1472
1540
  }
1473
1541
  }
1474
- // ---------------------------------------------------------------------------
1475
- // ADR-231 — User-initiated asset sharing (creator MCP)
1476
- // ---------------------------------------------------------------------------
1542
+ /** What has been removed from the catalogue and can be brought back. */
1543
+ class RemovedResource extends BaseResource {
1544
+ /** One page of removed songs (the default) or people, newest changed first. */
1545
+ async list(params = {}) {
1546
+ const qp = new URLSearchParams();
1547
+ if (params.kind)
1548
+ qp.set("kind", params.kind);
1549
+ if (params.limit !== undefined)
1550
+ qp.set("limit", String(params.limit));
1551
+ if (params.offset !== undefined)
1552
+ qp.set("offset", String(params.offset));
1553
+ const q = qp.toString();
1554
+ return this.request("GET", `/admin/removed${q ? `?${q}` : ""}`);
1555
+ }
1556
+ }
1477
1557
  class ShareSendResource extends BaseResource {
1478
1558
  async send(args) {
1479
1559
  return this.request("POST", "/mcp/share-send", args);
@@ -3474,20 +3554,30 @@ class UsersResource extends BaseResource {
3474
3554
  async getMyProfile() {
3475
3555
  return this.request("GET", "/admin/users/profile");
3476
3556
  }
3557
+ /**
3558
+ * Write your bio: the biography on your own person record (one bio per
3559
+ * person). Refused 409 NO_LINKED_PERSON when your account has no person.
3560
+ */
3477
3561
  async setBio(bio) {
3478
- return this.request("POST", "/admin/users/bio", {
3479
- bio,
3480
- });
3562
+ return this.request("POST", "/admin/users/bio", { bio });
3563
+ }
3564
+ /**
3565
+ * Settle a waiting `bio_choice`: "current" keeps your bio, "earlier" uses
3566
+ * the earlier one. The text not kept goes to `set_aside_bios`, never
3567
+ * deleted. Refused 409 NO_CHOICE_WAITING when nothing is waiting.
3568
+ */
3569
+ async chooseBio(keep) {
3570
+ return this.request("POST", "/admin/users/bio", { choose: keep });
3481
3571
  }
3482
3572
  async setAvatarUrl(avatarUrl) {
3483
3573
  return this.request("POST", "/admin/users/avatar", { avatar_url: avatarUrl });
3484
3574
  }
3485
3575
  /**
3486
- * Copy `people.biography` into `user_profiles.bio`. Returns the new
3487
- * bio on success. On `would_overwrite` (existing bio + overwrite
3488
- * false), surfaces both the existing and proposed bios so the agent
3489
- * can present a yes/no to the user before re-calling with
3490
- * `overwrite: true`.
3576
+ * @deprecated Retired: there is one bio now. Your profile bio IS the
3577
+ * biography on your own person record, so there is nothing to import, and
3578
+ * the server answers 410 RETIRED. Read the bio with `getMyProfile` and
3579
+ * change it with `setBio`. Kept so an existing caller gets that message
3580
+ * rather than a missing method; removed in the next major.
3491
3581
  */
3492
3582
  async importBioFromPerson(params) {
3493
3583
  return this.request("POST", "/admin/users/import-bio-from-person", params ?? {});
@@ -3716,6 +3806,19 @@ class ShareTraceResource extends BaseResource {
3716
3806
  return this.request("GET", `/admin/share-trace?${params.toString()}`);
3717
3807
  }
3718
3808
  }
3809
+ class SongAccessResource extends BaseResource {
3810
+ /** GET /admin/works/{id}/access — who can see one of your songs. */
3811
+ async list(workId) {
3812
+ return this.request("GET", `/admin/works/${encodeURIComponent(workId)}/access`);
3813
+ }
3814
+ /**
3815
+ * POST /admin/works/{id}/access — end one line. A removal that took
3816
+ * nothing off throws (500 ACCESS_NOT_REMOVED).
3817
+ */
3818
+ async remove(workId, key) {
3819
+ return this.request("POST", `/admin/works/${encodeURIComponent(workId)}/access`, { key });
3820
+ }
3821
+ }
3719
3822
  class RecordingSplitsResource extends BaseResource {
3720
3823
  async list(recordingId) {
3721
3824
  return this.request("GET", `/admin/recordings/${recordingId}/splits`);
@@ -4508,7 +4611,8 @@ class ShareLinksResource extends BaseResource {
4508
4611
  }
4509
4612
  /**
4510
4613
  * Update a narrow subset of share-link fields (name, description,
4511
- * personal_message, expires_at, max_views, is_active, password).
4614
+ * personal_message, expires_at, max_views, is_active, password,
4615
+ * revoke_recipient_ids).
4512
4616
  * Permission-bearing fields are not updatable post-create — revoke
4513
4617
  * and recreate to change permissions.
4514
4618
  */
@@ -4817,6 +4921,7 @@ export class PicaClient {
4817
4921
  // ADR-232 — share trace aggregator + count explain registry +
4818
4922
  // audio pipeline status read.
4819
4923
  shareTrace;
4924
+ songAccess;
4820
4925
  countExplain;
4821
4926
  audioPipelineStatus;
4822
4927
  publishers;
@@ -4861,6 +4966,7 @@ export class PicaClient {
4861
4966
  groups;
4862
4967
  assistantInteractions;
4863
4968
  shareSend;
4969
+ removed;
4864
4970
  accessSimulate;
4865
4971
  approvals;
4866
4972
  // Registration Integrity Floor (spec 2026-07-09, Task 8).
@@ -4933,6 +5039,7 @@ export class PicaClient {
4933
5039
  this.recordingSplits = new RecordingSplitsResource(baseUrl, config.apiKey, debug);
4934
5040
  this.recordingCredits = new RecordingCreditsResource(baseUrl, config.apiKey, debug);
4935
5041
  this.shareTrace = new ShareTraceResource(baseUrl, config.apiKey, debug);
5042
+ this.songAccess = new SongAccessResource(baseUrl, config.apiKey, debug);
4936
5043
  this.countExplain = new CountExplainResource(baseUrl, config.apiKey, debug);
4937
5044
  this.audioPipelineStatus = new AudioPipelineStatusResource(baseUrl, config.apiKey, debug);
4938
5045
  this.publishers = new PublishersResource(baseUrl, config.apiKey, debug);
@@ -4972,6 +5079,7 @@ export class PicaClient {
4972
5079
  this.groups = new GroupsResource(baseUrl, config.apiKey, debug);
4973
5080
  this.assistantInteractions = new AssistantInteractionsResource(baseUrl, config.apiKey, debug);
4974
5081
  this.shareSend = new ShareSendResource(baseUrl, config.apiKey, debug);
5082
+ this.removed = new RemovedResource(baseUrl, config.apiKey, debug);
4975
5083
  this.accessSimulate = new AccessSimulateResource(baseUrl, config.apiKey, debug);
4976
5084
  this.approvals = new ApprovalsResource(baseUrl, config.apiKey, debug);
4977
5085
  this.integrity = new IntegrityResource(baseUrl, config.apiKey, debug);