@haddowg/json-api-mcp 0.0.0-stage → 1.2.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.cjs ADDED
@@ -0,0 +1,808 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _haddowg_json_api_client = require("@haddowg/json-api-client");
3
+ //#region src/contract.ts
4
+ /** The oldest generator contract this package reads: the baseline current servers stamp. */
5
+ const MINIMUM_CONTRACT = 13;
6
+ /** The newest generator contract this package reads. */
7
+ const MAXIMUM_CONTRACT = 18;
8
+ /**
9
+ * Throw unless the document's `info.x-generator.contract` is in the range this package reads. A
10
+ * document outside it would yield tools that miss or misread what the server offers, so there is
11
+ * no warning mode.
12
+ */
13
+ function assertContract(doc) {
14
+ const contract = doc.info?.["x-generator"]?.contract;
15
+ if (typeof contract !== "number") throw new Error(`The OpenAPI document declares no generator contract (info.x-generator.contract). @haddowg/json-api-mcp reads contracts 13 to 18; regenerate the document from a newer haddowg/json-api server.`);
16
+ if (contract < 13) throw new Error(`The OpenAPI document declares generator contract ${contract}, older than this package reads (13 to 18). Regenerate it from a newer haddowg/json-api server.`);
17
+ if (contract > 18) throw new Error(`The OpenAPI document declares generator contract ${contract}, newer than this package reads (13 to 18). Upgrade @haddowg/json-api-mcp.`);
18
+ return contract;
19
+ }
20
+ //#endregion
21
+ //#region src/openapi.ts
22
+ const HTTP_METHODS = [
23
+ "get",
24
+ "post",
25
+ "patch",
26
+ "put",
27
+ "delete"
28
+ ];
29
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
30
+ //#endregion
31
+ //#region src/portable.ts
32
+ /**
33
+ * Convert document schemas into tool input schemas every major model API accepts: OpenAI's strict
34
+ * function calling and Anthropic's strict tool use. That means no `oneOf`, `allOf`, `if`, `then`
35
+ * or `$ref`, every object closed with `additionalProperties: false` and listing all its
36
+ * properties as required, an optional property made nullable instead, and no numeric, length or
37
+ * pattern keywords. Constraints that can't stay as keywords move into the description, and a
38
+ * value with no portable shape, such as a free-form object, is taken as JSON text.
39
+ *
40
+ * Each schema comes with a {@link Decoder} that turns the arguments a model sends back into the
41
+ * value the API expects: a `null` the conversion added is dropped, and JSON text is parsed.
42
+ */
43
+ /** The string formats both APIs accept. */
44
+ const PORTABLE_FORMATS = /* @__PURE__ */ new Set([
45
+ "date-time",
46
+ "time",
47
+ "date",
48
+ "duration",
49
+ "email",
50
+ "hostname",
51
+ "ipv4",
52
+ "ipv6",
53
+ "uuid"
54
+ ]);
55
+ /** Thrown when a model's argument can't be decoded, such as JSON text that doesn't parse. */
56
+ var ArgumentError = class extends Error {
57
+ constructor(message) {
58
+ super(message);
59
+ this.name = "ArgumentError";
60
+ }
61
+ };
62
+ /** Turn a model's argument back into the value the API expects. */
63
+ function decode(value, decoder, path = "value") {
64
+ if (value === null || value === void 0) return value;
65
+ switch (decoder.kind) {
66
+ case "json":
67
+ if (typeof value !== "string") return value;
68
+ try {
69
+ return JSON.parse(value);
70
+ } catch {
71
+ throw new ArgumentError(`${path} must be JSON text`);
72
+ }
73
+ case "array": return Array.isArray(value) ? value.map((item, index) => decode(item, decoder.items, `${path}[${index}]`)) : value;
74
+ case "object": {
75
+ if (!isObject(value)) return value;
76
+ const out = {};
77
+ for (const [key, member] of Object.entries(value)) {
78
+ const prop = decoder.props[key];
79
+ if (prop === void 0) out[key] = member;
80
+ else if (!(member === null && prop.nullMeansOmit)) out[key] = decode(member, prop.decoder, `${path}.${key}`);
81
+ }
82
+ return out;
83
+ }
84
+ default: return value;
85
+ }
86
+ }
87
+ //#endregion
88
+ //#region src/inputs.ts
89
+ /**
90
+ * Each tool's input schema, and the plan the call handler follows to turn a model's arguments
91
+ * back into a client call. Every schema is portable (see ./portable.ts): a parameter the API
92
+ * doesn't require is nullable, and `null` at the top level means "not given".
93
+ */
94
+ /** The Countable profile's token for counting the collection itself. */
95
+ const SELF_COUNT = "_self_";
96
+ //#endregion
97
+ //#region src/flatten.ts
98
+ /**
99
+ * Flatten the client's materialised results into plain tool output. A resource becomes
100
+ * `{ id, type, ...attributes, relation }`, where a relation is an id (ids for a to-many, or type
101
+ * and id for a polymorphic relation), or the flattened related resource when it was included.
102
+ * Links and meta on resources are dropped; a collection keeps only its pagination links.
103
+ */
104
+ /** A materialised related value carries `$raw` when it was included, not when it's only linkage. */
105
+ const isIncluded = (value) => "$raw" in value;
106
+ function identifier(value, polymorphic) {
107
+ return polymorphic ? {
108
+ type: value["type"],
109
+ id: value["id"]
110
+ } : value["id"];
111
+ }
112
+ /**
113
+ * Flatten one materialised resource. A resource already on the path, or one more than
114
+ * `maxDepth` relationships from the root, is cut back to its id.
115
+ */
116
+ function flattenResource(value, descriptor, path = [], maxDepth = null) {
117
+ const type = String(value["type"]);
118
+ const key = `${type}:${String(value["id"])}`;
119
+ const relations = descriptor[type]?.relations ?? {};
120
+ const out = {
121
+ id: value["id"],
122
+ type
123
+ };
124
+ for (const [name, member] of Object.entries(value)) {
125
+ if (name === "id" || name === "type") continue;
126
+ const relation = relations[name];
127
+ if (relation === void 0) {
128
+ out[name] = member;
129
+ continue;
130
+ }
131
+ const polymorphic = (0, _haddowg_json_api_client.isPolymorphic)(relation);
132
+ const one = (item) => {
133
+ if (!isObject(item)) return item;
134
+ const itemKey = `${String(item["type"])}:${String(item["id"])}`;
135
+ return isIncluded(item) && itemKey !== key && !path.includes(itemKey) && (maxDepth === null || path.length < maxDepth) ? flattenResource(item, descriptor, [...path, key], maxDepth) : identifier(item, polymorphic);
136
+ };
137
+ out[name] = Array.isArray(member) ? member.map(one) : one(member);
138
+ }
139
+ return out;
140
+ }
141
+ const PAGE_LINKS = [
142
+ "first",
143
+ "prev",
144
+ "next",
145
+ "last"
146
+ ];
147
+ function linkHref(link) {
148
+ if (typeof link === "string") return link;
149
+ if (isObject(link) && typeof link["href"] === "string") return link["href"];
150
+ }
151
+ /** Flatten a materialised collection into `{ items, links, meta? }`. */
152
+ function flattenCollection(value, descriptor, maxDepth = null) {
153
+ const links = {};
154
+ for (const name of PAGE_LINKS) {
155
+ const href = linkHref(value.$links?.[name]);
156
+ if (href !== void 0) links[name] = href;
157
+ }
158
+ const out = {
159
+ items: value.map((item) => isObject(item) ? flattenResource(item, descriptor, [], maxDepth) : item),
160
+ links
161
+ };
162
+ if (value.$meta !== void 0) out["meta"] = value.$meta;
163
+ return out;
164
+ }
165
+ //#endregion
166
+ //#region src/call.ts
167
+ /** Thrown when a `pageLink` isn't a link this tool may follow. */
168
+ var PageLinkError = class extends Error {
169
+ constructor(message) {
170
+ super(message);
171
+ this.name = "PageLinkError";
172
+ }
173
+ };
174
+ const text = (value) => ({
175
+ type: "text",
176
+ text: value
177
+ });
178
+ function structured(value) {
179
+ return {
180
+ content: [text(JSON.stringify(value))],
181
+ structuredContent: value
182
+ };
183
+ }
184
+ function requireId(args) {
185
+ const id = args["id"];
186
+ if (typeof id === "string" && id !== "") return id;
187
+ if (typeof id === "number") return String(id);
188
+ throw new ArgumentError("id is required");
189
+ }
190
+ function stringList(value, name) {
191
+ if (value === null || value === void 0) return;
192
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new ArgumentError(`${name} must be a list of strings`);
193
+ return value;
194
+ }
195
+ /** The read query a model's arguments describe. */
196
+ function readQuery(args, plan) {
197
+ const query = {};
198
+ if (plan.filter !== void 0 && isObject(args["filter"])) {
199
+ const filter = decode(args["filter"], plan.filter, "filter");
200
+ for (const [key, value] of Object.entries(filter)) if (isObject(value)) {
201
+ const bounds = Object.fromEntries(Object.entries(value).filter(([, v]) => v !== null));
202
+ if (Object.keys(bounds).length === 0) delete filter[key];
203
+ else filter[key] = bounds;
204
+ }
205
+ if (Object.keys(filter).length > 0) query.filter = filter;
206
+ }
207
+ const sort = plan.sort ? stringList(args["sort"], "sort") : void 0;
208
+ if (sort !== void 0 && sort.length > 0) query.sort = sort;
209
+ const include = plan.include ? stringList(args["include"], "include") : void 0;
210
+ if (plan.includePaths !== void 0) {
211
+ const allowed = plan.includePaths;
212
+ const refused = (include ?? []).filter((path) => !allowed.includes(path));
213
+ if (refused.length > 0) throw new ArgumentError(`include can't name ${refused.map((p) => `"${p}"`).join(", ")}; this tool includes only ${allowed.join(", ")}`);
214
+ }
215
+ if (include !== void 0 && include.length > 0) query.include = include;
216
+ if (plan.fields.length > 0 && isObject(args["fields"])) {
217
+ const fields = {};
218
+ for (const [type, names] of Object.entries(args["fields"])) {
219
+ const list = stringList(names, `fields.${type}`);
220
+ if (list !== void 0) fields[type] = list;
221
+ }
222
+ if (Object.keys(fields).length > 0) query.fields = fields;
223
+ }
224
+ if (plan.countProfile !== void 0 && args["withCount"] === true) query.withCount = [SELF_COUNT];
225
+ if (plan.sizeKey !== void 0) {
226
+ const asked = args["pageSize"];
227
+ let size = typeof asked === "number" && Number.isFinite(asked) ? Math.trunc(asked) : plan.defaultPageSize;
228
+ size = Math.max(1, plan.maxPageSize === void 0 ? size : Math.min(size, plan.maxPageSize));
229
+ query.page = { [plan.sizeKey]: size };
230
+ }
231
+ return query;
232
+ }
233
+ /** The flat write input a model's arguments describe: relations as identifiers, nulls resolved. */
234
+ function writeInput(args, plan) {
235
+ const input = {};
236
+ for (const [name, field] of Object.entries(plan.fields)) {
237
+ const value = args[name];
238
+ if (value === void 0 || value === null && field.property.nullMeansOmit) continue;
239
+ if (value === null) {
240
+ input[name] = null;
241
+ continue;
242
+ }
243
+ const relation = field.relation;
244
+ if (relation === void 0) {
245
+ input[name] = decode(value, field.property.decoder, name);
246
+ continue;
247
+ }
248
+ const identify = (item) => {
249
+ if ((0, _haddowg_json_api_client.isPolymorphic)(relation)) {
250
+ if (!isObject(item) || typeof item["type"] !== "string" || typeof item["id"] !== "string") throw new ArgumentError(`${name} takes the related resource's type and id`);
251
+ return {
252
+ type: item["type"],
253
+ id: item["id"]
254
+ };
255
+ }
256
+ if (typeof item !== "string" && typeof item !== "number") throw new ArgumentError(`${name} takes the related resource's id`);
257
+ return {
258
+ type: relation.types[0],
259
+ id: String(item)
260
+ };
261
+ };
262
+ if (relation.cardinality === "many") {
263
+ if (!Array.isArray(value)) throw new ArgumentError(`${name} takes a list`);
264
+ input[name] = value.map(identify);
265
+ } else input[name] = identify(value);
266
+ }
267
+ for (const name of stringList(args["clear"], "clear") ?? []) {
268
+ if (!plan.clearable.includes(name)) throw new ArgumentError(`clear can't name "${name}"`);
269
+ input[name] = null;
270
+ }
271
+ if (plan.clientId !== "none" && args["id"] !== null && args["id"] !== void 0) input["id"] = String(args["id"]);
272
+ return input;
273
+ }
274
+ /** Escape a string for a regular expression. */
275
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
276
+ /**
277
+ * Check a `pageLink` against the tool's own endpoint: the base URL's origin and the endpoint's
278
+ * path, with any id in it. Returns the absolute URL to fetch.
279
+ */
280
+ function checkPageLink(link, baseUrl, template) {
281
+ if (typeof link !== "string" || link === "") throw new PageLinkError("pageLink must be a link string");
282
+ let base;
283
+ try {
284
+ base = new URL(baseUrl);
285
+ } catch {
286
+ throw new PageLinkError("pageLink needs an absolute base URL to be checked against");
287
+ }
288
+ let url;
289
+ try {
290
+ url = new URL(link, base);
291
+ } catch {
292
+ throw new PageLinkError("pageLink is not a valid URL");
293
+ }
294
+ const prefix = base.pathname.replace(/\/$/, "");
295
+ const pattern = template.split(/(\{[^}]+\})/).map((part) => /^\{[^}]+\}$/.test(part) ? "[^/]+" : escape(part)).join("");
296
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.origin !== base.origin || !new RegExp(`^${escape(prefix)}${pattern}$`).test(url.pathname)) throw new PageLinkError("pageLink must be one of the links this tool returned; it points somewhere else");
297
+ return url.href;
298
+ }
299
+ /** The descriptor entry a call sends this action with: the operation's method and the body plan picked. */
300
+ function effectiveAction(entry) {
301
+ const { inputMedia: _inputMedia, inputType, contentType: _contentType, ...action } = entry.op.actionDescriptor;
302
+ const plan = entry.input.action;
303
+ const base = {
304
+ ...action,
305
+ method: entry.op.method
306
+ };
307
+ switch (plan.kind) {
308
+ case "none": return {
309
+ ...base,
310
+ input: "none"
311
+ };
312
+ case "flat": return inputType === void 0 ? {
313
+ ...base,
314
+ input: "document"
315
+ } : {
316
+ ...base,
317
+ input: "document",
318
+ inputType
319
+ };
320
+ case "body": return plan.mediaKind === "document" ? {
321
+ ...base,
322
+ input: "document"
323
+ } : {
324
+ ...base,
325
+ input: "raw",
326
+ contentType: plan.mediaType
327
+ };
328
+ }
329
+ }
330
+ function withAction(descriptor, entry) {
331
+ const resource = descriptor[entry.op.type];
332
+ return {
333
+ ...descriptor,
334
+ [entry.op.type]: {
335
+ ...resource,
336
+ actions: {
337
+ ...resource.actions,
338
+ [entry.actionKey]: effectiveAction(entry)
339
+ }
340
+ }
341
+ };
342
+ }
343
+ async function binaryContent(value, uri, mimeType) {
344
+ const bytes = value instanceof ArrayBuffer ? value : await value.arrayBuffer();
345
+ const data = Buffer.from(bytes).toString("base64");
346
+ return mimeType.startsWith("image/") ? {
347
+ type: "image",
348
+ data,
349
+ mimeType
350
+ } : {
351
+ type: "resource",
352
+ resource: {
353
+ uri,
354
+ mimeType,
355
+ blob: data
356
+ }
357
+ };
358
+ }
359
+ /** Run one tool call against the API. Throws what the call failed with. */
360
+ async function runCall(entry, rawArgs, env) {
361
+ if (rawArgs !== void 0 && rawArgs !== null && !isObject(rawArgs)) throw new ArgumentError("the arguments must be an object");
362
+ const args = rawArgs ?? {};
363
+ if (env.baseUrl === void 0) throw new Error("No base URL: pass baseUrl, return one from requestInit, or give the document a server");
364
+ const { op } = entry;
365
+ const descriptor = op.kind === "action" ? withAction(env.descriptor, entry) : env.descriptor;
366
+ const headers = env.headers;
367
+ const accessor = (0, _haddowg_json_api_client.createClient)(descriptor, {
368
+ baseUrl: env.baseUrl,
369
+ ...env.transport !== void 0 ? { transport: env.transport } : {},
370
+ ...headers !== void 0 ? { headers: () => headers } : {}
371
+ })[op.type];
372
+ const query = entry.input.query === void 0 ? void 0 : readQuery(args, entry.input.query);
373
+ if (query !== void 0 && env.signal !== void 0) query.signal = env.signal;
374
+ switch (op.kind) {
375
+ case "list":
376
+ case "related": {
377
+ if (entry.result === "item") {
378
+ const related = await accessor.id(requireId(args)).rel(op.relation).related(query);
379
+ return structured({ item: isObject(related) ? flattenResource(related, env.descriptor, [], env.includeDepth) : null });
380
+ }
381
+ let collection;
382
+ if (args["pageLink"] !== void 0 && args["pageLink"] !== null && entry.input.query?.paginated) {
383
+ const url = checkPageLink(args["pageLink"], env.baseUrl, op.path);
384
+ const doc = await (0, _haddowg_json_api_client.execute)({
385
+ baseUrl: env.baseUrl,
386
+ transport: env.transport ?? _haddowg_json_api_client.fetchTransport,
387
+ ...headers !== void 0 ? { headers: () => headers } : {}
388
+ }, {
389
+ method: "GET",
390
+ path: url,
391
+ link: true,
392
+ ...entry.input.query?.countProfile !== void 0 && new URL(url).searchParams.has("withCount") ? { profiles: [entry.input.query.countProfile] } : {},
393
+ ...env.signal !== void 0 ? { signal: env.signal } : {}
394
+ });
395
+ collection = doc === void 0 ? [] : (0, _haddowg_json_api_client.materialise)(doc, {
396
+ descriptor: env.descriptor,
397
+ navigate: () => Promise.reject(/* @__PURE__ */ new Error("not navigable"))
398
+ });
399
+ } else if (op.kind === "list") collection = await accessor.list(query);
400
+ else collection = await accessor.id(requireId(args)).rel(op.relation).related(query);
401
+ return structured(flattenCollection(Array.isArray(collection) ? collection : [], env.descriptor, env.includeDepth));
402
+ }
403
+ case "get": {
404
+ const resource = await accessor.get(requireId(args), query);
405
+ return structured({ item: isObject(resource) ? flattenResource(resource, env.descriptor, [], env.includeDepth) : null });
406
+ }
407
+ case "create": {
408
+ const input = writeInput(args, entry.input.write);
409
+ const created = await accessor.create(input);
410
+ return structured(isObject(created) ? flattenResource(created, env.descriptor, [], env.includeDepth) : {
411
+ id: String(input["id"] ?? ""),
412
+ type: op.type
413
+ });
414
+ }
415
+ case "update": {
416
+ const id = requireId(args);
417
+ const updated = await accessor.id(id).update(writeInput(args, entry.input.write));
418
+ return structured(isObject(updated) ? flattenResource(updated, env.descriptor, [], env.includeDepth) : {
419
+ id,
420
+ type: op.type
421
+ });
422
+ }
423
+ case "delete": {
424
+ const id = requireId(args);
425
+ await accessor.id(id).delete();
426
+ return { content: [text(`Deleted ${op.type} ${id}.`)] };
427
+ }
428
+ case "action": return runAction(entry, args, accessor, env);
429
+ }
430
+ }
431
+ async function runAction(entry, args, accessor, env) {
432
+ const { op } = entry;
433
+ const action = op.actionDescriptor;
434
+ const call = (action.scope === "resource" ? accessor.id(requireId(args)).actions : accessor.actions)[entry.actionKey];
435
+ const options = env.signal === void 0 ? {} : { signal: env.signal };
436
+ const plan = entry.input.action;
437
+ let result;
438
+ if (plan.kind === "none") result = await call(options);
439
+ else {
440
+ const body = plan.kind === "flat" ? writeInput(args, plan.write) : args["body"] === null ? void 0 : decode(args["body"], plan.decoder, "body");
441
+ if (plan.kind === "body" && plan.required && body === void 0) throw new ArgumentError("body is required");
442
+ result = await call(body, options);
443
+ }
444
+ if (action.location !== void 0 && (0, _haddowg_json_api_client.isActionLocation)(result)) return structured({ ...result });
445
+ switch (entry.result) {
446
+ case "collection": return structured(flattenCollection(Array.isArray(result) ? result : [], env.descriptor, env.includeDepth));
447
+ case "resource":
448
+ case "loose":
449
+ if (Array.isArray(result)) return structured(flattenCollection(result, env.descriptor, env.includeDepth));
450
+ return isObject(result) ? structured(flattenResource(result, env.descriptor, [], env.includeDepth)) : { content: [text("The action returned no resource.")] };
451
+ case "meta": return structured(isObject(result) ? result : {});
452
+ case "raw": {
453
+ const mimeType = action.rawOutput?.[0]?.mediaType ?? "application/octet-stream";
454
+ if (result === void 0) return { content: [text("Done.")] };
455
+ if (typeof result === "string") return { content: [text(result)] };
456
+ if (result instanceof ArrayBuffer || typeof Blob !== "undefined" && result instanceof Blob) return { content: [await binaryContent(result, `${env.baseUrl}${action.path}`, mimeType)] };
457
+ return { content: [text(JSON.stringify(result))] };
458
+ }
459
+ default: return { content: [text("Done.")] };
460
+ }
461
+ }
462
+ /** The error a failed call carries, before the host's `mapError`. */
463
+ function toToolError(error) {
464
+ if (error instanceof _haddowg_json_api_client.JsonApiError) return {
465
+ status: error.status,
466
+ message: error.message,
467
+ errors: error.errors.map((e) => {
468
+ const out = {};
469
+ if (e.status !== void 0) out.status = e.status;
470
+ if (e.code !== void 0) out.code = e.code;
471
+ if (e.title !== void 0) out.title = e.title;
472
+ if (e.detail !== void 0) out.detail = e.detail;
473
+ if (e.source !== void 0) out.source = e.source;
474
+ if (e.path !== void 0) out.path = e.path;
475
+ return out;
476
+ }),
477
+ cause: error
478
+ };
479
+ return {
480
+ message: error instanceof Error ? error.message : String(error),
481
+ errors: [],
482
+ cause: error
483
+ };
484
+ }
485
+ /** A tool error result: the message, the status and each error object, as JSON text. */
486
+ function errorResult(error) {
487
+ const body = { error: error.message };
488
+ if (error.status !== void 0) body["status"] = error.status;
489
+ if (error.errors.length > 0) body["errors"] = error.errors;
490
+ return {
491
+ content: [text(JSON.stringify(body))],
492
+ isError: true
493
+ };
494
+ }
495
+ //#endregion
496
+ //#region src/model.ts
497
+ /** The artefact's own format. A runtime refuses a format it doesn't know. */
498
+ const MODEL_FORMAT = 3;
499
+ /**
500
+ * Every format this runtime reads. Format 1 has no `credentialSchemes`. Formats 1 and 2 have no
501
+ * `includeDepth`, and store each operation's `scopes` and the `security` of its operation object.
502
+ */
503
+ const READABLE_FORMATS = [
504
+ 1,
505
+ 2,
506
+ 3
507
+ ];
508
+ /** The package that writes the artefact. */
509
+ const MODEL_GENERATOR = "@haddowg/json-api-mcp";
510
+ /** Thrown when an artefact can't be read: another format, or a contract out of range. */
511
+ var McpModelError = class extends Error {
512
+ constructor(message) {
513
+ super(message);
514
+ this.name = "McpModelError";
515
+ }
516
+ };
517
+ /** Check a parsed artefact before the runtime trusts it. */
518
+ function assertModel(value) {
519
+ if (!isObject(value) || value["generator"] !== "@haddowg/json-api-mcp") throw new McpModelError(`This isn't a model written by ${MODEL_GENERATOR}. Generate one with the json-api-mcp CLI.`);
520
+ if (typeof value["format"] !== "number" || !READABLE_FORMATS.includes(value["format"])) throw new McpModelError(`The model is format ${String(value["format"])} and this runtime reads formats ${READABLE_FORMATS.slice(0, -1).join(", ")} and 3. Regenerate it with the same version of ${MODEL_GENERATOR} the server runs.`);
521
+ const contract = value["contract"];
522
+ if (typeof contract !== "number" || contract < 13 || contract > 18) throw new McpModelError(`The model was built from a document with generator contract ${String(contract)}, and this runtime reads contracts 13 to 18.`);
523
+ if (!isObject(value["descriptor"]) || !Array.isArray(value["tools"])) throw new McpModelError("The model has no descriptor or tools; regenerate it.");
524
+ const credentialSchemes = value["credentialSchemes"];
525
+ if (value["format"] !== 1 && (!Array.isArray(credentialSchemes) || !credentialSchemes.every((s) => typeof s === "string"))) throw new McpModelError("The model has no credentialSchemes list; regenerate it.");
526
+ const includeDepth = value["includeDepth"];
527
+ if (value["format"] === 3 && includeDepth !== null && !(typeof includeDepth === "number" && Number.isInteger(includeDepth) && includeDepth >= 0)) throw new McpModelError("The model has no includeDepth; regenerate it.");
528
+ return value;
529
+ }
530
+ /**
531
+ * The schemes a format 1 model's caller is taken to authenticate with, since it doesn't record
532
+ * them: every scheme some operation lists scopes under.
533
+ */
534
+ function inferCredentialSchemes(model) {
535
+ const schemes = /* @__PURE__ */ new Set();
536
+ for (const tool of model.tools) for (const requirement of tool.operation.security ?? []) for (const [scheme, scopes] of Object.entries(requirement)) if (scopes.length > 0) schemes.add(scheme);
537
+ return [...schemes];
538
+ }
539
+ //#endregion
540
+ //#region src/scopes.ts
541
+ /** The descriptor's key for each kind's operation in a type's `scopes`. */
542
+ const SCOPE_KEYS = {
543
+ list: "fetchCollection",
544
+ get: "fetchOne",
545
+ create: "create",
546
+ update: "update",
547
+ delete: "delete"
548
+ };
549
+ /** The scopes the descriptor says an operation needs, or `undefined` when it names none. */
550
+ function descriptorScopes(op) {
551
+ let scopes;
552
+ if (op.kind === "action") scopes = op.actionDescriptor?.scopes;
553
+ else if (op.kind === "related") scopes = op.resource.scopes?.[`fetchRelated.${op.relation ?? ""}`];
554
+ else scopes = op.resource.scopes?.[SCOPE_KEYS[op.kind]];
555
+ return scopes === void 0 || scopes.length === 0 ? void 0 : scopes;
556
+ }
557
+ //#endregion
558
+ //#region src/mcp.ts
559
+ /** Every required scope appears among the granted ones. */
560
+ const exactScopeMatcher = (granted, required) => required.every((scope) => granted.includes(scope));
561
+ /**
562
+ * Whether granted scopes satisfy an operation's security. No requirement, or an empty one, needs
563
+ * nothing. Given `credentialSchemes`, the schemes the caller authenticates with, a requirement
564
+ * counts only when it names nothing else, so an operation whose every requirement needs another
565
+ * credential fails. A counted requirement passes when its scopes, across its schemes, all match,
566
+ * or when it lists none. Without `credentialSchemes` every requirement counts.
567
+ */
568
+ function satisfiesSecurity(security, granted, matcher = exactScopeMatcher, credentialSchemes) {
569
+ if (security.length === 0) return true;
570
+ return security.some((requirement) => {
571
+ const schemes = Object.keys(requirement);
572
+ if (schemes.length === 0) return true;
573
+ if (credentialSchemes !== void 0 && !schemes.every((scheme) => credentialSchemes.includes(scheme))) return false;
574
+ const scopes = Object.values(requirement).flat();
575
+ return scopes.length === 0 || matcher(granted, scopes);
576
+ });
577
+ }
578
+ /**
579
+ * The schemes the scope filter counts: the host's, else the model's, else for a format 1 model
580
+ * the schemes it lists scopes under. `undefined`, so every requirement counts, when that's empty.
581
+ */
582
+ function resolveCredentialSchemes(model, override) {
583
+ if (override !== void 0) return override;
584
+ const schemes = model.credentialSchemes ?? inferCredentialSchemes(model);
585
+ return schemes.length > 0 ? schemes : void 0;
586
+ }
587
+ /** Rebuild an operation for the hooks, adding back the descriptor entries it names. */
588
+ function hydrate(model, tool) {
589
+ const source = tool.operation;
590
+ const resource = model.descriptor[source.type];
591
+ if (resource === void 0) throw new McpModelError(`The model's descriptor has no type "${source.type}"; regenerate it.`);
592
+ const security = source.security ?? model.security ?? [];
593
+ const operation = model.format < 3 || source.security === void 0 ? source.operation : {
594
+ ...source.operation,
595
+ security: source.security
596
+ };
597
+ const op = {
598
+ operationId: source.operationId,
599
+ toolName: source.toolName,
600
+ kind: source.kind,
601
+ type: source.type,
602
+ method: source.method,
603
+ path: source.path,
604
+ resource,
605
+ operation,
606
+ security
607
+ };
608
+ if (source.relation !== void 0) {
609
+ op.relation = source.relation;
610
+ const relation = resource.relations[source.relation];
611
+ if (relation !== void 0) op.relationDescriptor = relation;
612
+ }
613
+ if (source.action !== void 0) {
614
+ op.action = source.action;
615
+ const action = resource.actions?.[source.actionKey ?? source.action];
616
+ if (action !== void 0) op.actionDescriptor = action;
617
+ }
618
+ const scopes = source.scopes ?? descriptorScopes(op);
619
+ if (scopes !== void 0) op.scopes = scopes;
620
+ const entry = {
621
+ op,
622
+ tool: tool.tool,
623
+ input: tool.plan,
624
+ result: tool.result
625
+ };
626
+ if (source.actionKey !== void 0) entry.actionKey = source.actionKey;
627
+ return entry;
628
+ }
629
+ /**
630
+ * Serve MCP tools from a model the generator wrote: `createJsonApiMcp({ model })` with the parsed
631
+ * artefact. Throws when the artefact isn't one this runtime can read.
632
+ */
633
+ function createJsonApiMcp(options) {
634
+ const { hooks = {} } = options;
635
+ const model = assertModel(options.model);
636
+ const descriptor = model.descriptor;
637
+ const catalogue = { entries: model.tools.map((tool) => hydrate(model, tool)) };
638
+ const defaultBaseUrl = options.baseUrl ?? model.baseUrl;
639
+ const credentialSchemes = resolveCredentialSchemes(model, options.credentialSchemes);
640
+ const includeDepth = model.includeDepth ?? null;
641
+ async function forRequest(request) {
642
+ const ctx = request.context;
643
+ let entries = catalogue.entries;
644
+ if (request.grantedScopes !== void 0) {
645
+ const granted = request.grantedScopes ?? [];
646
+ entries = entries.filter((e) => satisfiesSecurity(e.op.security, granted, hooks.scopeMatcher, credentialSchemes));
647
+ }
648
+ if (hooks.capabilities !== void 0) {
649
+ const capabilities = await hooks.capabilities(ctx);
650
+ const lookup = (id) => capabilities instanceof Map ? capabilities.get(id) : capabilities[id];
651
+ entries = entries.filter((e) => lookup(e.op.operationId) !== "denied");
652
+ }
653
+ if (hooks.includeOperation !== void 0) {
654
+ const include = hooks.includeOperation;
655
+ const keep = await Promise.all(entries.map((e) => include(e.op, ctx)));
656
+ entries = entries.filter((_, index) => keep[index] === true);
657
+ }
658
+ const byName = /* @__PURE__ */ new Map();
659
+ const tools = [];
660
+ for (const entry of entries) {
661
+ const tool = hooks.decorateTool === void 0 ? entry.tool : await hooks.decorateTool(structuredClone(entry.tool), entry.op, ctx);
662
+ tools.push(tool);
663
+ byName.set(tool.name, entry);
664
+ }
665
+ async function callTool(name, args, callOptions = {}) {
666
+ const entry = byName.get(name);
667
+ if (entry === void 0) return errorResult({
668
+ message: `Unknown tool "${name}"`,
669
+ errors: [],
670
+ cause: void 0
671
+ });
672
+ try {
673
+ const init = hooks.requestInit === void 0 ? {} : await hooks.requestInit(entry.op, ctx);
674
+ return await runCall(entry, args, {
675
+ descriptor,
676
+ baseUrl: init.baseUrl ?? defaultBaseUrl,
677
+ transport: options.transport,
678
+ headers: init.headers,
679
+ signal: callOptions.signal,
680
+ includeDepth
681
+ });
682
+ } catch (error) {
683
+ const toolError = toToolError(error);
684
+ return errorResult(hooks.mapError === void 0 ? toolError : await hooks.mapError(toolError, entry.op, ctx));
685
+ }
686
+ }
687
+ return {
688
+ tools,
689
+ callTool
690
+ };
691
+ }
692
+ return {
693
+ operations: catalogue.entries.map((e) => e.op),
694
+ tools: catalogue.entries.map((e) => e.tool),
695
+ skipped: model.skipped,
696
+ contract: model.contract,
697
+ source: model.source,
698
+ credentialSchemes,
699
+ forRequest
700
+ };
701
+ }
702
+ //#endregion
703
+ //#region src/names.ts
704
+ /** The name pattern OpenAI's and Anthropic's tool names share. */
705
+ const TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
706
+ const CRUD = {
707
+ fetchCollection: "list",
708
+ fetchOne: "get",
709
+ create: "create",
710
+ update: "update",
711
+ delete: "delete"
712
+ };
713
+ /**
714
+ * Parse an `operationId` into what a tool needs. `undefined` for an operation that isn't a tool:
715
+ * relationship endpoints, Atomic Operations, or an id this package doesn't recognise.
716
+ */
717
+ function parseOperationId(operationId) {
718
+ const parts = operationId.split(".");
719
+ const [verb, type] = parts;
720
+ if (verb === void 0 || type === void 0 || type === "") return;
721
+ const crud = CRUD[verb];
722
+ if (crud !== void 0) return parts.length === 2 ? {
723
+ kind: crud,
724
+ type
725
+ } : void 0;
726
+ if (verb === "fetchRelated" && parts.length === 3 && parts[2] !== "") return {
727
+ kind: "related",
728
+ type,
729
+ relation: parts[2]
730
+ };
731
+ if (verb === "action" && parts[2] !== void 0 && parts[2] !== "") {
732
+ if (parts.length === 3) return {
733
+ kind: "action",
734
+ type,
735
+ action: parts[2]
736
+ };
737
+ const method = parts[3];
738
+ if (parts.length === 4 && HTTP_METHODS.includes(method)) return {
739
+ kind: "action",
740
+ type,
741
+ action: parts[2],
742
+ method
743
+ };
744
+ }
745
+ }
746
+ const snake = (value) => value.replace(/-/g, "_");
747
+ /**
748
+ * The tool name for a parsed `operationId`: noun first, the type as the document spells it with
749
+ * kebab-case turned into snake_case. A related read ends in `list` or `get` by the relation's
750
+ * cardinality; an action is named after its path, plus the method for a multi-method action.
751
+ */
752
+ function toolName(parsed, descriptor) {
753
+ const type = snake(parsed.type);
754
+ switch (parsed.kind) {
755
+ case "related": {
756
+ const verb = (descriptor[parsed.type]?.relations[parsed.relation])?.cardinality === "one" ? "get" : "list";
757
+ return `${type}_${snake(parsed.relation)}_${verb}`;
758
+ }
759
+ case "action": return parsed.method === void 0 ? `${type}_${snake(parsed.action)}` : `${type}_${snake(parsed.action)}_${parsed.method}`;
760
+ default: return `${type}_${parsed.kind}`;
761
+ }
762
+ }
763
+ /** Thrown when the tools can't be named: a name too long, with a stray character, or a clash. */
764
+ var ToolNameError = class extends Error {
765
+ problems;
766
+ constructor(problems) {
767
+ super(`Cannot build MCP tools from this document:\n- ${problems.join("\n- ")}`);
768
+ this.name = "ToolNameError";
769
+ this.problems = problems;
770
+ }
771
+ };
772
+ //#endregion
773
+ //#region src/safety.ts
774
+ /** The vendor extension an action's operation declares its safety in. */
775
+ const SAFETY_EXTENSION = "x-safety";
776
+ /**
777
+ * Read an action's declared safety from its operation. An operation without the extension, or
778
+ * with one this package can't read, declares nothing.
779
+ */
780
+ function readActionSafety(operation) {
781
+ const raw = operation[SAFETY_EXTENSION];
782
+ const safety = {};
783
+ if (!isObject(raw)) return safety;
784
+ for (const key of [
785
+ "readOnly",
786
+ "destructive",
787
+ "idempotent"
788
+ ]) if (typeof raw[key] === "boolean") safety[key] = raw[key];
789
+ return safety;
790
+ }
791
+ //#endregion
792
+ exports.MAXIMUM_CONTRACT = MAXIMUM_CONTRACT;
793
+ exports.MINIMUM_CONTRACT = MINIMUM_CONTRACT;
794
+ exports.MODEL_FORMAT = MODEL_FORMAT;
795
+ exports.MODEL_GENERATOR = MODEL_GENERATOR;
796
+ exports.McpModelError = McpModelError;
797
+ exports.PORTABLE_FORMATS = PORTABLE_FORMATS;
798
+ exports.SAFETY_EXTENSION = SAFETY_EXTENSION;
799
+ exports.TOOL_NAME_PATTERN = TOOL_NAME_PATTERN;
800
+ exports.ToolNameError = ToolNameError;
801
+ exports.assertContract = assertContract;
802
+ exports.assertModel = assertModel;
803
+ exports.createJsonApiMcp = createJsonApiMcp;
804
+ exports.exactScopeMatcher = exactScopeMatcher;
805
+ exports.parseOperationId = parseOperationId;
806
+ exports.readActionSafety = readActionSafety;
807
+ exports.satisfiesSecurity = satisfiesSecurity;
808
+ exports.toolName = toolName;