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