@haddowg/json-api-mcp 1.2.1 → 1.4.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/README.md +16 -2
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{generate-CuWnYJUB.d.mts → generate-D0kx-DHv.d.mts} +8 -3
- package/dist/{generate-CuWnYJUB.d.mts.map → generate-D0kx-DHv.d.mts.map} +1 -1
- package/dist/{generate-CiLuqQJ0.mjs → generate-yf5Kc4Yh.mjs} +169 -46
- package/dist/generate-yf5Kc4Yh.mjs.map +1 -0
- package/dist/generate.d.mts +1 -1
- package/dist/generate.mjs +1 -1
- package/dist/index.cjs +201 -56
- package/dist/index.d.cts +8 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +8 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +201 -56
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
- package/dist/generate-CiLuqQJ0.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { JsonApiError, createClient, execute, fetchTransport, isActionLocation,
|
|
|
3
3
|
/** The oldest generator contract this package reads: the baseline current servers stamp. */
|
|
4
4
|
const MINIMUM_CONTRACT = 13;
|
|
5
5
|
/** The newest generator contract this package reads. */
|
|
6
|
-
const MAXIMUM_CONTRACT =
|
|
6
|
+
const MAXIMUM_CONTRACT = 19;
|
|
7
7
|
/**
|
|
8
8
|
* Throw unless the document's `info.x-generator.contract` is in the range this package reads. A
|
|
9
9
|
* document outside it would yield tools that miss or misread what the server offers, so there is
|
|
@@ -11,9 +11,9 @@ const MAXIMUM_CONTRACT = 18;
|
|
|
11
11
|
*/
|
|
12
12
|
function assertContract(doc) {
|
|
13
13
|
const contract = doc.info?.["x-generator"]?.contract;
|
|
14
|
-
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
|
|
15
|
-
if (contract < 13) throw new Error(`The OpenAPI document declares generator contract ${contract}, older than this package reads (13 to
|
|
16
|
-
if (contract >
|
|
14
|
+
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 19; regenerate the document from a newer haddowg/json-api server.`);
|
|
15
|
+
if (contract < 13) throw new Error(`The OpenAPI document declares generator contract ${contract}, older than this package reads (13 to 19). Regenerate it from a newer haddowg/json-api server.`);
|
|
16
|
+
if (contract > 19) throw new Error(`The OpenAPI document declares generator contract ${contract}, newer than this package reads (13 to 19). Upgrade @haddowg/json-api-mcp.`);
|
|
17
17
|
return contract;
|
|
18
18
|
}
|
|
19
19
|
//#endregion
|
|
@@ -91,9 +91,11 @@ const SELF_COUNT = "_self_";
|
|
|
91
91
|
//#region src/flatten.ts
|
|
92
92
|
/**
|
|
93
93
|
* Flatten the client's materialised results into plain tool output. A resource becomes
|
|
94
|
-
* `{ id, type, ...attributes, relation }`, where a relation is an id (ids for a to-many,
|
|
95
|
-
* and id for a polymorphic relation), or the flattened related resource when it was
|
|
96
|
-
*
|
|
94
|
+
* `{ id, type, ...attributes, relation, links? }`, where a relation is an id (ids for a to-many,
|
|
95
|
+
* or type and id for a polymorphic relation), or the flattened related resource when it was
|
|
96
|
+
* included, and `links` holds only the links the type names. Generic links such as `self` and
|
|
97
|
+
* resource meta are dropped. Results go back in a JSON:API-like envelope of `data`, `links` and
|
|
98
|
+
* `meta`, where `links` only carries a collection's pagination.
|
|
97
99
|
*/
|
|
98
100
|
/** A materialised related value carries `$raw` when it was included, not when it's only linkage. */
|
|
99
101
|
const isIncluded = (value) => "$raw" in value;
|
|
@@ -103,14 +105,36 @@ function identifier(value, polymorphic) {
|
|
|
103
105
|
id: value["id"]
|
|
104
106
|
} : value["id"];
|
|
105
107
|
}
|
|
108
|
+
/** A link as a flattened resource carries it: `{ href, type?, title?, meta? }`. */
|
|
109
|
+
function flatLink(link) {
|
|
110
|
+
if (typeof link === "string") return { href: link };
|
|
111
|
+
if (!isObject(link) || typeof link["href"] !== "string") return;
|
|
112
|
+
const out = { href: link["href"] };
|
|
113
|
+
if (typeof link["type"] === "string") out["type"] = link["type"];
|
|
114
|
+
if (typeof link["title"] === "string") out["title"] = link["title"];
|
|
115
|
+
if (isObject(link["meta"])) out["meta"] = link["meta"];
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/** The links of a resource its type names, or undefined when it carries none of them. */
|
|
119
|
+
function namedLinks(value, names) {
|
|
120
|
+
const links = value.$links;
|
|
121
|
+
if (names === void 0 || !isObject(links)) return;
|
|
122
|
+
const out = {};
|
|
123
|
+
for (const name of names) {
|
|
124
|
+
const link = flatLink(links[name]);
|
|
125
|
+
if (link !== void 0) out[name] = link;
|
|
126
|
+
}
|
|
127
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
128
|
+
}
|
|
106
129
|
/**
|
|
107
130
|
* Flatten one materialised resource. A resource already on the path, or one more than
|
|
108
|
-
* `
|
|
131
|
+
* `includeDepth` relationships from the root, is cut back to its id.
|
|
109
132
|
*/
|
|
110
|
-
function flattenResource(value,
|
|
133
|
+
function flattenResource(value, ctx, path = []) {
|
|
111
134
|
const type = String(value["type"]);
|
|
112
135
|
const key = `${type}:${String(value["id"])}`;
|
|
113
|
-
const relations = descriptor[type]?.relations ?? {};
|
|
136
|
+
const relations = ctx.descriptor[type]?.relations ?? {};
|
|
137
|
+
const maxDepth = ctx.includeDepth;
|
|
114
138
|
const out = {
|
|
115
139
|
id: value["id"],
|
|
116
140
|
type
|
|
@@ -126,10 +150,12 @@ function flattenResource(value, descriptor, path = [], maxDepth = null) {
|
|
|
126
150
|
const one = (item) => {
|
|
127
151
|
if (!isObject(item)) return item;
|
|
128
152
|
const itemKey = `${String(item["type"])}:${String(item["id"])}`;
|
|
129
|
-
return isIncluded(item) && itemKey !== key && !path.includes(itemKey) && (maxDepth === null || path.length < maxDepth) ? flattenResource(item,
|
|
153
|
+
return isIncluded(item) && itemKey !== key && !path.includes(itemKey) && (maxDepth === null || path.length < maxDepth) ? flattenResource(item, ctx, [...path, key]) : identifier(item, polymorphic);
|
|
130
154
|
};
|
|
131
155
|
out[name] = Array.isArray(member) ? member.map(one) : one(member);
|
|
132
156
|
}
|
|
157
|
+
const links = namedLinks(value, ctx.resourceLinks[type]);
|
|
158
|
+
if (links !== void 0) out["links"] = links;
|
|
133
159
|
return out;
|
|
134
160
|
}
|
|
135
161
|
const PAGE_LINKS = [
|
|
@@ -142,15 +168,30 @@ function linkHref(link) {
|
|
|
142
168
|
if (typeof link === "string") return link;
|
|
143
169
|
if (isObject(link) && typeof link["href"] === "string") return link["href"];
|
|
144
170
|
}
|
|
145
|
-
/**
|
|
146
|
-
function
|
|
171
|
+
/** The document `meta` a materialised resource came with. */
|
|
172
|
+
function resourceDocumentMeta(value) {
|
|
173
|
+
const document = isObject(value) ? value.$document : void 0;
|
|
174
|
+
return isObject(document?.meta) ? document.meta : void 0;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* One resource in its envelope: `{ data, meta? }`, with `data` null when there is none.
|
|
178
|
+
* `fallbackMeta` is the document meta when the value can't carry it, such as a null.
|
|
179
|
+
*/
|
|
180
|
+
function resourceEnvelope(value, ctx, fallbackMeta) {
|
|
181
|
+
const out = { data: isObject(value) ? flattenResource(value, ctx) : null };
|
|
182
|
+
const meta = resourceDocumentMeta(value) ?? fallbackMeta;
|
|
183
|
+
if (meta !== void 0) out["meta"] = meta;
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
/** A materialised collection in its envelope: `{ data, links, meta? }`. */
|
|
187
|
+
function collectionEnvelope(value, ctx) {
|
|
147
188
|
const links = {};
|
|
148
189
|
for (const name of PAGE_LINKS) {
|
|
149
190
|
const href = linkHref(value.$links?.[name]);
|
|
150
191
|
if (href !== void 0) links[name] = href;
|
|
151
192
|
}
|
|
152
193
|
const out = {
|
|
153
|
-
|
|
194
|
+
data: value.map((item) => isObject(item) ? flattenResource(item, ctx) : item),
|
|
154
195
|
links
|
|
155
196
|
};
|
|
156
197
|
if (value.$meta !== void 0) out["meta"] = value.$meta;
|
|
@@ -175,6 +216,47 @@ function structured(value) {
|
|
|
175
216
|
structuredContent: value
|
|
176
217
|
};
|
|
177
218
|
}
|
|
219
|
+
/** The `meta` of a JSON:API response body, when it has one. */
|
|
220
|
+
function responseMeta(response) {
|
|
221
|
+
if (response === void 0 || response.body === "") return;
|
|
222
|
+
try {
|
|
223
|
+
const body = JSON.parse(response.body);
|
|
224
|
+
return isObject(body) && isObject(body["meta"]) ? body["meta"] : void 0;
|
|
225
|
+
} catch {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A result in the shape a model before format 4 describes: `{ item }` for a single read,
|
|
231
|
+
* `{ items, links, meta }` for a collection, the bare resource, meta object or URL otherwise,
|
|
232
|
+
* and a delete as text.
|
|
233
|
+
*/
|
|
234
|
+
function legacyResult(entry, envelope, location, id) {
|
|
235
|
+
const data = envelope["data"];
|
|
236
|
+
if (location || entry.result === "resource" || entry.result === "loose") {
|
|
237
|
+
if (Array.isArray(data)) {
|
|
238
|
+
const { data: items, ...rest } = envelope;
|
|
239
|
+
return structured({
|
|
240
|
+
items,
|
|
241
|
+
...rest
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return isObject(data) ? structured(data) : { content: [text("The action returned no resource.")] };
|
|
245
|
+
}
|
|
246
|
+
switch (entry.result) {
|
|
247
|
+
case "item": return structured({ item: data ?? null });
|
|
248
|
+
case "collection": {
|
|
249
|
+
const { data: items, ...rest } = envelope;
|
|
250
|
+
return structured({
|
|
251
|
+
items,
|
|
252
|
+
...rest
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
case "deleted": return { content: [text(`Deleted ${entry.op.type} ${String(id)}.`)] };
|
|
256
|
+
case "meta": return structured(isObject(envelope["meta"]) ? envelope["meta"] : {});
|
|
257
|
+
default: return structured(envelope);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
178
260
|
function requireId(args) {
|
|
179
261
|
const id = args["id"];
|
|
180
262
|
if (typeof id === "string" && id !== "") return id;
|
|
@@ -355,29 +437,62 @@ async function runCall(entry, rawArgs, env) {
|
|
|
355
437
|
if (rawArgs !== void 0 && rawArgs !== null && !isObject(rawArgs)) throw new ArgumentError("the arguments must be an object");
|
|
356
438
|
const args = rawArgs ?? {};
|
|
357
439
|
if (env.baseUrl === void 0) throw new Error("No base URL: pass baseUrl, return one from requestInit, or give the document a server");
|
|
440
|
+
let last;
|
|
441
|
+
const inner = env.transport ?? fetchTransport;
|
|
442
|
+
const transport = async (request) => {
|
|
443
|
+
last = await inner(request);
|
|
444
|
+
return last;
|
|
445
|
+
};
|
|
446
|
+
const outcome = await callApi(entry, args, {
|
|
447
|
+
...env,
|
|
448
|
+
baseUrl: env.baseUrl,
|
|
449
|
+
transport
|
|
450
|
+
});
|
|
451
|
+
const id = entry.op.kind === "delete" ? requireId(args) : void 0;
|
|
452
|
+
if (outcome.kind === "content") return outcome.result;
|
|
453
|
+
const location = outcome.kind === "envelope" && outcome.location === true;
|
|
454
|
+
const envelope = outcome.kind === "envelope" ? outcome.envelope : withFallbackMeta(outcome.envelope, responseMeta(last));
|
|
455
|
+
return env.legacyResults ? legacyResult(entry, envelope, location, id) : structured(envelope);
|
|
456
|
+
}
|
|
457
|
+
function withFallbackMeta(envelope, meta) {
|
|
458
|
+
return meta === void 0 || envelope["meta"] !== void 0 ? envelope : {
|
|
459
|
+
...envelope,
|
|
460
|
+
meta
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const envelope = (value) => ({
|
|
464
|
+
kind: "envelope",
|
|
465
|
+
envelope: value
|
|
466
|
+
});
|
|
467
|
+
async function callApi(entry, args, env) {
|
|
358
468
|
const { op } = entry;
|
|
359
469
|
const descriptor = op.kind === "action" ? withAction(env.descriptor, entry) : env.descriptor;
|
|
360
470
|
const headers = env.headers;
|
|
361
471
|
const accessor = createClient(descriptor, {
|
|
362
472
|
baseUrl: env.baseUrl,
|
|
363
|
-
|
|
473
|
+
transport: env.transport,
|
|
364
474
|
...headers !== void 0 ? { headers: () => headers } : {}
|
|
365
475
|
})[op.type];
|
|
366
476
|
const query = entry.input.query === void 0 ? void 0 : readQuery(args, entry.input.query);
|
|
367
477
|
if (query !== void 0 && env.signal !== void 0) query.signal = env.signal;
|
|
478
|
+
const flat = {
|
|
479
|
+
descriptor: env.descriptor,
|
|
480
|
+
includeDepth: env.includeDepth,
|
|
481
|
+
resourceLinks: env.resourceLinks
|
|
482
|
+
};
|
|
368
483
|
switch (op.kind) {
|
|
369
484
|
case "list":
|
|
370
485
|
case "related": {
|
|
371
|
-
if (entry.result === "item") {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
486
|
+
if (entry.result === "item") return {
|
|
487
|
+
kind: "bare",
|
|
488
|
+
envelope: resourceEnvelope(await accessor.id(requireId(args)).rel(op.relation).related(query), flat)
|
|
489
|
+
};
|
|
375
490
|
let collection;
|
|
376
491
|
if (args["pageLink"] !== void 0 && args["pageLink"] !== null && entry.input.query?.paginated) {
|
|
377
492
|
const url = checkPageLink(args["pageLink"], env.baseUrl, op.path);
|
|
378
493
|
const doc = await execute({
|
|
379
494
|
baseUrl: env.baseUrl,
|
|
380
|
-
transport: env.transport
|
|
495
|
+
transport: env.transport,
|
|
381
496
|
...headers !== void 0 ? { headers: () => headers } : {}
|
|
382
497
|
}, {
|
|
383
498
|
method: "GET",
|
|
@@ -392,37 +507,44 @@ async function runCall(entry, rawArgs, env) {
|
|
|
392
507
|
});
|
|
393
508
|
} else if (op.kind === "list") collection = await accessor.list(query);
|
|
394
509
|
else collection = await accessor.id(requireId(args)).rel(op.relation).related(query);
|
|
395
|
-
return
|
|
396
|
-
}
|
|
397
|
-
case "get": {
|
|
398
|
-
const resource = await accessor.get(requireId(args), query);
|
|
399
|
-
return structured({ item: isObject(resource) ? flattenResource(resource, env.descriptor, [], env.includeDepth) : null });
|
|
510
|
+
return envelope(collectionEnvelope(Array.isArray(collection) ? collection : [], flat));
|
|
400
511
|
}
|
|
512
|
+
case "get": return {
|
|
513
|
+
kind: "bare",
|
|
514
|
+
envelope: resourceEnvelope(await accessor.get(requireId(args), query), flat)
|
|
515
|
+
};
|
|
401
516
|
case "create": {
|
|
402
517
|
const input = writeInput(args, entry.input.write);
|
|
403
518
|
const created = await accessor.create(input);
|
|
404
|
-
return
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
519
|
+
return {
|
|
520
|
+
kind: "bare",
|
|
521
|
+
envelope: resourceEnvelope(isObject(created) ? created : {
|
|
522
|
+
id: String(input["id"] ?? ""),
|
|
523
|
+
type: op.type
|
|
524
|
+
}, flat)
|
|
525
|
+
};
|
|
408
526
|
}
|
|
409
527
|
case "update": {
|
|
410
528
|
const id = requireId(args);
|
|
411
529
|
const updated = await accessor.id(id).update(writeInput(args, entry.input.write));
|
|
412
|
-
return
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
await accessor.id(id).delete();
|
|
420
|
-
return { content: [text(`Deleted ${op.type} ${id}.`)] };
|
|
530
|
+
return {
|
|
531
|
+
kind: "bare",
|
|
532
|
+
envelope: resourceEnvelope(isObject(updated) ? updated : {
|
|
533
|
+
id,
|
|
534
|
+
type: op.type
|
|
535
|
+
}, flat)
|
|
536
|
+
};
|
|
421
537
|
}
|
|
422
|
-
case "
|
|
538
|
+
case "delete":
|
|
539
|
+
await accessor.id(requireId(args)).delete();
|
|
540
|
+
return {
|
|
541
|
+
kind: "bare",
|
|
542
|
+
envelope: { data: null }
|
|
543
|
+
};
|
|
544
|
+
case "action": return runAction(entry, args, accessor, env, flat);
|
|
423
545
|
}
|
|
424
546
|
}
|
|
425
|
-
async function runAction(entry, args, accessor, env) {
|
|
547
|
+
async function runAction(entry, args, accessor, env, flat) {
|
|
426
548
|
const { op } = entry;
|
|
427
549
|
const action = op.actionDescriptor;
|
|
428
550
|
const call = (action.scope === "resource" ? accessor.id(requireId(args)).actions : accessor.actions)[entry.actionKey];
|
|
@@ -435,22 +557,36 @@ async function runAction(entry, args, accessor, env) {
|
|
|
435
557
|
if (plan.kind === "body" && plan.required && body === void 0) throw new ArgumentError("body is required");
|
|
436
558
|
result = await call(body, options);
|
|
437
559
|
}
|
|
438
|
-
if (action.location !== void 0 && isActionLocation(result)) return
|
|
560
|
+
if (action.location !== void 0 && isActionLocation(result)) return {
|
|
561
|
+
kind: "envelope",
|
|
562
|
+
envelope: { data: { ...result } },
|
|
563
|
+
location: true
|
|
564
|
+
};
|
|
565
|
+
const content = (value) => ({
|
|
566
|
+
kind: "content",
|
|
567
|
+
result: { content: [text(value)] }
|
|
568
|
+
});
|
|
439
569
|
switch (entry.result) {
|
|
440
|
-
case "collection": return
|
|
570
|
+
case "collection": return envelope(collectionEnvelope(Array.isArray(result) ? result : [], flat));
|
|
441
571
|
case "resource":
|
|
442
572
|
case "loose":
|
|
443
|
-
if (Array.isArray(result)) return
|
|
444
|
-
return
|
|
445
|
-
|
|
573
|
+
if (Array.isArray(result)) return envelope(collectionEnvelope(result, flat));
|
|
574
|
+
return {
|
|
575
|
+
kind: "bare",
|
|
576
|
+
envelope: resourceEnvelope(isObject(result) ? result : null, flat)
|
|
577
|
+
};
|
|
578
|
+
case "meta": return envelope({ meta: isObject(result) ? result : {} });
|
|
446
579
|
case "raw": {
|
|
447
580
|
const mimeType = action.rawOutput?.[0]?.mediaType ?? "application/octet-stream";
|
|
448
|
-
if (result === void 0) return
|
|
449
|
-
if (typeof result === "string") return
|
|
450
|
-
if (result instanceof ArrayBuffer || typeof Blob !== "undefined" && result instanceof Blob) return {
|
|
451
|
-
|
|
581
|
+
if (result === void 0) return content("Done.");
|
|
582
|
+
if (typeof result === "string") return content(result);
|
|
583
|
+
if (result instanceof ArrayBuffer || typeof Blob !== "undefined" && result instanceof Blob) return {
|
|
584
|
+
kind: "content",
|
|
585
|
+
result: { content: [await binaryContent(result, `${env.baseUrl}${action.path}`, mimeType)] }
|
|
586
|
+
};
|
|
587
|
+
return content(JSON.stringify(result));
|
|
452
588
|
}
|
|
453
|
-
default: return
|
|
589
|
+
default: return content("Done.");
|
|
454
590
|
}
|
|
455
591
|
}
|
|
456
592
|
/** The error a failed call carries, before the host's `mapError`. */
|
|
@@ -489,15 +625,18 @@ function errorResult(error) {
|
|
|
489
625
|
//#endregion
|
|
490
626
|
//#region src/model.ts
|
|
491
627
|
/** The artefact's own format. A runtime refuses a format it doesn't know. */
|
|
492
|
-
const MODEL_FORMAT =
|
|
628
|
+
const MODEL_FORMAT = 4;
|
|
493
629
|
/**
|
|
494
630
|
* Every format this runtime reads. Format 1 has no `credentialSchemes`. Formats 1 and 2 have no
|
|
495
631
|
* `includeDepth`, and store each operation's `scopes` and the `security` of its operation object.
|
|
632
|
+
* Formats 1 to 3 have no `resourceLinks`, and their output schemas describe the results before
|
|
633
|
+
* the envelope, which the runtime still returns for them.
|
|
496
634
|
*/
|
|
497
635
|
const READABLE_FORMATS = [
|
|
498
636
|
1,
|
|
499
637
|
2,
|
|
500
|
-
3
|
|
638
|
+
3,
|
|
639
|
+
4
|
|
501
640
|
];
|
|
502
641
|
/** The package that writes the artefact. */
|
|
503
642
|
const MODEL_GENERATOR = "@haddowg/json-api-mcp";
|
|
@@ -511,14 +650,16 @@ var McpModelError = class extends Error {
|
|
|
511
650
|
/** Check a parsed artefact before the runtime trusts it. */
|
|
512
651
|
function assertModel(value) {
|
|
513
652
|
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.`);
|
|
514
|
-
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
|
|
653
|
+
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 4. Regenerate it with the same version of ${MODEL_GENERATOR} the server runs.`);
|
|
515
654
|
const contract = value["contract"];
|
|
516
|
-
if (typeof contract !== "number" || contract < 13 || contract >
|
|
655
|
+
if (typeof contract !== "number" || contract < 13 || contract > 19) throw new McpModelError(`The model was built from a document with generator contract ${String(contract)}, and this runtime reads contracts 13 to 19.`);
|
|
517
656
|
if (!isObject(value["descriptor"]) || !Array.isArray(value["tools"])) throw new McpModelError("The model has no descriptor or tools; regenerate it.");
|
|
518
657
|
const credentialSchemes = value["credentialSchemes"];
|
|
519
658
|
if (value["format"] !== 1 && (!Array.isArray(credentialSchemes) || !credentialSchemes.every((s) => typeof s === "string"))) throw new McpModelError("The model has no credentialSchemes list; regenerate it.");
|
|
520
659
|
const includeDepth = value["includeDepth"];
|
|
521
|
-
if (value["format"]
|
|
660
|
+
if (value["format"] >= 3 && includeDepth !== null && !(typeof includeDepth === "number" && Number.isInteger(includeDepth) && includeDepth >= 0)) throw new McpModelError("The model has no includeDepth; regenerate it.");
|
|
661
|
+
const resourceLinks = value["resourceLinks"];
|
|
662
|
+
if (value["format"] === 4 && !(isObject(resourceLinks) && Object.values(resourceLinks).every((names) => Array.isArray(names) && names.every((name) => typeof name === "string")))) throw new McpModelError("The model has no resourceLinks; regenerate it.");
|
|
522
663
|
return value;
|
|
523
664
|
}
|
|
524
665
|
/**
|
|
@@ -632,6 +773,8 @@ function createJsonApiMcp(options) {
|
|
|
632
773
|
const defaultBaseUrl = options.baseUrl ?? model.baseUrl;
|
|
633
774
|
const credentialSchemes = resolveCredentialSchemes(model, options.credentialSchemes);
|
|
634
775
|
const includeDepth = model.includeDepth ?? null;
|
|
776
|
+
const resourceLinks = model.resourceLinks ?? {};
|
|
777
|
+
const legacyResults = model.format < 4;
|
|
635
778
|
async function forRequest(request) {
|
|
636
779
|
const ctx = request.context;
|
|
637
780
|
let entries = catalogue.entries;
|
|
@@ -671,7 +814,9 @@ function createJsonApiMcp(options) {
|
|
|
671
814
|
transport: options.transport,
|
|
672
815
|
headers: init.headers,
|
|
673
816
|
signal: callOptions.signal,
|
|
674
|
-
includeDepth
|
|
817
|
+
includeDepth,
|
|
818
|
+
resourceLinks,
|
|
819
|
+
legacyResults
|
|
675
820
|
});
|
|
676
821
|
} catch (error) {
|
|
677
822
|
const toolError = toToolError(error);
|