@withpica/mcp-sdk 3.24.0 → 3.25.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/CHANGELOG.md CHANGED
@@ -11,6 +11,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [3.25.0] - 2026-10-14
15
+
16
+ > **Publish order:** `asideFromReport` is imported as a value by
17
+ > `@withpica/mcp-utils`'s billing gate. Publish this package (3.25.0) BEFORE
18
+ > the mcp-utils release that uses it, and raise mcp-utils' range to `^3.25.0`
19
+ > in that release; an mcp-utils on an older SDK fails at load.
20
+
21
+ ### Added
22
+
23
+ - `asideFromReport(fn)`: run requests that still name the current client
24
+ catalogue, but leave them off the call's report (a check PICA makes for
25
+ itself, such as the billing gate reading whether the catalogue is paid for).
26
+ - `pica.handovers`: `create` and `send` for catalogues an organisation prepares for a client
27
+ (`POST /admin/handovers`, `POST /admin/handovers/{id}/send`), with `HandoverCreatedRecord` and
28
+ `HandoverRecord`.
29
+ - `runInCatalogue(id, fn)` → `{ value, report }` and `CatalogueCallReport` (ADR-333 D7): what the app
30
+ did with one call's requests in a client catalogue — how many it answered for that catalogue, and
31
+ each request it refused with `RELATIONSHIP_REFUSED` (path and `reason_code`, from the one
32
+ refusal shape: `error.code`, `error.details.reason_code`). The MCP server uses it so a tool that fails its parts soft
33
+ cannot answer "success" with every part refused. `withCatalogue` is unchanged for callers.
34
+ - `withCatalogue(id, fn)`, `currentCatalogue()`, `CATALOGUE_HEADER`, `CATALOGUE_RESOLVED_HEADER` and
35
+ `CatalogueNotHonouredError` (ADR-333 D7). Every request made inside `withCatalogue` sends
36
+ `x-pica-catalogue: <id>`. A successful answer without a matching `x-pica-catalogue-resolved`
37
+ echo throws `CatalogueNotHonouredError` instead of reporting success, so a route that ignores
38
+ the header can never pass for having acted for the client. The catalogue is held per call
39
+ (AsyncLocalStorage), so concurrent calls on one shared client never mix catalogues.
40
+ - `relationships` (ADR-333): `list`, `get`, `invite`, `answer`, `revoke`, `stepDown`, `events`,
41
+ `activity` and `acceptInvite` over `/admin/relationships/**`, with typed records
42
+ (`RelationshipRecord`, `RelationshipListPage`, `RelationshipEventRecord`,
43
+ `RelationshipStepRecord`, `ClaimableInviteRecord`). Every step sends `expected_version`.
44
+ - `portfolio` (ADR-333): `list` and `briefing` over `/admin/portfolio`, with
45
+ `PortfolioClientRecord` and `PortfolioSection` (a section is checked, out of scope, unavailable
46
+ or failed, never a bare 0).
47
+ - `duplicates.markDifferent(entityType, ids)` and `duplicates.unmarkDifferent(entityType, ids)`
48
+ for recordings and releases (`POST` / `DELETE /admin/catalog/duplicates/exclusions`).
49
+
14
50
  ## [3.24.0] - 2026-10-09
15
51
 
16
52
  ### Added
package/dist/index.d.ts CHANGED
@@ -1,7 +1,3 @@
1
- /**
2
- * Lightweight PICA SDK client for MCP server
3
- * Uses the PICA API directly without external dependencies
4
- */
5
1
  /**
6
2
  * ADR-179 resolver result shape. Mirrors the server-side
7
3
  * `ResolveWorkResult` in `lib/services/enrichment-cascade/resolver.ts`.
@@ -870,6 +866,68 @@ export declare class ApiError extends Error {
870
866
  retryAfterMs?: number | undefined;
871
867
  constructor(message: string, status: number, retryable: boolean, retryAfterMs?: number | undefined);
872
868
  }
869
+ /**
870
+ * ADR-333 D7 — acting for a client catalogue, one call at a time.
871
+ *
872
+ * An organisation's assistant names the client catalogue on each tool call
873
+ * (the optional `catalogue` argument). The MCP server runs that call inside
874
+ * `withCatalogue(id, …)`, and every request the call makes carries
875
+ * `x-pica-catalogue: <id>`. The app resolves it through the relationship on
876
+ * every request (PR 1.3) and answers with `x-pica-catalogue-resolved: <id>`.
877
+ *
878
+ * A per-call context, not a field on the client: the stdio server shares one
879
+ * client across concurrent tool calls, so a field would leak one call's
880
+ * catalogue into another's requests.
881
+ *
882
+ * Fails closed: a 2xx answer without a matching echo means the route did not
883
+ * act for that catalogue (it may have used the caller's own), so the call
884
+ * throws `CatalogueNotHonouredError` instead of reporting success. This is the
885
+ * client's check only; stopping such a route from writing at all is the app's
886
+ * job (PR 1.3 refuses a catalogue header it did not resolve).
887
+ */
888
+ export declare const CATALOGUE_HEADER = "x-pica-catalogue";
889
+ export declare const CATALOGUE_RESOLVED_HEADER = "x-pica-catalogue-resolved";
890
+ /**
891
+ * What the app did with one call's requests in a client catalogue: how many it
892
+ * answered for that catalogue, and which it refused because the route does
893
+ * not open through a relationship, or because the relationship does not
894
+ * allow it (code RELATIONSHIP_REFUSED: proxy.ts, the request-context
895
+ * builders, requireCatalogueContext).
896
+ * The MCP server reads it after the tool returns: a tool that fails its parts
897
+ * soft would otherwise answer "success" with every part refused.
898
+ */
899
+ export interface CatalogueCallReport {
900
+ catalogueId: string;
901
+ answered: number;
902
+ refused: Array<{
903
+ path: string;
904
+ reason: string | null;
905
+ }>;
906
+ }
907
+ /** Run `fn` with every SDK request in it acting for `catalogueId`. */
908
+ export declare function withCatalogue<T>(catalogueId: string | undefined, fn: () => Promise<T>): Promise<T>;
909
+ /**
910
+ * `withCatalogue`, also returning the call's report. The report is the one
911
+ * the call's requests wrote to; it is complete once `fn` settles.
912
+ */
913
+ export declare function runInCatalogue<T>(catalogueId: string, fn: () => Promise<T>): Promise<{
914
+ value: T;
915
+ report: CatalogueCallReport;
916
+ }>;
917
+ /** The catalogue the current call acts for, if any. */
918
+ export declare function currentCatalogue(): string | undefined;
919
+ /**
920
+ * Run `fn` in the current client catalogue (its requests still name it), but
921
+ * leave its requests off the current call's report: for a check PICA makes
922
+ * for itself (the billing gate reading the client's plan), which must not
923
+ * read as the tool's own work having been answered (the MCP server judges a
924
+ * call "every part refused" by that report).
925
+ */
926
+ export declare function asideFromReport<T>(fn: () => Promise<T>): Promise<T>;
927
+ export declare class CatalogueNotHonouredError extends ApiError {
928
+ catalogueId: string;
929
+ constructor(catalogueId: string, path: string);
930
+ }
873
931
  /**
874
932
  * Uniform delete outcome (mirrors the server's deletion contract). `deleted` =
875
933
  * gone; `archived` = hidden but kept (registry data / dependents preserved);
@@ -3654,6 +3712,14 @@ declare class DuplicatesResource extends BaseResource {
3654
3712
  */
3655
3713
  findDuplicates(entityType: "works" | "people" | "releases" | "recordings" | "audio_files"): Promise<any>;
3656
3714
  merge(entityType: "work" | "person" | "recording" | "release", winnerId: string, loserIds: string[]): Promise<any>;
3715
+ /**
3716
+ * "They're different": mark every pair among `ids` (2–25 recordings or
3717
+ * releases of this catalogue) as not duplicates, so findDuplicates stops
3718
+ * offering them. Idempotent. Resolves {entity_type, ids, pairs, changed}.
3719
+ */
3720
+ markDifferent(entityType: "recordings" | "releases", ids: string[]): Promise<any>;
3721
+ /** Take back markDifferent for every pair among `ids`. */
3722
+ unmarkDifferent(entityType: "recordings" | "releases", ids: string[]): Promise<any>;
3657
3723
  }
3658
3724
  declare class EntityContextResource extends BaseResource {
3659
3725
  getWorkFull(workId: string): Promise<Record<string, unknown>>;
@@ -5207,6 +5273,318 @@ declare class RightsResource extends BaseResource {
5207
5273
  /** GET /admin/rights-statements/measures — the four measures (ADR-321 §7a). */
5208
5274
  measures(): Promise<any>;
5209
5275
  }
5276
+ /** A relationship's scope, as the routes return it (camelCase). */
5277
+ export interface RelationshipScopeRecord {
5278
+ half: "both" | "composition" | "master";
5279
+ territories: string[];
5280
+ termStartsOn: string | null;
5281
+ termEndsOn: string | null;
5282
+ money: boolean;
5283
+ directory: boolean;
5284
+ catalogue: "whole" | "chosen";
5285
+ workIds: string[];
5286
+ recordingIds: string[];
5287
+ }
5288
+ /**
5289
+ * One relationship (`org_grants` row) as GET /admin/relationships returns it.
5290
+ * `version` is passed back as `expected_version` on every step, so a step
5291
+ * never lands on terms that changed since they were read.
5292
+ */
5293
+ export interface RelationshipRecord {
5294
+ id: string;
5295
+ grantorOrgId: string;
5296
+ grantorOrgName: string;
5297
+ granteeOrgId: string;
5298
+ granteeOrgName: string;
5299
+ type: "co_owner" | "administrator" | "representative" | "viewer";
5300
+ status: "proposed" | "active" | "declined" | "revoked" | "stepped_down" | "expired" | "suspended";
5301
+ proposedBySide: "grantor" | "grantee";
5302
+ scope: RelationshipScopeRecord;
5303
+ proposedAt: string;
5304
+ respondedAt: string | null;
5305
+ endedAt: string | null;
5306
+ endReason: string | null;
5307
+ proposalExpiresAt: string | null;
5308
+ suspendedAt: string | null;
5309
+ version: number;
5310
+ [key: string]: unknown;
5311
+ }
5312
+ /** An invitation waiting for the person (no address in it). */
5313
+ export interface ClaimableInviteRecord {
5314
+ invite_id: string;
5315
+ direction: "grantee_invites_owner" | "owner_invites_grantee";
5316
+ inviting_org_id: string;
5317
+ inviting_org_name: string;
5318
+ type: RelationshipRecord["type"];
5319
+ scope: RelationshipScopeRecord;
5320
+ expires_at: string;
5321
+ eligible_organisations: Array<{
5322
+ id: string;
5323
+ name: string;
5324
+ }>;
5325
+ }
5326
+ export interface RelationshipListPage {
5327
+ rows: RelationshipRecord[];
5328
+ nextCursor: string | null;
5329
+ waiting_for_you: ClaimableInviteRecord[];
5330
+ /** Set when the waiting invitations could not be read: never "none waiting". */
5331
+ waiting_for_you_unavailable?: "email_not_verified" | "check_failed";
5332
+ }
5333
+ /** One audit row (`org_grant_events`). */
5334
+ export interface RelationshipEventRecord {
5335
+ id: string;
5336
+ relationshipId: string;
5337
+ kind: string;
5338
+ at: string;
5339
+ actorSide: "grantor" | "grantee" | "system" | "platform";
5340
+ actorDisplayName: string | null;
5341
+ grantorOrgId: string;
5342
+ grantorOrgName: string;
5343
+ granteeOrgId: string;
5344
+ granteeOrgName: string;
5345
+ type: RelationshipRecord["type"];
5346
+ typeBefore: RelationshipRecord["type"] | null;
5347
+ statusBefore: RelationshipRecord["status"] | null;
5348
+ statusAfter: RelationshipRecord["status"];
5349
+ version: number;
5350
+ /** The scope before this event (null for `proposed`). */
5351
+ scopeBefore: RelationshipScopeRecord | null;
5352
+ /** The scope after this event (for an ending: the scope that ended). */
5353
+ scope: RelationshipScopeRecord;
5354
+ reason: string | null;
5355
+ action: string | null;
5356
+ entityType: string | null;
5357
+ entityId: string | null;
5358
+ detail: Record<string, unknown>;
5359
+ [key: string]: unknown;
5360
+ }
5361
+ export interface RelationshipPageOf<T> {
5362
+ rows: T[];
5363
+ nextCursor: string | null;
5364
+ }
5365
+ /** The result of accepting, declining, editing, revoking or stepping down. */
5366
+ export interface RelationshipStepRecord {
5367
+ relationship: RelationshipRecord;
5368
+ event: RelationshipEventRecord | null;
5369
+ }
5370
+ /** The result of accepting an invitation: the claim and the accept, both recorded. */
5371
+ export interface RelationshipInviteAcceptRecord {
5372
+ relationship: RelationshipRecord;
5373
+ events: RelationshipEventRecord[];
5374
+ }
5375
+ /** A hand-over just created (ADR-333 D5): nothing is sent yet. */
5376
+ export interface HandoverCreatedRecord {
5377
+ handover_id: string;
5378
+ catalogue_org_id: string;
5379
+ }
5380
+ /** A hand-over as its builder sees it (POST /admin/handovers/{id}/send). */
5381
+ export interface HandoverRecord {
5382
+ handover_id: string;
5383
+ catalogue_org_id: string;
5384
+ catalogue_name: string | null;
5385
+ builder_org_id: string;
5386
+ builder_name: string | null;
5387
+ client_name: string;
5388
+ client_email?: string;
5389
+ status: string;
5390
+ relationship_type: string;
5391
+ sent_at: string | null;
5392
+ expires_at: string | null;
5393
+ version: number;
5394
+ }
5395
+ /**
5396
+ * Catalogues an organisation builds for a client, who accepts to own them
5397
+ * (ADR-333 D5). Builder side only: the client answers on screen, signed in.
5398
+ */
5399
+ declare class HandoversResource extends BaseResource {
5400
+ /**
5401
+ * POST /admin/handovers — create a catalogue for a client
5402
+ * (catalogue_name), or a new hand-over on one this organisation built whose
5403
+ * last one closed (catalogue_org_id). Nothing is sent.
5404
+ */
5405
+ create(body: {
5406
+ catalogue_name?: string;
5407
+ catalogue_org_id?: string;
5408
+ client_name: string;
5409
+ client_email: string;
5410
+ relationship_type: string;
5411
+ }): Promise<HandoverCreatedRecord>;
5412
+ /** POST /admin/handovers/{id}/send { expected_version? } — email the client. */
5413
+ send(id: string, body: {
5414
+ expected_version?: number;
5415
+ }): Promise<HandoverRecord>;
5416
+ }
5417
+ /**
5418
+ * /admin/relationships/** — both sides of a relationship. Refusals arrive as
5419
+ * `{ success: false, error: { code, message, details? } }` (read them with
5420
+ * the MCP package's parseApiErrorBody); a step whose terms changed since they
5421
+ * were read is a 409 `status_changed` carrying `details.relationship`.
5422
+ */
5423
+ declare class RelationshipsResource extends BaseResource {
5424
+ /** GET /admin/relationships?side=owner|organisation&status=&cursor=&limit= */
5425
+ list(params: {
5426
+ side: "owner" | "organisation";
5427
+ statuses?: string[];
5428
+ cursor?: string;
5429
+ limit?: number;
5430
+ }): Promise<RelationshipListPage>;
5431
+ /** GET /admin/relationships/{id} */
5432
+ get(id: string): Promise<RelationshipRecord>;
5433
+ /**
5434
+ * POST /admin/relationships { as, email, type, scope? } — invite someone by
5435
+ * email. as="owner" asks someone at an organisation to look after this
5436
+ * catalogue; as="organisation" asks a client to let this organisation look
5437
+ * after theirs. The reply is the same whether or not the address is on PICA.
5438
+ */
5439
+ invite(body: {
5440
+ as: "owner" | "organisation";
5441
+ email: string;
5442
+ type: string;
5443
+ scope?: Record<string, unknown>;
5444
+ }): Promise<Record<string, unknown>>;
5445
+ /** PATCH /admin/relationships/{id} { action: accept | decline | edit, expected_version, … } */
5446
+ answer(id: string, body: {
5447
+ action: "accept" | "decline" | "edit";
5448
+ expected_version: number;
5449
+ type?: string;
5450
+ scope?: Record<string, unknown>;
5451
+ reason?: string;
5452
+ }): Promise<RelationshipStepRecord>;
5453
+ /** DELETE /admin/relationships/{id} { expected_version, reason? } — the owner ends it. */
5454
+ revoke(id: string, body: {
5455
+ expected_version: number;
5456
+ reason?: string;
5457
+ }): Promise<RelationshipStepRecord>;
5458
+ /** POST /admin/relationships/{id}/step-down { expected_version, reason? } — the organisation ends its side. */
5459
+ stepDown(id: string, body: {
5460
+ expected_version: number;
5461
+ reason?: string;
5462
+ }): Promise<RelationshipStepRecord>;
5463
+ /** GET /admin/relationships/{id}/activity — one relationship's history, either party. */
5464
+ events(id: string, params?: {
5465
+ cursor?: string;
5466
+ limit?: number;
5467
+ }): Promise<RelationshipPageOf<RelationshipEventRecord>>;
5468
+ /**
5469
+ * GET /admin/relationships/activity?side=&org= — side=owner: what
5470
+ * organisations did in this catalogue (org = one organisation);
5471
+ * side=organisation: this organisation's own actions (org = one catalogue).
5472
+ */
5473
+ activity(params: {
5474
+ side: "owner" | "organisation";
5475
+ org?: string;
5476
+ cursor?: string;
5477
+ limit?: number;
5478
+ }): Promise<RelationshipPageOf<RelationshipEventRecord>>;
5479
+ /**
5480
+ * PATCH /admin/relationships/invites/{id} { action: "accept", organisation_id, type?, scope? }
5481
+ * — answer yes to an invitation for one of the person's organisations.
5482
+ */
5483
+ acceptInvite(inviteId: string, body: {
5484
+ organisation_id: string;
5485
+ type?: string;
5486
+ scope?: Record<string, unknown>;
5487
+ }): Promise<RelationshipInviteAcceptRecord>;
5488
+ }
5489
+ /** A portfolio section: checked, out of scope, unavailable, or failed (never 0). */
5490
+ export type PortfolioSection<T> = {
5491
+ state: "ok";
5492
+ data: T;
5493
+ } | {
5494
+ state: "not_in_scope" | "unavailable" | "failed";
5495
+ reason: string;
5496
+ };
5497
+ /** One client catalogue an organisation looks after (portfolio PR 1.4). */
5498
+ export interface PortfolioClientRecord {
5499
+ relationshipId: string;
5500
+ catalogue: {
5501
+ orgId: string;
5502
+ name: string;
5503
+ };
5504
+ relationship: {
5505
+ type: RelationshipRecord["type"];
5506
+ typeLabel: string;
5507
+ status: "active" | "suspended";
5508
+ access: "open" | "paused" | "not_started";
5509
+ acceptedAt: string | null;
5510
+ termEndsOn: string | null;
5511
+ endingSoon: boolean;
5512
+ version: number;
5513
+ /** What a change by this caller does in that catalogue. */
5514
+ changes: "direct" | "proposal" | "none";
5515
+ scope: {
5516
+ half: RelationshipScopeRecord["half"];
5517
+ territories: string[];
5518
+ termStartsOn: string | null;
5519
+ termEndsOn: string | null;
5520
+ money: boolean;
5521
+ directory: boolean;
5522
+ catalogue: "whole" | "chosen";
5523
+ songsChosen: number;
5524
+ recordingsChosen: number;
5525
+ };
5526
+ };
5527
+ /** null = the scope doesn't cover that half ("not covered", never 0). */
5528
+ missing: PortfolioSection<{
5529
+ songs: number | null;
5530
+ songsWithGaps: number | null;
5531
+ recordings: number | null;
5532
+ recordingsWithGaps: number | null;
5533
+ }>;
5534
+ moneyBlockers: PortfolioSection<{
5535
+ songs: number;
5536
+ blocked: number;
5537
+ clear: number;
5538
+ unknown: number;
5539
+ byCode: Array<{
5540
+ code: string;
5541
+ label: string;
5542
+ songs: number;
5543
+ }>;
5544
+ }>;
5545
+ enquiries: PortfolioSection<{
5546
+ waiting: number;
5547
+ oldestAt: string | null;
5548
+ }>;
5549
+ deadlines: PortfolioSection<{
5550
+ total: number;
5551
+ items: Array<{
5552
+ kind: string;
5553
+ on: string;
5554
+ label: string;
5555
+ }>;
5556
+ }>;
5557
+ /** Briefing only: at most five, in scope only. */
5558
+ highlights?: Array<{
5559
+ kind: string;
5560
+ label: string;
5561
+ workId?: string;
5562
+ recordingId?: string;
5563
+ }>;
5564
+ [key: string]: unknown;
5565
+ }
5566
+ export interface PortfolioPage {
5567
+ rows: PortfolioClientRecord[];
5568
+ nextCursor: string | null;
5569
+ asOf: string;
5570
+ }
5571
+ /**
5572
+ * /admin/portfolio — the client catalogues an organisation looks after. A
5573
+ * page may hold fewer rows than `limit` and still not be the last: follow
5574
+ * `nextCursor` until it is null.
5575
+ */
5576
+ declare class PortfolioResource extends BaseResource {
5577
+ /** GET /admin/portfolio?cursor=&limit= (limit ≤ 25) */
5578
+ list(params?: {
5579
+ cursor?: string;
5580
+ limit?: number;
5581
+ }): Promise<PortfolioPage>;
5582
+ /** GET /admin/portfolio/briefing?cursor=&limit= — the list plus what needs attention. */
5583
+ briefing(params?: {
5584
+ cursor?: string;
5585
+ limit?: number;
5586
+ }): Promise<PortfolioPage>;
5587
+ }
5210
5588
  declare class RoyaltiesResource extends BaseResource {
5211
5589
  payments(params?: {
5212
5590
  source?: string;
@@ -5748,6 +6126,11 @@ export declare class PicaClient {
5748
6126
  producerAgreements: ProducerAgreementsResource;
5749
6127
  workForHire: WorkForHireResource;
5750
6128
  royalties: RoyaltiesResource;
6129
+ /** ADR-333 — who looks after whose catalogue, from either side. */
6130
+ relationships: RelationshipsResource;
6131
+ handovers: HandoversResource;
6132
+ /** ADR-333 — the client catalogues an organisation looks after. */
6133
+ portfolio: PortfolioResource;
5751
6134
  /** ADR-321 Phase 2 — programmable rights (propose / status / inspect). */
5752
6135
  rights: RightsResource;
5753
6136
  statements: StatementsResource;