@haddowg/json-api-mcp 1.2.1 → 1.3.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 CHANGED
@@ -45,6 +45,16 @@ session.tools // answer tools/list with these
45
45
  await session.callTool('albums_list', { sort: ['-releasedAt'], pageSize: 5 })
46
46
  ```
47
47
 
48
+ Every result's `structuredContent` is a JSON:API-like envelope. A single resource comes back as
49
+ `{ data }`, a collection as `{ data, links, meta }` with only the pagination links, a delete as
50
+ `{ data: null }` and a meta-only action as `{ meta }`. A flattened resource keeps the links its
51
+ type names, such as a presigned upload URL, under `links`.
52
+
53
+ Coming from 1.2: a get returns `{ data }` where it returned `{ item }`, a create or update is
54
+ wrapped in `{ data }` instead of coming back bare, and a collection lists its resources under
55
+ `data` instead of `items`. Regenerate the model to get the new shapes. A model generated by 1.2
56
+ still loads and keeps the 1.2 shapes its output schemas describe.
57
+
48
58
  Scope filtering counts only the security requirements a caller can meet with its credential
49
59
  schemes, the document's `oauth2` schemes unless you pass `credentialSchemes`. Scopes, feature
50
60
  gates, headers and error hints come from optional hooks: `scopeMatcher`, `capabilities`,
package/dist/cli.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as WriteModelOptions } from "./generate-CuWnYJUB.mjs";
1
+ import { n as WriteModelOptions } from "./generate-D0kx-DHv.mjs";
2
2
 
3
3
  //#region src/cli.d.ts
4
4
  declare const USAGE = "Usage: json-api-mcp --input <url|file> --output <file> [--default-page-size <n>] [--include-depth <n|all>] [--check]";
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as writeMcpModel } from "./generate-CiLuqQJ0.mjs";
2
+ import { r as writeMcpModel } from "./generate-BcqHRSxB.mjs";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/cli.ts
@@ -123,7 +123,81 @@ function includePaths(doc, op) {
123
123
  return [];
124
124
  }
125
125
  const ref = (type) => ({ $ref: `#/$defs/${type}` });
126
- /** One type's flattened resource schema: id, type, attributes and relations, all optional but id and type. */
126
+ /** Links every resource may carry, which a flattened resource leaves out. */
127
+ const GENERIC_LINKS = /* @__PURE__ */ new Set([
128
+ "self",
129
+ "related",
130
+ "describedby"
131
+ ]);
132
+ /** The members a flattened link keeps. */
133
+ const LINK_MEMBERS = [
134
+ "href",
135
+ "type",
136
+ "title",
137
+ "meta"
138
+ ];
139
+ /**
140
+ * The links a resource schema names: its declared links and its action links. From generator
141
+ * contract 18 they're the properties of the `<Type>ResourceLinks` component's own `allOf`
142
+ * member; before, they sit beside `self` in its properties.
143
+ */
144
+ function namedLinkSchemas(doc, resource) {
145
+ const component = deref(doc, (isObject(resource?.["properties"]) ? resource["properties"] : {})["links"]);
146
+ if (!isObject(component)) return [];
147
+ const own = isObject(component["properties"]) ? Object.entries(component["properties"]) : [];
148
+ const extended = (Array.isArray(component["allOf"]) ? component["allOf"] : []).filter((member) => isObject(member) && member["$ref"] === void 0).flatMap((member) => isObject(member["properties"]) ? Object.entries(member["properties"]) : []);
149
+ return [...own, ...extended].filter((entry) => !GENERIC_LINKS.has(entry[0]) && isObject(entry[1]));
150
+ }
151
+ const hasHref = (s) => isObject(s) && isObject(s["properties"]) && s["properties"]["href"] !== void 0;
152
+ /** The link object branch of a link schema: the schema itself, or its `anyOf` branch with `href`. */
153
+ function linkObjectSchema(schema) {
154
+ if (hasHref(schema)) return schema;
155
+ return (Array.isArray(schema["anyOf"]) ? schema["anyOf"] : []).find(hasHref);
156
+ }
157
+ /**
158
+ * One named link as a flattened resource carries it: `{ href, type?, title?, meta? }`, typed from
159
+ * the document's link schema, meta members included. A link the server renders as a bare URL is
160
+ * normalised to `{ href }`.
161
+ */
162
+ function linkOutput(doc, schema) {
163
+ const resolved = outputSchema(doc, schema);
164
+ const object = linkObjectSchema(resolved);
165
+ const source = isObject(object?.["properties"]) ? object["properties"] : {};
166
+ const properties = { href: {
167
+ type: "string",
168
+ format: "uri-reference"
169
+ } };
170
+ for (const member of LINK_MEMBERS) {
171
+ const value = source[member];
172
+ if (isObject(value)) properties[member] = member === "href" ? {
173
+ ...properties["href"],
174
+ ...value
175
+ } : value;
176
+ }
177
+ const out = {
178
+ type: "object",
179
+ properties,
180
+ required: ["href", ...(Array.isArray(object?.["required"]) ? object["required"] : []).filter((name) => name !== "href" && name in properties)]
181
+ };
182
+ if (typeof resolved["description"] === "string") out["description"] = resolved["description"];
183
+ return out;
184
+ }
185
+ /**
186
+ * The names of the links each type names, for the types that name any. Throws when such a type
187
+ * also has an attribute or relationship called `links`, which the flattened resource couldn't
188
+ * hold beside them.
189
+ */
190
+ function resourceLinkNames(doc, descriptor, schemas) {
191
+ const out = {};
192
+ for (const [type, resource] of Object.entries(descriptor)) {
193
+ const names = namedLinkSchemas(doc, schemas.get(type)).map(([name]) => name);
194
+ if (names.length === 0) continue;
195
+ if ("links" in resource.attributes || "links" in resource.relations) throw new Error(`The "${type}" type names links (${names.join(", ")}) and also has a field called "links". A flattened resource carries its links under "links", so rename the field.`);
196
+ out[type] = names;
197
+ }
198
+ return out;
199
+ }
200
+ /** One type's flattened resource schema: id, type, attributes, relations and named links, all optional but id and type. */
127
201
  function flatResource(doc, descriptor, schemas, type, expanded) {
128
202
  const resource = schemas.get(type);
129
203
  const attributes = outputSchema(doc, (isObject(resource?.["properties"]) ? resource["properties"] : {})["attributes"]);
@@ -161,6 +235,12 @@ function flatResource(doc, descriptor, schemas, type, expanded) {
161
235
  description: `The related ${target}: ${polymorphic ? "its type and id" : "its id"}, or the resource itself when included.`
162
236
  };
163
237
  }
238
+ const links = namedLinkSchemas(doc, resource);
239
+ if (links.length > 0) properties["links"] = {
240
+ type: "object",
241
+ description: "The links the resource names, such as an upload or download URL or a custom action. A link that does not apply to this resource is absent.",
242
+ properties: Object.fromEntries(links.map(([name, link]) => [name, linkOutput(doc, link)]))
243
+ };
164
244
  const schema = {
165
245
  type: "object",
166
246
  properties,
@@ -195,65 +275,96 @@ const LINKS = {
195
275
  last: { type: "string" }
196
276
  }
197
277
  };
198
- /** A collection: `{ items, links, meta }`. */
278
+ /** The document `meta` member of an envelope: what the response declares, when it sends any. */
279
+ function documentMeta(ctx, description) {
280
+ return {
281
+ ...responseMeta(ctx),
282
+ description
283
+ };
284
+ }
285
+ /** A collection: `{ data, links, meta? }`. */
199
286
  function collectionOutput(ctx, types) {
200
287
  const reachable = reachableTypes(ctx.descriptor, types, ctx.includes);
201
288
  return {
202
289
  type: "object",
203
290
  properties: {
204
- items: {
291
+ data: {
205
292
  type: "array",
206
- items: types.length === 1 ? ref(types[0]) : { anyOf: types.map(ref) }
293
+ items: types.length === 1 ? ref(types[0]) : { anyOf: types.map(ref) },
294
+ description: "The resources on this page."
207
295
  },
208
296
  links: LINKS,
209
- meta: {
210
- ...responseMeta(ctx),
211
- description: "The document meta, such as page totals."
212
- }
297
+ meta: documentMeta(ctx, "The document meta, such as page totals.")
213
298
  },
214
- required: ["items", "links"],
299
+ required: ["data", "links"],
215
300
  $defs: defsFor(ctx.doc, ctx.descriptor, ctx.schemas, reachable)
216
301
  };
217
302
  }
218
- /** One resource, flattened, at the root. */
219
- function resourceOutput(ctx, type) {
220
- const reachable = reachableTypes(ctx.descriptor, [type], ctx.includes);
221
- const defs = defsFor(ctx.doc, ctx.descriptor, ctx.schemas, reachable);
303
+ /**
304
+ * One resource: `{ data, meta? }`. `data` is null where the operation may find nothing, such as
305
+ * a to-one related read with nothing related.
306
+ */
307
+ function resourceOutput(ctx, types, nullable = false) {
308
+ const reachable = reachableTypes(ctx.descriptor, types, ctx.includes);
222
309
  return {
223
- ...defs[type],
224
- $defs: defs
310
+ type: "object",
311
+ properties: {
312
+ data: nullable !== false ? {
313
+ anyOf: [...types.map(ref), { type: "null" }],
314
+ description: nullable
315
+ } : types.length === 1 ? {
316
+ ...ref(types[0]),
317
+ description: "The resource."
318
+ } : {
319
+ anyOf: types.map(ref),
320
+ description: "The resource."
321
+ },
322
+ meta: documentMeta(ctx, "The document meta, when the API sends any.")
323
+ },
324
+ required: ["data"],
325
+ $defs: defsFor(ctx.doc, ctx.descriptor, ctx.schemas, reachable)
225
326
  };
226
327
  }
227
- /** A single read: `{ item }`, and for a to-one related read null when nothing is related. */
228
- function itemOutput(ctx, types, nullable) {
229
- const reachable = reachableTypes(ctx.descriptor, types, ctx.includes);
328
+ /** A delete: `{ data: null, meta? }`. */
329
+ function deletedOutput(ctx) {
230
330
  return {
231
331
  type: "object",
232
- properties: { item: nullable ? {
233
- anyOf: [...types.map(ref), { type: "null" }],
234
- description: "The related resource, or null when there is none."
235
- } : types.length === 1 ? {
236
- ...ref(types[0]),
237
- description: "The resource."
238
- } : {
239
- anyOf: types.map(ref),
240
- description: "The resource."
241
- } },
242
- required: ["item"],
243
- $defs: defsFor(ctx.doc, ctx.descriptor, ctx.schemas, reachable)
332
+ properties: {
333
+ data: {
334
+ type: "null",
335
+ description: "Always null: the resource is gone."
336
+ },
337
+ meta: documentMeta(ctx, "The document meta, when the API sends any.")
338
+ },
339
+ required: ["data"]
244
340
  };
245
341
  }
246
- /** A meta-only result: the document's `meta`. */
342
+ /** A meta-only result: `{ meta }`, the document's own `meta`, with no `data`. */
247
343
  function metaOutput(ctx) {
248
344
  return {
249
- ...responseMeta(ctx),
250
- type: "object"
345
+ type: "object",
346
+ properties: { meta: {
347
+ ...responseMeta(ctx),
348
+ type: "object",
349
+ description: "The document meta."
350
+ } },
351
+ required: ["meta"]
251
352
  };
252
353
  }
253
354
  /** A JSON:API result whose primary type the descriptor doesn't name. */
254
- const LOOSE_OUTPUT = { type: "object" };
355
+ const LOOSE_OUTPUT = {
356
+ type: "object",
357
+ properties: {
358
+ data: { description: "The flattened resource or resources." },
359
+ links: LINKS,
360
+ meta: {
361
+ type: "object",
362
+ description: "The document meta, when the API sends any."
363
+ }
364
+ }
365
+ };
255
366
  /** The URL an action answered with (generator contract 17): `{ url, type, expiresAt? }`. */
256
- function locationOutput(mediaType) {
367
+ function locationData(mediaType) {
257
368
  return {
258
369
  type: "object",
259
370
  description: "The URL the action produced, such as a short-lived download link. The tool does not fetch it.",
@@ -277,6 +388,14 @@ function locationOutput(mediaType) {
277
388
  additionalProperties: false
278
389
  };
279
390
  }
391
+ /** An action that answered with a URL: `{ data: { url, type, expiresAt? } }`. */
392
+ function locationOutput(mediaType) {
393
+ return {
394
+ type: "object",
395
+ properties: { data: locationData(mediaType) },
396
+ required: ["data"]
397
+ };
398
+ }
280
399
  /** An action's usual output or the URL it may answer with instead, sharing the root `$defs`. */
281
400
  function orLocation(schema, mediaType) {
282
401
  const { $defs, ...rest } = schema;
@@ -1158,7 +1277,7 @@ function actionOutput(ctx, action) {
1158
1277
  function documentOutput(ctx, action) {
1159
1278
  switch (actionResult(action)) {
1160
1279
  case "collection": return collectionOutput(ctx, [action.outputType]);
1161
- case "resource": return resourceOutput(ctx, action.outputType);
1280
+ case "resource": return resourceOutput(ctx, [action.outputType], "The resource the action returned.");
1162
1281
  case "meta": return metaOutput(ctx);
1163
1282
  case "loose": return LOOSE_OUTPUT;
1164
1283
  default: return;
@@ -1169,6 +1288,7 @@ function buildCatalogue(doc, descriptor, defaultPageSize, includeDepth = null) {
1169
1288
  const entries = [];
1170
1289
  const skipped = [];
1171
1290
  const schemas = resourceSchemas(doc);
1291
+ const resourceLinks = resourceLinkNames(doc, descriptor, schemas);
1172
1292
  for (const [path, item] of Object.entries(doc.paths ?? {})) for (const method of HTTP_METHODS) {
1173
1293
  const operation = item[method];
1174
1294
  const operationId = operation?.operationId;
@@ -1223,7 +1343,8 @@ function buildCatalogue(doc, descriptor, defaultPageSize, includeDepth = null) {
1223
1343
  entries.sort((a, b) => a.tool.name.localeCompare(b.tool.name));
1224
1344
  return {
1225
1345
  entries,
1226
- skipped
1346
+ skipped,
1347
+ resourceLinks
1227
1348
  };
1228
1349
  }
1229
1350
  function buildEntry(op, parsed, inputCtx, outputCtx, descriptor) {
@@ -1239,21 +1360,22 @@ function buildEntry(op, parsed, inputCtx, outputCtx, descriptor) {
1239
1360
  break;
1240
1361
  case "get":
1241
1362
  input = singleInput(inputCtx, `The id of the \`${op.type}\` resource to get.`);
1242
- output = itemOutput(outputCtx, [op.type], false);
1363
+ output = resourceOutput(outputCtx, [op.type]);
1243
1364
  result = "item";
1244
1365
  break;
1245
1366
  case "create":
1246
1367
  input = createInput(inputCtx, op.type);
1247
- output = resourceOutput(outputCtx, op.type);
1368
+ output = resourceOutput(outputCtx, [op.type]);
1248
1369
  result = "resource";
1249
1370
  break;
1250
1371
  case "update":
1251
1372
  input = updateInput(inputCtx, op.type);
1252
- output = resourceOutput(outputCtx, op.type);
1373
+ output = resourceOutput(outputCtx, [op.type]);
1253
1374
  result = "resource";
1254
1375
  break;
1255
1376
  case "delete":
1256
1377
  input = deleteInput(op.type);
1378
+ output = deletedOutput(outputCtx);
1257
1379
  result = "deleted";
1258
1380
  break;
1259
1381
  case "related": {
@@ -1270,7 +1392,7 @@ function buildEntry(op, parsed, inputCtx, outputCtx, descriptor) {
1270
1392
  result = "collection";
1271
1393
  } else {
1272
1394
  input = singleInput(inputCtx, `The id of the \`${op.type}\` resource whose ${parsed.relation} to get.`);
1273
- output = itemOutput(outputCtx, relation.types, true);
1395
+ output = resourceOutput(outputCtx, relation.types, "The related resource, or null when there is none.");
1274
1396
  result = "item";
1275
1397
  }
1276
1398
  break;
@@ -1367,7 +1489,7 @@ function buildMcpModel(document, descriptor, options) {
1367
1489
  const catalogue = buildCatalogue(document, descriptor, options.defaultPageSize ?? 10, includeDepth);
1368
1490
  const info = document.info;
1369
1491
  const model = {
1370
- format: 3,
1492
+ format: 4,
1371
1493
  generator: MODEL_GENERATOR,
1372
1494
  contract,
1373
1495
  source: {
@@ -1376,6 +1498,7 @@ function buildMcpModel(document, descriptor, options) {
1376
1498
  },
1377
1499
  credentialSchemes: oauthSchemes(document),
1378
1500
  includeDepth,
1501
+ resourceLinks: catalogue.resourceLinks,
1379
1502
  descriptor,
1380
1503
  tools: catalogue.entries.map((entry) => {
1381
1504
  const { op } = entry;
@@ -1471,4 +1594,4 @@ async function writeMcpModel(options) {
1471
1594
  //#endregion
1472
1595
  export { buildMcpModel as i, serialiseMcpModel as n, writeMcpModel as r, generateMcpModel as t };
1473
1596
 
1474
- //# sourceMappingURL=generate-CiLuqQJ0.mjs.map
1597
+ //# sourceMappingURL=generate-BcqHRSxB.mjs.map