@withpica/mcp-server 2.98.0 → 2.100.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.
Files changed (134) hide show
  1. package/CHANGELOG.md +254 -0
  2. package/README.md +8 -8
  3. package/dist/lib/changelog.generated.d.ts +2 -2
  4. package/dist/lib/changelog.generated.d.ts.map +1 -1
  5. package/dist/lib/changelog.generated.js +2 -2
  6. package/dist/lib/changelog.generated.js.map +1 -1
  7. package/dist/prompts/creator-question-atlas.d.ts.map +1 -1
  8. package/dist/prompts/creator-question-atlas.js +25 -2
  9. package/dist/prompts/creator-question-atlas.js.map +1 -1
  10. package/dist/prompts/index.js +2 -2
  11. package/dist/resources/agent-guide.d.ts.map +1 -1
  12. package/dist/resources/agent-guide.js +38 -7
  13. package/dist/resources/agent-guide.js.map +1 -1
  14. package/dist/resources/index.d.ts.map +1 -1
  15. package/dist/resources/index.js +0 -3
  16. package/dist/resources/index.js.map +1 -1
  17. package/dist/resources/required-schemas.generated.d.ts +17 -11
  18. package/dist/resources/required-schemas.generated.d.ts.map +1 -1
  19. package/dist/resources/required-schemas.generated.js +30 -15
  20. package/dist/resources/required-schemas.generated.js.map +1 -1
  21. package/dist/resources/required-schemas.source.d.ts.map +1 -1
  22. package/dist/resources/required-schemas.source.js +15 -11
  23. package/dist/resources/required-schemas.source.js.map +1 -1
  24. package/dist/server-instructions.d.ts +2 -2
  25. package/dist/server-instructions.d.ts.map +1 -1
  26. package/dist/server-instructions.js +2 -2
  27. package/dist/server-instructions.js.map +1 -1
  28. package/dist/skills/skills.generated.js +8 -8
  29. package/dist/skills/skills.generated.js.map +1 -1
  30. package/dist/tools/api-error-body.d.ts +16 -4
  31. package/dist/tools/api-error-body.d.ts.map +1 -1
  32. package/dist/tools/api-error-body.js +40 -1
  33. package/dist/tools/api-error-body.js.map +1 -1
  34. package/dist/tools/approvals.d.ts.map +1 -1
  35. package/dist/tools/approvals.js +12 -1
  36. package/dist/tools/approvals.js.map +1 -1
  37. package/dist/tools/assets.d.ts.map +1 -1
  38. package/dist/tools/assets.js +15 -3
  39. package/dist/tools/assets.js.map +1 -1
  40. package/dist/tools/collaborators.d.ts.map +1 -1
  41. package/dist/tools/collaborators.js +27 -3
  42. package/dist/tools/collaborators.js.map +1 -1
  43. package/dist/tools/credits.d.ts +4 -1
  44. package/dist/tools/credits.d.ts.map +1 -1
  45. package/dist/tools/credits.js +204 -137
  46. package/dist/tools/credits.js.map +1 -1
  47. package/dist/tools/deprecations.d.ts +65 -7
  48. package/dist/tools/deprecations.d.ts.map +1 -1
  49. package/dist/tools/deprecations.js +96 -96
  50. package/dist/tools/deprecations.js.map +1 -1
  51. package/dist/tools/discovery.d.ts +28 -2
  52. package/dist/tools/discovery.d.ts.map +1 -1
  53. package/dist/tools/discovery.js +74 -29
  54. package/dist/tools/discovery.js.map +1 -1
  55. package/dist/tools/documents.d.ts +2 -2
  56. package/dist/tools/documents.d.ts.map +1 -1
  57. package/dist/tools/documents.js +8 -92
  58. package/dist/tools/documents.js.map +1 -1
  59. package/dist/tools/duplicates.d.ts +15 -0
  60. package/dist/tools/duplicates.d.ts.map +1 -1
  61. package/dist/tools/duplicates.js +285 -1
  62. package/dist/tools/duplicates.js.map +1 -1
  63. package/dist/tools/import-documents.d.ts +1 -0
  64. package/dist/tools/import-documents.d.ts.map +1 -1
  65. package/dist/tools/import-documents.js +67 -0
  66. package/dist/tools/import-documents.js.map +1 -1
  67. package/dist/tools/import.d.ts.map +1 -1
  68. package/dist/tools/import.js +14 -0
  69. package/dist/tools/import.js.map +1 -1
  70. package/dist/tools/index.d.ts +2 -1
  71. package/dist/tools/index.d.ts.map +1 -1
  72. package/dist/tools/index.js +38 -0
  73. package/dist/tools/index.js.map +1 -1
  74. package/dist/tools/integrity.d.ts.map +1 -1
  75. package/dist/tools/integrity.js +65 -8
  76. package/dist/tools/integrity.js.map +1 -1
  77. package/dist/tools/memory.d.ts.map +1 -1
  78. package/dist/tools/memory.js +24 -6
  79. package/dist/tools/memory.js.map +1 -1
  80. package/dist/tools/metadata.d.ts.map +1 -1
  81. package/dist/tools/metadata.js +83 -121
  82. package/dist/tools/metadata.js.map +1 -1
  83. package/dist/tools/notes.d.ts +3 -4
  84. package/dist/tools/notes.d.ts.map +1 -1
  85. package/dist/tools/notes.js +3 -39
  86. package/dist/tools/notes.js.map +1 -1
  87. package/dist/tools/people.d.ts +54 -22
  88. package/dist/tools/people.d.ts.map +1 -1
  89. package/dist/tools/people.js +236 -27
  90. package/dist/tools/people.js.map +1 -1
  91. package/dist/tools/profile-claims.d.ts +31 -0
  92. package/dist/tools/profile-claims.d.ts.map +1 -0
  93. package/dist/tools/profile-claims.js +188 -0
  94. package/dist/tools/profile-claims.js.map +1 -0
  95. package/dist/tools/projects.d.ts +3 -4
  96. package/dist/tools/projects.d.ts.map +1 -1
  97. package/dist/tools/projects.js +3 -27
  98. package/dist/tools/projects.js.map +1 -1
  99. package/dist/tools/public-filter.d.ts.map +1 -1
  100. package/dist/tools/public-filter.js +4 -7
  101. package/dist/tools/public-filter.js.map +1 -1
  102. package/dist/tools/recovery-hints.d.ts.map +1 -1
  103. package/dist/tools/recovery-hints.js +164 -30
  104. package/dist/tools/recovery-hints.js.map +1 -1
  105. package/dist/tools/releases.d.ts +2 -4
  106. package/dist/tools/releases.d.ts.map +1 -1
  107. package/dist/tools/releases.js +6 -210
  108. package/dist/tools/releases.js.map +1 -1
  109. package/dist/tools/rights.d.ts +9 -0
  110. package/dist/tools/rights.d.ts.map +1 -1
  111. package/dist/tools/rights.js +199 -3
  112. package/dist/tools/rights.js.map +1 -1
  113. package/dist/tools/royalties.d.ts +5 -5
  114. package/dist/tools/royalties.d.ts.map +1 -1
  115. package/dist/tools/royalties.js +2 -53
  116. package/dist/tools/royalties.js.map +1 -1
  117. package/dist/tools/shows.d.ts.map +1 -1
  118. package/dist/tools/shows.js +25 -2
  119. package/dist/tools/shows.js.map +1 -1
  120. package/dist/tools/split-sheets.d.ts +8 -0
  121. package/dist/tools/split-sheets.d.ts.map +1 -1
  122. package/dist/tools/split-sheets.js +178 -1
  123. package/dist/tools/split-sheets.js.map +1 -1
  124. package/dist/tools/statements.js +1 -1
  125. package/dist/tools/statements.js.map +1 -1
  126. package/dist/tools/version-guard.d.ts +16 -4
  127. package/dist/tools/version-guard.d.ts.map +1 -1
  128. package/dist/tools/version-guard.js +22 -8
  129. package/dist/tools/version-guard.js.map +1 -1
  130. package/dist/tools/works.d.ts.map +1 -1
  131. package/dist/tools/works.js +24 -11
  132. package/dist/tools/works.js.map +1 -1
  133. package/package.json +2 -2
  134. package/server.json +2 -2
@@ -73,6 +73,55 @@ function recordingCreditExists(err) {
73
73
  return typedError("RECORDING_CREDIT_EXISTS", body.message ??
74
74
  `This person is already credited in this role on this recording${creditId ? ` (credit ${creditId})` : ""} with different details. Nothing was changed — one person holds one credit per role. Edit that credit with pica_credit_update (e.g. instrument: "drums, guitar") instead of adding a second.`, creditId ? { existing_credit_id: creditId } : undefined);
75
75
  }
76
+ /**
77
+ * The columns that record a master credit's attestation. Mirrors
78
+ * RECORDING_CREDIT_ATTESTATION_FIELDS in
79
+ * lib/constants/recording-credit-attestation.ts, which
80
+ * POST /api/admin/recordings/[id]/credits and the bulk route refuse with the
81
+ * same code; this package cannot import the app.
82
+ */
83
+ const RECORDING_ATTESTATION_FIELDS = [
84
+ "attestation_status",
85
+ "attested_at",
86
+ "attested_by",
87
+ "attestation_source_type",
88
+ "attestation_source_id",
89
+ ];
90
+ /**
91
+ * Refuse a pica_recording_credits_update call that carries any attestation
92
+ * column — at the top level (single form) or on any credits[] entry (batch
93
+ * form). Presence is what counts: "pending" is refused too, so no caller is
94
+ * left believing it controls the field. Returns null when the call is clean.
95
+ */
96
+ function refuseRecordingAttestation(args) {
97
+ const has = (o) => {
98
+ if (!o || typeof o !== "object")
99
+ return null;
100
+ for (const f of RECORDING_ATTESTATION_FIELDS) {
101
+ if (Object.hasOwn(o, f))
102
+ return f;
103
+ }
104
+ return null;
105
+ };
106
+ const guidance = "Every credit starts as pending and only the credited person can " +
107
+ "confirm it. Remove it and retry (put any source evidence in notes), " +
108
+ "then ask them to confirm with pica_collaborators_invite (recording_id) " +
109
+ "or pica_credits_send_for_attestation on the recording's song.";
110
+ const topField = has(args);
111
+ if (topField) {
112
+ return typedError("ATTESTATION_NOT_SETTABLE", `${topField} can't be set by this tool — nothing was saved. ${guidance}`, { field: topField });
113
+ }
114
+ if (Array.isArray(args.credits)) {
115
+ for (let i = 0; i < args.credits.length; i++) {
116
+ const field = has(args.credits[i]);
117
+ if (field) {
118
+ return typedError("ATTESTATION_NOT_SETTABLE", `credits[${i}] carries ${field}, which this tool can't set — ` +
119
+ `nothing was saved. ${guidance}`, { field, index: i });
120
+ }
121
+ }
122
+ }
123
+ return null;
124
+ }
76
125
  export class CreditsTools {
77
126
  pica;
78
127
  constructor(pica) {
@@ -154,8 +203,10 @@ export class CreditsTools {
154
203
  "'publisher' (their publishing share is that entry's split) — " +
155
204
  "there is no separate publisher field. is_primary marks the " +
156
205
  "primary attribution and applies to composition credits only " +
157
- "(recording credits have no primary flag). You can also set notes " +
158
- "and attestation_status. Once it's saved, it's worth asking the " +
206
+ "(recording credits have no primary flag). You can also set notes. " +
207
+ "Attestation is not settable here: every credit this saves starts " +
208
+ "as pending, and only the credited person confirms it — send it " +
209
+ "with pica_credits_send_for_attestation. Once it's saved, it's worth asking the " +
159
210
  "user: send these to the collaborators to verify, or generate a " +
160
211
  "split sheet for sign-off? Is anyone here on a work-for-hire or " +
161
212
  "producer agreement (a producer takes points off the top of " +
@@ -221,11 +272,6 @@ export class CreditsTools {
221
272
  type: "string",
222
273
  description: "Free-text notes about this credit (e.g., recording-session context, contribution specifics).",
223
274
  },
224
- attestation_status: {
225
- type: "string",
226
- enum: ["pending", "attested", "disputed", "declined"],
227
- description: "Attestation status. Default: pending. Service-managed fields attested_at / attested_by are set by the attestation workflow, not this tool.",
228
- },
229
275
  },
230
276
  required: ["person_id", "role", "splits"],
231
277
  },
@@ -237,7 +283,7 @@ export class CreditsTools {
237
283
  nextSteps: [
238
284
  {
239
285
  tool: "pica_credits_list",
240
- reason: "Read the credits back to confirm every advertised field (is_primary, notes, attestation_status) actually persisted. Silent persistence drops are real — never trust the write response alone.",
286
+ reason: "Read the credits back to confirm every advertised field (is_primary, notes) actually persisted. Silent persistence drops are real — never trust the write response alone.",
241
287
  when: "on_success",
242
288
  },
243
289
  {
@@ -286,6 +332,10 @@ export class CreditsTools {
286
332
  "Arranger, Programmer, Other. Look up the person first with " +
287
333
  "pica_people_query; if they're not in the catalog, leave " +
288
334
  "person_id empty and their name still shows via credited_name. " +
335
+ "Attestation is not settable here: every credit starts as " +
336
+ "pending and only the credited person confirms it — ask them " +
337
+ "with pica_collaborators_invite (recording_id) or " +
338
+ "pica_credits_send_for_attestation on the recording's song. " +
289
339
  "After saving, confirm it stuck with pica_recordings_inspect " +
290
340
  "(sections: ['recording_credits']). → then: " +
291
341
  "pica_recordings_inspect (verify saved), pica_people_query " +
@@ -329,11 +379,6 @@ export class CreditsTools {
329
379
  type: "string",
330
380
  description: "Optional free-text notes about this credit (recording-session context, role specifics).",
331
381
  },
332
- attestation_status: {
333
- type: "string",
334
- enum: ["pending", "attested", "disputed", "declined"],
335
- description: "Optional starting attestation status — master credits are attestable just like composition credits. Defaults to 'pending'. Use pica_credits_send_for_attestation to ask the credited person to confirm; attested_at / attested_by are set by the attestation workflow, not this tool.",
336
- },
337
382
  instrument: {
338
383
  type: "string",
339
384
  description: "Optional instrument(s) for this credit, e.g. 'drums' or 'drums, bass' (one credit per person — list multiple instruments on the same row). Stored on recording_credits.instrument and rendered in /inspect's contributors table.",
@@ -408,96 +453,6 @@ export class CreditsTools {
408
453
  },
409
454
  executor: withBillingGate(this.pica, "recording credit update", this.updateRecordingCredits.bind(this)),
410
455
  },
411
- // ADR-264 WS4 prerequisite — bulk recording credits. One album's
412
- // production credits stops being 30 sequential per-recording calls.
413
- {
414
- definition: {
415
- name: "pica_recording_credits_bulk_update",
416
- scope: "write:catalog",
417
- audiences: ["stdio", "http"],
418
- tier: "write",
419
- description: "Apply one set of recording credits across many recordings in a " +
420
- "single call. Use when the user asks: 'credit the producer on " +
421
- "the whole album', 'add these credits to every track', 'backfill " +
422
- "the engineer across these recordings'. Target either a release " +
423
- "(release_id — every recording on its tracklist) or an explicit " +
424
- "recording_ids list, never both. The same credit set is applied " +
425
- "to EVERY target recording; credits that already exist on a " +
426
- "recording (same credited name + role) are skipped, never " +
427
- "overwritten, so re-running is safe. Set dry_run: true first to " +
428
- "preview exactly which rows would be created vs skipped. Caps: " +
429
- "100 recordings, 50 credits, 500 total rows per call. Per-track " +
430
- "differences (a featured artist on track 3 only) go via " +
431
- "pica_recording_credits_update on that recording. → then: " +
432
- "pica_recordings_inspect (verify saved)",
433
- workflows: ["recording-credits-required"],
434
- vernacular_kind: "specialist",
435
- vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
436
- previewMode: "dry_run",
437
- inputSchema: {
438
- type: "object",
439
- properties: {
440
- release_id: {
441
- type: "string",
442
- description: "UUID of a release — targets every recording on its tracklist. Provide exactly one of release_id / recording_ids.",
443
- },
444
- recording_ids: {
445
- type: "array",
446
- items: { type: "string" },
447
- description: "Explicit recording UUIDs to credit (max 100). Provide exactly one of release_id / recording_ids.",
448
- },
449
- credits: {
450
- type: "array",
451
- description: "The credit set applied to EVERY target recording (max 50). Each entry needs credited_name + role; person_id links an existing person record (resolve via pica_people_query first).",
452
- items: {
453
- type: "object",
454
- properties: {
455
- credited_name: {
456
- type: "string",
457
- description: "The displayed credit name — the contributor's professional name as it should appear on the recording.",
458
- },
459
- role: {
460
- type: "string",
461
- enum: [...RECORDING_CREDIT_ROLES],
462
- description: "Recording credit role from the 16-entry RECORDING_CREDIT_ROLES enum. Songwriting roles (Writer, Composer, Lyricist) belong on the work via pica_credits_update.",
463
- },
464
- person_id: {
465
- type: "string",
466
- description: "Optional UUID of an existing person record in the catalog. Leave empty if the contributor has no person record — credited_name still shows.",
467
- },
468
- split_percentage: {
469
- type: "number",
470
- description: "Optional percentage share for this credit (0-100), applied identically on every target recording.",
471
- },
472
- split_type: {
473
- type: "string",
474
- description: "Optional free-form split type label (e.g. 'master', 'producer_points').",
475
- },
476
- notes: {
477
- type: "string",
478
- description: "Optional free-text notes attached to each created credit row.",
479
- },
480
- },
481
- required: ["credited_name", "role"],
482
- },
483
- },
484
- dry_run: {
485
- type: "boolean",
486
- description: "When true, returns the per-row would_create / would_skip_existing decision set without persisting anything. Preview before committing.",
487
- },
488
- },
489
- required: ["credits"],
490
- },
491
- nextSteps: [
492
- {
493
- tool: "pica_recordings_inspect",
494
- reason: "Read recording_credits back via sections:['recording_credits'] on a sample recording to confirm the rows persisted.",
495
- when: "on_success",
496
- },
497
- ],
498
- },
499
- executor: withBillingGate(this.pica, "bulk recording credit update", this.bulkUpdateRecordingCredits.bind(this)),
500
- },
501
456
  // ADR-232 — atomic credit primitives. Composable verbs over the same
502
457
  // substrate as pica_credits_update / pica_recording_credits_update,
503
458
  // but discrete: add one row, remove one row, no replace-semantics.
@@ -631,8 +586,8 @@ export class CreditsTools {
631
586
  description: "(recordings only) Displayed credit name (NOT NULL on recording_credits). Required for recordings.",
632
587
  },
633
588
  share_pct: {
634
- type: "number",
635
- description: "Percentage share (0-100). Maps to split_percentage on work_collaborators (composition) / recording_credits (master). Omit when unknown — it is stored as NULL (unknown), never as 0; the health plan then reports the work as incomplete rather than rescaling the others. REQUIRED for credit_type 'owner' (master ownership is recorded as given, never assumed 100).",
589
+ type: ["number", "null"],
590
+ description: "Percentage share (0-100). Maps to split_percentage on work_collaborators (composition) / recording_credits (master). Omit it (or send null) when unknown — it is stored as NULL (unknown), never as 0; the health plan then reports the work as incomplete rather than rescaling the others. REQUIRED for credit_type 'owner' (master ownership is recorded as given, never assumed 100).",
636
591
  },
637
592
  notes: {
638
593
  type: "string",
@@ -668,14 +623,20 @@ export class CreditsTools {
668
623
  audiences: ["stdio", "http"],
669
624
  tier: "write",
670
625
  description: "Edit ONE existing credit in place — change a writer's share, an " +
671
- "instrumentalist's instrument, a role, the displayed name or the " +
672
- "notes — leaving every other credit exactly as it is. Use when the " +
673
- "user asks: 'change Mary's split to 40', 'set Charlie's instrument " +
674
- "to bass', 'fix the spelling of this credit'. This is the verb " +
675
- "between pica_credit_add and pica_credit_remove: it never " +
676
- "replaces the set (that is pica_credits_update) and never " +
677
- "deletes-and-recreates, so the credit keeps its id and " +
678
- "attestation state. Find the credit_id with pica_credits_list. " +
626
+ "instrumentalist's instrument, a role, the displayed name, the " +
627
+ "notes, or WHO the credit is for (person_id) — leaving every " +
628
+ "other credit exactly as it is. Use when the user asks: 'change " +
629
+ "Mary's split to 40', 'set Charlie's instrument to bass', 'fix " +
630
+ "the spelling of this credit', 'link the Brian Lucey credit to " +
631
+ "his new person record'. This is the verb between pica_credit_add " +
632
+ "and pica_credit_remove: it never replaces the set (that is " +
633
+ "pica_credits_update) and never deletes-and-recreates, so the " +
634
+ "credit keeps its id. Its attestation state is kept too, except " +
635
+ "when person_id changes WHO it is for: a confirmed, disputed or " +
636
+ "declined credit then goes back to pending, because the newly " +
637
+ "linked person gave none of those answers (the response's " +
638
+ "person_change says so). " +
639
+ "Find the credit_id with pica_credits_list. " +
679
640
  "Writer-split closure is NOT enforced on a single edit — re-split " +
680
641
  "a song one writer at a time; the response's `allocation.status` " +
681
642
  "(closed / open / incomplete) tells you whether the set still " +
@@ -715,6 +676,14 @@ export class CreditsTools {
715
676
  type: ["string", "null"],
716
677
  description: "Free-text notes. Pass null to clear.",
717
678
  },
679
+ person_id: {
680
+ type: "string",
681
+ description: "Change who this credit is for (UUID from pica_people_query) — link a name-only credit to a person record, or move a credit linked to the wrong person. The person must be in this organisation and not deleted; if they already hold this role on the song or recording, it is refused (CREDIT_PERSON_CONFLICT) rather than making a second credit. A confirmed, disputed or declined credit goes back to pending.",
682
+ },
683
+ confirm_name_mismatch: {
684
+ type: "boolean",
685
+ description: "Only with person_id on a name-only credit: set true after the user confirms the person is the one credited even though the names differ (e.g. a stage name). Without it a mismatch is refused (PERSON_NAME_MISMATCH).",
686
+ },
718
687
  },
719
688
  required: ["work_id", "credit_id"],
720
689
  },
@@ -799,19 +768,28 @@ export class CreditsTools {
799
768
  credit_ids: creditIds,
800
769
  reminder_type: reminderType,
801
770
  });
802
- const delivered = (result.results || []).filter((r) => r.delivered);
803
- const notPicaUsers = (result.results || []).filter((r) => r.reason === "not_a_pica_user");
804
- const failed = (result.results || []).filter((r) => !r.delivered && r.reason !== "not_a_pica_user");
771
+ const rows = (result.results || []);
772
+ const name = (r) => r.person_name || r.person_id;
773
+ const of = (o) => rows.filter((r) => r.outcome === o);
774
+ const sent = of("sent");
775
+ const held = of("held");
776
+ const notPicaUsers = rows.filter((r) => r.outcome === "no_channel" && r.reason === "not_a_pica_user");
777
+ const noEmail = rows.filter((r) => r.outcome === "no_channel" && r.reason !== "not_a_pica_user");
778
+ const failed = of("failed");
805
779
  const summary = [
806
- `Sent attestation prompt to ${delivered.length}/${result.results?.length ?? 0} credited people`,
807
- delivered.length > 0
808
- ? `Delivered: ${delivered.map((r) => r.person_name || r.person_id).join(", ")}`
780
+ `Asked ${sent.length + held.length}/${rows.length} credited people`,
781
+ sent.length > 0 ? `Sent to: ${sent.map(name).join(", ")}` : null,
782
+ held.length > 0
783
+ ? `Email held for approval (not delivered yet): ${held.map(name).join(", ")}`
809
784
  : null,
810
785
  notPicaUsers.length > 0
811
- ? `Not PICA users (use pica_collaborators_invite): ${notPicaUsers.map((r) => r.person_name || r.person_id).join(", ")}`
786
+ ? `Not PICA users (use pica_collaborators_invite): ${notPicaUsers.map(name).join(", ")}`
787
+ : null,
788
+ noEmail.length > 0
789
+ ? `No email on file (add one to their person record): ${noEmail.map(name).join(", ")}`
812
790
  : null,
813
791
  failed.length > 0
814
- ? `Failed: ${failed.map((r) => r.person_name || r.person_id).join(", ")}`
792
+ ? `Send failed: ${failed.map(name).join(", ")}`
815
793
  : null,
816
794
  ]
817
795
  .filter(Boolean)
@@ -819,6 +797,14 @@ export class CreditsTools {
819
797
  return formatSuccess(summary, result);
820
798
  }
821
799
  catch (err) {
800
+ // Nobody on the request could be reached: the route refuses with 422
801
+ // and names who and why. Not a transport failure.
802
+ const body = parseApiErrorBody(err);
803
+ if (body.code === "NO_REACHABLE_RECIPIENT") {
804
+ return typedError("NO_REACHABLE_RECIPIENT", body.message ?? "Nobody on this request could be reached.", body.details && typeof body.details === "object"
805
+ ? body.details
806
+ : undefined);
807
+ }
822
808
  const errorMessage = err instanceof Error ? err.message : String(err);
823
809
  return typedError("CREDITS_ATTESTATION_SEND_FAILED", `Failed to send credits for attestation: ${errorMessage}`);
824
810
  }
@@ -883,6 +869,26 @@ export class CreditsTools {
883
869
  isError: true,
884
870
  };
885
871
  }
872
+ // attestation_status is not caller-settable. Since 2026-09-29 the
873
+ // collaborators route rejects it (UNKNOWN_FIELD) and
874
+ // replace_work_collaborators forces every inserted row to 'pending'
875
+ // (migration 20260929190000) — a caller could otherwise fabricate
876
+ // "attested" with no attestation ever happening. This tool kept
877
+ // advertising and forwarding it, so agents sent it and the whole write
878
+ // failed (ops 00d9e736, 9a1cb927). Refuse it here, before anything is
879
+ // written, with the way to actually get a credit confirmed — never
880
+ // silently drop it, or the caller believes the credit is attested.
881
+ const attestationIdx = args.credits
882
+ .map((c) => c ?? {})
883
+ .findIndex((c) => Object.hasOwn(c, "attestation_status"));
884
+ if (attestationIdx !== -1) {
885
+ return typedError("ATTESTATION_NOT_SETTABLE", `credits[${attestationIdx}] carries attestation_status, which this ` +
886
+ `tool can't set — nothing was saved. Every credit starts as ` +
887
+ `pending and only the credited person can confirm it. Remove ` +
888
+ `attestation_status from every entry and retry (put any source ` +
889
+ `evidence in notes), then call pica_credits_send_for_attestation ` +
890
+ `so each person can confirm their own credit.`, { field: "attestation_status", index: attestationIdx });
891
+ }
886
892
  // ADR-252: the 100% rule is a COMPOSITION concept (writer/publisher shares).
887
893
  // Master/recording credits carry independent, unconstrained splits (a
888
894
  // producer takes points; a vocalist may have 0%). Validate the total over
@@ -912,9 +918,9 @@ export class CreditsTools {
912
918
  isError: true,
913
919
  };
914
920
  }
915
- // ADR-252: only fields with a canonical home survive. is_primary,
916
- // notes, attestation_status live on work_collaborators (composition);
917
- // notes / attestation_status are also on recording_credits (master).
921
+ // ADR-252: only fields with a canonical home survive. is_primary and
922
+ // notes live on work_collaborators (composition); notes is also on
923
+ // recording_credits (master). attestation_status is refused above.
918
924
  // territory / rights_type / start_date / end_date / publisher_id /
919
925
  // publisher_split_percentage had no home after the work_credits drop and
920
926
  // are no longer accepted — see write-constants.ts.
@@ -933,8 +939,6 @@ export class CreditsTools {
933
939
  credit.is_primary = c.is_primary;
934
940
  if (c.notes !== undefined)
935
941
  credit.notes = c.notes;
936
- if (c.attestation_status)
937
- credit.attestation_status = c.attestation_status;
938
942
  return credit;
939
943
  });
940
944
  const publishingCredits = allCredits.filter((c) => PUBLISHING_ROLES.has(String(c.role || "writer")
@@ -1049,9 +1053,21 @@ export class CreditsTools {
1049
1053
  * is set, mirroring the work-side updateCredits notification flow.
1050
1054
  */
1051
1055
  async updateRecordingCredits(args) {
1056
+ // attestation_status is not caller-settable. An attestation is the
1057
+ // credited person confirming their credit; until 2026-10-03 this tool
1058
+ // advertised it and the single form forwarded it, so any caller could
1059
+ // create a master credit already "attested". The route now refuses it
1060
+ // too, and session-role writes are refused in the database (migration
1061
+ // 20261003072610). Refuse here, before anything is written, with the way
1062
+ // to actually get a credit confirmed — never silently drop it, or the
1063
+ // caller believes the credit is attested. Same code and shape as the
1064
+ // pica_credits_update refusal in open PR #2622.
1065
+ const attestationRefusal = refuseRecordingAttestation(args);
1066
+ if (attestationRefusal)
1067
+ return attestationRefusal;
1052
1068
  // Superset dispatch (write-merge fold, mirrors ReleasesTools.attachTrack)
1053
- // — subsumes pica_recording_credits_bulk_update. Both executors fully
1054
- // self-validate their own args; no other logic changes below.
1069
+ // — subsumed pica_recording_credits_bulk_update (that name was removed in
1070
+ // mcp-server 2.99.0). Both executors fully self-validate their own args.
1055
1071
  if (Array.isArray(args.credits)) {
1056
1072
  return this.bulkUpdateRecordingCredits(args);
1057
1073
  }
@@ -1104,11 +1120,6 @@ export class CreditsTools {
1104
1120
  display_order: args.display_order ?? 0,
1105
1121
  notes: args.notes ?? null,
1106
1122
  instrument: args.instrument ?? null,
1107
- // ADR-252: master credits are attestable. The SDK input type predates
1108
- // the column; the route validates + persists it.
1109
- ...(args.attestation_status
1110
- ? { attestation_status: args.attestation_status }
1111
- : {}),
1112
1123
  });
1113
1124
  }
1114
1125
  catch (err) {
@@ -1176,7 +1187,10 @@ export class CreditsTools {
1176
1187
  return formatSuccess(msg, credit);
1177
1188
  }
1178
1189
  /**
1179
- * ADR-264 WS4 prerequisite — pica_recording_credits_bulk_update executor.
1190
+ * ADR-264 WS4 prerequisite — the bulk path of pica_recording_credits_update
1191
+ * (reached when args.credits is an array). It was also the executor of the
1192
+ * standalone pica_recording_credits_bulk_update tool until mcp-server 2.99.0
1193
+ * removed that name.
1180
1194
  *
1181
1195
  * Thin wrapper over POST /admin/recording-credits/bulk via
1182
1196
  * RecordingCreditsResource.bulkUpdate. The route + service own validation
@@ -1275,12 +1289,21 @@ export class CreditsTools {
1275
1289
  }
1276
1290
  patch.share_pct = v;
1277
1291
  }
1278
- for (const key of ["credit_type", "credited_name", "notes", "instrument"]) {
1292
+ for (const key of [
1293
+ "credit_type",
1294
+ "credited_name",
1295
+ "notes",
1296
+ "instrument",
1297
+ "person_id",
1298
+ ]) {
1279
1299
  if (args[key] !== undefined)
1280
1300
  patch[key] = args[key];
1281
1301
  }
1282
1302
  if (Object.keys(patch).length === 0) {
1283
- return typedError("VALIDATION_ERROR", "nothing to update — pass at least one of share_pct, credit_type, credited_name, notes, instrument.");
1303
+ return typedError("VALIDATION_ERROR", "nothing to update — pass at least one of share_pct, credit_type, credited_name, notes, instrument, person_id.");
1304
+ }
1305
+ if (args.confirm_name_mismatch === true) {
1306
+ patch.confirm_name_mismatch = true;
1284
1307
  }
1285
1308
  try {
1286
1309
  const result = await this.pica.credits.atomicUpdate(workId, creditId, patch);
@@ -1300,11 +1323,35 @@ export class CreditsTools {
1300
1323
  severity: "important",
1301
1324
  });
1302
1325
  }
1326
+ const personChange = result?.person_change;
1327
+ if (personChange?.attestation_reset) {
1328
+ // person_change.message is the server's own sentence
1329
+ // (describeCreditPersonChange in lib/services/credit-person-link.ts),
1330
+ // so the agent hears the same account the routes give.
1331
+ const what = personChange.message ??
1332
+ `This credit's ${personChange.previous_attestation_status ?? "previous"} status was cleared; it is now ${personChange.attestation_status ?? "pending"}.`;
1333
+ hints.push({
1334
+ gap: "credit_confirmation_reset",
1335
+ suggestion: `${what} The person now linked gave no confirmation of their own. Tell the user, and offer to ask the newly linked person to confirm (pica_credits_send_for_attestation on this work).`,
1336
+ severity: "important",
1337
+ });
1338
+ }
1303
1339
  return formatSuccess("Credit updated", result, hints);
1304
1340
  }
1305
1341
  catch (err) {
1306
1342
  const message = err instanceof Error ? err.message : String(err);
1307
1343
  const body = parseApiErrorBody(err);
1344
+ // Typed refusals of a change of person (lib/services/credit-person-link.ts).
1345
+ // Checked before the generic status branches: PERSON_NOT_FOUND is a
1346
+ // 404 and CREDIT_PERSON_CONFLICT a 409, and each needs its own next step.
1347
+ if (body.code === "PERSON_NOT_FOUND" ||
1348
+ body.code === "PERSON_DELETED" ||
1349
+ body.code === "PERSON_REQUIRED" ||
1350
+ body.code === "PERSON_NAME_MISMATCH" ||
1351
+ body.code === "CREDIT_PERSON_CONFLICT" ||
1352
+ body.code === "PERSON_LINK_IS_THE_CATALOGUES") {
1353
+ return typedError(body.code, body.message ?? message, body.details ?? undefined);
1354
+ }
1308
1355
  if (body.code === "INSUFFICIENT_SCOPE" || body.status === 403) {
1309
1356
  return typedError("INSUFFICIENT_SCOPE", `credit_id '${creditId}' is not a credit on work '${workId}' in this organisation. List the work's credits with pica_credits_list to find a valid id. Underlying error: ${message}`);
1310
1357
  }
@@ -1359,7 +1406,7 @@ export class CreditsTools {
1359
1406
  content: [
1360
1407
  {
1361
1408
  type: "text",
1362
- text: "share_pct must be a number between 0 and 100.",
1409
+ text: "share_pct must be a number between 0 and 100, or null (unknown).",
1363
1410
  },
1364
1411
  ],
1365
1412
  isError: true,
@@ -1439,6 +1486,26 @@ export class CreditsTools {
1439
1486
  const existing = recordingCreditExists(err);
1440
1487
  if (existing)
1441
1488
  return existing;
1489
+ // A 4xx carrying a machine code is the route REFUSING this call for a
1490
+ // reason it names (VALIDATION_ERROR, PERSON_ORG_MISMATCH,
1491
+ // RECORDING_TARGET_REQUIRED, SHARES_ARE_SUGGESTIONS, …). Relay that
1492
+ // code and message instead of folding it into CREDIT_ADD_FAILED, which
1493
+ // reads as a transport failure and invites a blind retry — ops
1494
+ // 704708a1 was a 400 VALIDATION_ERROR reported as CREDIT_ADD_FAILED.
1495
+ const refusal = parseApiErrorBody(err);
1496
+ if (refusal.code &&
1497
+ refusal.status !== undefined &&
1498
+ refusal.status >= 400 &&
1499
+ refusal.status < 500) {
1500
+ // Siblings first, so a route key can never overwrite the envelope's
1501
+ // own `error` / `message` (typedError writes those last).
1502
+ return typedError(refusal.code, refusal.message ?? errorMessage, {
1503
+ ...refusal.extra,
1504
+ ...(refusal.details !== undefined
1505
+ ? { details: refusal.details }
1506
+ : {}),
1507
+ });
1508
+ }
1442
1509
  return typedError("CREDIT_ADD_FAILED", `Failed to add credit: ${errorMessage}`);
1443
1510
  }
1444
1511
  }