@withpica/mcp-sdk 3.24.0 → 3.26.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 +45 -0
- package/dist/index.d.ts +417 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +261 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight PICA SDK client for MCP server
|
|
5
|
+
* Uses the PICA API directly without external dependencies
|
|
6
|
+
*/
|
|
7
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
8
|
// --- Request infrastructure ---
|
|
4
9
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
10
|
const LONG_TIMEOUT_MS = 120_000;
|
|
@@ -32,6 +37,99 @@ export class ApiError extends Error {
|
|
|
32
37
|
this.name = "ApiError";
|
|
33
38
|
}
|
|
34
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* ADR-333 D7 — acting for a client catalogue, one call at a time.
|
|
42
|
+
*
|
|
43
|
+
* An organisation's assistant names the client catalogue on each tool call
|
|
44
|
+
* (the optional `catalogue` argument). The MCP server runs that call inside
|
|
45
|
+
* `withCatalogue(id, …)`, and every request the call makes carries
|
|
46
|
+
* `x-pica-catalogue: <id>`. The app resolves it through the relationship on
|
|
47
|
+
* every request (PR 1.3) and answers with `x-pica-catalogue-resolved: <id>`.
|
|
48
|
+
*
|
|
49
|
+
* A per-call context, not a field on the client: the stdio server shares one
|
|
50
|
+
* client across concurrent tool calls, so a field would leak one call's
|
|
51
|
+
* catalogue into another's requests.
|
|
52
|
+
*
|
|
53
|
+
* Fails closed: a 2xx answer without a matching echo means the route did not
|
|
54
|
+
* act for that catalogue (it may have used the caller's own), so the call
|
|
55
|
+
* throws `CatalogueNotHonouredError` instead of reporting success. This is the
|
|
56
|
+
* client's check only; stopping such a route from writing at all is the app's
|
|
57
|
+
* job (PR 1.3 refuses a catalogue header it did not resolve).
|
|
58
|
+
*/
|
|
59
|
+
export const CATALOGUE_HEADER = "x-pica-catalogue";
|
|
60
|
+
export const CATALOGUE_RESOLVED_HEADER = "x-pica-catalogue-resolved";
|
|
61
|
+
const catalogueContext = new AsyncLocalStorage();
|
|
62
|
+
/** Run `fn` with every SDK request in it acting for `catalogueId`. */
|
|
63
|
+
export function withCatalogue(catalogueId, fn) {
|
|
64
|
+
return catalogueId
|
|
65
|
+
? runInCatalogue(catalogueId, fn).then((r) => r.value)
|
|
66
|
+
: fn();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `withCatalogue`, also returning the call's report. The report is the one
|
|
70
|
+
* the call's requests wrote to; it is complete once `fn` settles.
|
|
71
|
+
*/
|
|
72
|
+
export async function runInCatalogue(catalogueId, fn) {
|
|
73
|
+
const report = {
|
|
74
|
+
catalogueId,
|
|
75
|
+
answered: 0,
|
|
76
|
+
refused: [],
|
|
77
|
+
};
|
|
78
|
+
const value = await catalogueContext.run(report, fn);
|
|
79
|
+
return { value, report };
|
|
80
|
+
}
|
|
81
|
+
/** The catalogue the current call acts for, if any. */
|
|
82
|
+
export function currentCatalogue() {
|
|
83
|
+
return catalogueContext.getStore()?.catalogueId;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Run `fn` in the current client catalogue (its requests still name it), but
|
|
87
|
+
* leave its requests off the current call's report: for a check PICA makes
|
|
88
|
+
* for itself, which must not read as the tool's own work having been
|
|
89
|
+
* answered (the MCP server judges a call "every part refused" by that
|
|
90
|
+
* report). No caller since the MCP billing gate was retired (ADR-335); kept
|
|
91
|
+
* because it is part of the published SDK.
|
|
92
|
+
*/
|
|
93
|
+
export function asideFromReport(fn) {
|
|
94
|
+
const current = catalogueContext.getStore();
|
|
95
|
+
if (!current)
|
|
96
|
+
return fn();
|
|
97
|
+
return catalogueContext.run({ catalogueId: current.catalogueId, answered: 0, refused: [] }, fn);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Record a relationship refusal on the current call's report (no-op outside
|
|
101
|
+
* one). Every client-catalogue refusal, from the proxy or a route, answers
|
|
102
|
+
* `{ success: false, error: { code, message, details: { reason_code } } }`
|
|
103
|
+
* (catalogueRefusalBody, lib/auth/relationship-refused-error.ts; pinned by
|
|
104
|
+
* lib/auth/__tests__/catalogue-refusal-shape.test.ts).
|
|
105
|
+
*/
|
|
106
|
+
function recordCatalogueRefusal(url, body) {
|
|
107
|
+
const report = catalogueContext.getStore();
|
|
108
|
+
if (!report)
|
|
109
|
+
return;
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(body);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (parsed?.error?.code !== "RELATIONSHIP_REFUSED")
|
|
118
|
+
return;
|
|
119
|
+
const reason = parsed.error.details?.reason_code;
|
|
120
|
+
report.refused.push({
|
|
121
|
+
path: new URL(url).pathname,
|
|
122
|
+
reason: typeof reason === "string" ? reason : null,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export class CatalogueNotHonouredError extends ApiError {
|
|
126
|
+
catalogueId;
|
|
127
|
+
constructor(catalogueId, path) {
|
|
128
|
+
super(`This action can't be done for a client's catalogue yet, so nothing was done for catalogue ${catalogueId} (${path}).`, 0, false);
|
|
129
|
+
this.catalogueId = catalogueId;
|
|
130
|
+
this.name = "CatalogueNotHonouredError";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
35
133
|
/**
|
|
36
134
|
* Thrown when the API responds 404 — the entity at the given id either
|
|
37
135
|
* doesn't exist or doesn't belong to the caller's org. Distinguished from
|
|
@@ -106,9 +204,30 @@ class BaseResource {
|
|
|
106
204
|
const timingOn = typeof process !== "undefined" && process.env?.PICA_SDK_TIMING === "1";
|
|
107
205
|
const t0 = timingOn ? Date.now() : 0;
|
|
108
206
|
let statusForTiming = 0;
|
|
207
|
+
const catalogueId = currentCatalogue();
|
|
208
|
+
const headers = catalogueId
|
|
209
|
+
? {
|
|
210
|
+
...init.headers,
|
|
211
|
+
[CATALOGUE_HEADER]: catalogueId,
|
|
212
|
+
}
|
|
213
|
+
: init.headers;
|
|
109
214
|
try {
|
|
110
|
-
const res = await fetch(url, {
|
|
215
|
+
const res = await fetch(url, {
|
|
216
|
+
...init,
|
|
217
|
+
headers,
|
|
218
|
+
signal: controller.signal,
|
|
219
|
+
});
|
|
111
220
|
statusForTiming = res.status;
|
|
221
|
+
if (catalogueId &&
|
|
222
|
+
res.ok &&
|
|
223
|
+
res.headers.get(CATALOGUE_RESOLVED_HEADER) !== catalogueId) {
|
|
224
|
+
throw new CatalogueNotHonouredError(catalogueId, new URL(url).pathname);
|
|
225
|
+
}
|
|
226
|
+
if (catalogueId && res.ok) {
|
|
227
|
+
const report = catalogueContext.getStore();
|
|
228
|
+
if (report)
|
|
229
|
+
report.answered += 1;
|
|
230
|
+
}
|
|
112
231
|
return res;
|
|
113
232
|
}
|
|
114
233
|
catch (err) {
|
|
@@ -142,6 +261,7 @@ class BaseResource {
|
|
|
142
261
|
if (response.ok)
|
|
143
262
|
return response;
|
|
144
263
|
const errorText = await response.text();
|
|
264
|
+
recordCatalogueRefusal(url, errorText);
|
|
145
265
|
// Auth errors — fail fast, never retry
|
|
146
266
|
if (response.status === 401 || response.status === 403) {
|
|
147
267
|
throw new ApiError(`Authentication failed: ${response.status} ${errorText}`, response.status, false);
|
|
@@ -868,6 +988,13 @@ class CreditsResource extends BaseResource {
|
|
|
868
988
|
async listForWork(workId) {
|
|
869
989
|
return this.request("GET", `/admin/works/${workId}/credits`);
|
|
870
990
|
}
|
|
991
|
+
/**
|
|
992
|
+
* Every credit row on a work, composition and master, one entry per row
|
|
993
|
+
* with its own id (listForWork groups per person and keeps one id).
|
|
994
|
+
*/
|
|
995
|
+
async listRowsForWork(workId) {
|
|
996
|
+
return this.request("GET", `/admin/works/${workId}/credits?view=rows`);
|
|
997
|
+
}
|
|
871
998
|
async updateForWork(workId, credits) {
|
|
872
999
|
return this.request("POST", `/admin/works/${workId}/credits`, credits);
|
|
873
1000
|
}
|
|
@@ -2775,6 +2902,24 @@ class DuplicatesResource extends BaseResource {
|
|
|
2775
2902
|
loser_ids: loserIds,
|
|
2776
2903
|
});
|
|
2777
2904
|
}
|
|
2905
|
+
/**
|
|
2906
|
+
* "They're different": mark every pair among `ids` (2–25 recordings or
|
|
2907
|
+
* releases of this catalogue) as not duplicates, so findDuplicates stops
|
|
2908
|
+
* offering them. Idempotent. Resolves {entity_type, ids, pairs, changed}.
|
|
2909
|
+
*/
|
|
2910
|
+
async markDifferent(entityType, ids) {
|
|
2911
|
+
return this.request("POST", "/admin/catalog/duplicates/exclusions", {
|
|
2912
|
+
entity_type: entityType,
|
|
2913
|
+
ids,
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
/** Take back markDifferent for every pair among `ids`. */
|
|
2917
|
+
async unmarkDifferent(entityType, ids) {
|
|
2918
|
+
return this.request("DELETE", "/admin/catalog/duplicates/exclusions", {
|
|
2919
|
+
entity_type: entityType,
|
|
2920
|
+
ids,
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2778
2923
|
}
|
|
2779
2924
|
// --- Entity Context Resource (ADR-116 Phase 3) ---
|
|
2780
2925
|
class EntityContextResource extends BaseResource {
|
|
@@ -4040,6 +4185,113 @@ class RightsResource extends BaseResource {
|
|
|
4040
4185
|
return this.request("GET", "/admin/rights-statements/measures");
|
|
4041
4186
|
}
|
|
4042
4187
|
}
|
|
4188
|
+
function pageQuery(params) {
|
|
4189
|
+
const qp = new URLSearchParams();
|
|
4190
|
+
for (const [key, value] of Object.entries(params)) {
|
|
4191
|
+
if (value === undefined)
|
|
4192
|
+
continue;
|
|
4193
|
+
if (Array.isArray(value))
|
|
4194
|
+
value.forEach((v) => qp.append(key, v));
|
|
4195
|
+
else
|
|
4196
|
+
qp.set(key, String(value));
|
|
4197
|
+
}
|
|
4198
|
+
const qs = qp.toString();
|
|
4199
|
+
return qs ? `?${qs}` : "";
|
|
4200
|
+
}
|
|
4201
|
+
/**
|
|
4202
|
+
* Catalogues an organisation builds for a client, who accepts to own them
|
|
4203
|
+
* (ADR-333 D5). Builder side only: the client answers on screen, signed in.
|
|
4204
|
+
*/
|
|
4205
|
+
class HandoversResource extends BaseResource {
|
|
4206
|
+
/**
|
|
4207
|
+
* POST /admin/handovers — create a catalogue for a client
|
|
4208
|
+
* (catalogue_name), or a new hand-over on one this organisation built whose
|
|
4209
|
+
* last one closed (catalogue_org_id). Nothing is sent.
|
|
4210
|
+
*/
|
|
4211
|
+
async create(body) {
|
|
4212
|
+
return this.request("POST", "/admin/handovers", body);
|
|
4213
|
+
}
|
|
4214
|
+
/** POST /admin/handovers/{id}/send { expected_version? } — email the client. */
|
|
4215
|
+
async send(id, body) {
|
|
4216
|
+
return this.request("POST", `/admin/handovers/${encodeURIComponent(id)}/send`, body);
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
/**
|
|
4220
|
+
* /admin/relationships/** — both sides of a relationship. Refusals arrive as
|
|
4221
|
+
* `{ success: false, error: { code, message, details? } }` (read them with
|
|
4222
|
+
* the MCP package's parseApiErrorBody); a step whose terms changed since they
|
|
4223
|
+
* were read is a 409 `status_changed` carrying `details.relationship`.
|
|
4224
|
+
*/
|
|
4225
|
+
class RelationshipsResource extends BaseResource {
|
|
4226
|
+
/** GET /admin/relationships?side=owner|organisation&status=&cursor=&limit= */
|
|
4227
|
+
async list(params) {
|
|
4228
|
+
return this.request("GET", `/admin/relationships${pageQuery({
|
|
4229
|
+
side: params.side,
|
|
4230
|
+
status: params.statuses,
|
|
4231
|
+
cursor: params.cursor,
|
|
4232
|
+
limit: params.limit,
|
|
4233
|
+
})}`);
|
|
4234
|
+
}
|
|
4235
|
+
/** GET /admin/relationships/{id} */
|
|
4236
|
+
async get(id) {
|
|
4237
|
+
return this.request("GET", `/admin/relationships/${encodeURIComponent(id)}`);
|
|
4238
|
+
}
|
|
4239
|
+
/**
|
|
4240
|
+
* POST /admin/relationships { as, email, type, scope? } — invite someone by
|
|
4241
|
+
* email. as="owner" asks someone at an organisation to look after this
|
|
4242
|
+
* catalogue; as="organisation" asks a client to let this organisation look
|
|
4243
|
+
* after theirs. The reply is the same whether or not the address is on PICA.
|
|
4244
|
+
*/
|
|
4245
|
+
async invite(body) {
|
|
4246
|
+
return this.request("POST", "/admin/relationships", body);
|
|
4247
|
+
}
|
|
4248
|
+
/** PATCH /admin/relationships/{id} { action: accept | decline | edit, expected_version, … } */
|
|
4249
|
+
async answer(id, body) {
|
|
4250
|
+
return this.request("PATCH", `/admin/relationships/${encodeURIComponent(id)}`, body);
|
|
4251
|
+
}
|
|
4252
|
+
/** DELETE /admin/relationships/{id} { expected_version, reason? } — the owner ends it. */
|
|
4253
|
+
async revoke(id, body) {
|
|
4254
|
+
return this.request("DELETE", `/admin/relationships/${encodeURIComponent(id)}`, body);
|
|
4255
|
+
}
|
|
4256
|
+
/** POST /admin/relationships/{id}/step-down { expected_version, reason? } — the organisation ends its side. */
|
|
4257
|
+
async stepDown(id, body) {
|
|
4258
|
+
return this.request("POST", `/admin/relationships/${encodeURIComponent(id)}/step-down`, body);
|
|
4259
|
+
}
|
|
4260
|
+
/** GET /admin/relationships/{id}/activity — one relationship's history, either party. */
|
|
4261
|
+
async events(id, params = {}) {
|
|
4262
|
+
return this.request("GET", `/admin/relationships/${encodeURIComponent(id)}/activity${pageQuery(params)}`);
|
|
4263
|
+
}
|
|
4264
|
+
/**
|
|
4265
|
+
* GET /admin/relationships/activity?side=&org= — side=owner: what
|
|
4266
|
+
* organisations did in this catalogue (org = one organisation);
|
|
4267
|
+
* side=organisation: this organisation's own actions (org = one catalogue).
|
|
4268
|
+
*/
|
|
4269
|
+
async activity(params) {
|
|
4270
|
+
return this.request("GET", `/admin/relationships/activity${pageQuery(params)}`);
|
|
4271
|
+
}
|
|
4272
|
+
/**
|
|
4273
|
+
* PATCH /admin/relationships/invites/{id} { action: "accept", organisation_id, type?, scope? }
|
|
4274
|
+
* — answer yes to an invitation for one of the person's organisations.
|
|
4275
|
+
*/
|
|
4276
|
+
async acceptInvite(inviteId, body) {
|
|
4277
|
+
return this.request("PATCH", `/admin/relationships/invites/${encodeURIComponent(inviteId)}`, { action: "accept", ...body });
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
/**
|
|
4281
|
+
* /admin/portfolio — the client catalogues an organisation looks after. A
|
|
4282
|
+
* page may hold fewer rows than `limit` and still not be the last: follow
|
|
4283
|
+
* `nextCursor` until it is null.
|
|
4284
|
+
*/
|
|
4285
|
+
class PortfolioResource extends BaseResource {
|
|
4286
|
+
/** GET /admin/portfolio?cursor=&limit= (limit ≤ 25) */
|
|
4287
|
+
async list(params = {}) {
|
|
4288
|
+
return this.request("GET", `/admin/portfolio${pageQuery(params)}`);
|
|
4289
|
+
}
|
|
4290
|
+
/** GET /admin/portfolio/briefing?cursor=&limit= — the list plus what needs attention. */
|
|
4291
|
+
async briefing(params = {}) {
|
|
4292
|
+
return this.request("GET", `/admin/portfolio/briefing${pageQuery(params)}`);
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4043
4295
|
class RoyaltiesResource extends BaseResource {
|
|
4044
4296
|
async payments(params) {
|
|
4045
4297
|
const qp = new URLSearchParams();
|
|
@@ -4542,6 +4794,11 @@ export class PicaClient {
|
|
|
4542
4794
|
producerAgreements;
|
|
4543
4795
|
workForHire;
|
|
4544
4796
|
royalties;
|
|
4797
|
+
/** ADR-333 — who looks after whose catalogue, from either side. */
|
|
4798
|
+
relationships;
|
|
4799
|
+
handovers;
|
|
4800
|
+
/** ADR-333 — the client catalogues an organisation looks after. */
|
|
4801
|
+
portfolio;
|
|
4545
4802
|
/** ADR-321 Phase 2 — programmable rights (propose / status / inspect). */
|
|
4546
4803
|
rights;
|
|
4547
4804
|
statements;
|
|
@@ -4653,6 +4910,9 @@ export class PicaClient {
|
|
|
4653
4910
|
this.producerAgreements = new ProducerAgreementsResource(baseUrl, config.apiKey, debug);
|
|
4654
4911
|
this.workForHire = new WorkForHireResource(baseUrl, config.apiKey, debug);
|
|
4655
4912
|
this.royalties = new RoyaltiesResource(baseUrl, config.apiKey, debug);
|
|
4913
|
+
this.relationships = new RelationshipsResource(baseUrl, config.apiKey, debug);
|
|
4914
|
+
this.handovers = new HandoversResource(baseUrl, config.apiKey, debug);
|
|
4915
|
+
this.portfolio = new PortfolioResource(baseUrl, config.apiKey, debug);
|
|
4656
4916
|
this.rights = new RightsResource(baseUrl, config.apiKey, debug);
|
|
4657
4917
|
this.statements = new StatementsResource(baseUrl, config.apiKey, debug);
|
|
4658
4918
|
this.selections = new SelectionsResource(baseUrl, config.apiKey, debug);
|