@knitli/openapi-mcp 1.2.0 → 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/README.md CHANGED
@@ -117,6 +117,13 @@ nothing is an error, not a silently smaller release — fix the typo it names.
117
117
  OpenAPI 3.1 `webhooks` are not part of `paths` and are left untouched, dangling
118
118
  refs and all; Graph's v1.0 document (3.0) has none.
119
119
 
120
+ `--optional <propertyName>` (repeatable) removes that property from every
121
+ `required` array anywhere in the sliced output — component schemas and inline
122
+ request, parameter and response schemas alike — dropping `required` entirely
123
+ where that empties it. Graph's OpenAPI document marks `@odata.type` `required` on nearly
124
+ every schema, but the Graph service itself never requires it on request
125
+ bodies, so pass `--optional '@odata.type'` when slicing Graph specs.
126
+
120
127
  ## Configure and start the local MCP server
121
128
 
122
129
  Save this as `operator/config.json`, substituting absolute paths, the public key,
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ const USAGE = `openapi-mcp — compile OpenAPI documents into signed MCP artifac
15
15
  --catalog <id> --release <id> --generation <n> --issuer <id> --key-id <id>
16
16
  --policy-id <id> --allowed-origin <https-origin> --out <directory> --sign-key <path>
17
17
  [--permissions <path>] [--reference-root <path> --reference-map <path>]
18
- slice --spec <openapi.yaml|json> --out <sliced.json> [--tag <tag>]... [--operation <operationId>]... [--max-document-keys <n>] [--max-document-nodes <n>]
18
+ slice --spec <openapi.yaml|json> --out <sliced.json> [--tag <tag>]... [--operation <operationId>]... [--optional <propertyName>]... [--max-document-keys <n>] [--max-document-nodes <n>]
19
19
  verify --artifact <path> --sig <path> --pub <path> (legacy v3 exact-file signature)
20
20
  keygen [--out <dir>]
21
21
  serve --config <absolute-config-path>
@@ -214,6 +214,7 @@ if (command === "slice") {
214
214
  out: { type: "string" },
215
215
  tag: { type: "string", multiple: true },
216
216
  operation: { type: "string", multiple: true },
217
+ optional: { type: "string", multiple: true },
217
218
  "max-document-keys": { type: "string" },
218
219
  "max-document-nodes": { type: "string" },
219
220
  },
@@ -231,6 +232,7 @@ if (command === "slice") {
231
232
  const sliced = sliceSpec(document, {
232
233
  tags: values.tag ?? [],
233
234
  operations: values.operation ?? [],
235
+ optional: values.optional ?? [],
234
236
  });
235
237
  await writeFile(values.out, `${JSON.stringify(sliced, null, 2)}\n`);
236
238
  const paths = sliced.paths;
@@ -308,6 +308,18 @@ function validateDiscriminator(schema, branches, value) {
308
308
  schemaFailure();
309
309
  return selected[0];
310
310
  }
311
+ /** Indices of the `anyOf`/`oneOf` branches the value validates against. When the schema also
312
+ * carries a discriminator, the branch it selects must be among them. */
313
+ function matchingBranches(schema, raw, value, closure, depth, context) {
314
+ const branches = schemaArray(raw);
315
+ const selected = validateDiscriminator(schema, branches, value);
316
+ const matches = branches
317
+ .map((branch, index) => validates(branch, value, closure, depth + 1, context) ? index : -1)
318
+ .filter((index) => index >= 0);
319
+ if (selected !== null && !matches.includes(selected))
320
+ schemaFailure();
321
+ return matches;
322
+ }
311
323
  function validateReference(id, schema, value, closure, depth, context) {
312
324
  const activeValues = context.activeReferences.get(id) ?? new Set();
313
325
  if (activeValues.has(value))
@@ -349,22 +361,22 @@ function validateSchema(schema, value, closure, depth, context) {
349
361
  validateSchema(branch, value, closure, depth + 1, context);
350
362
  }
351
363
  if (Object.hasOwn(schema, "anyOf")) {
352
- const matches = schemaArray(schema.anyOf).filter((branch) => validates(branch, value, closure, depth + 1, context));
364
+ const matches = matchingBranches(schema, schema.anyOf, value, closure, depth, context);
353
365
  if (matches.length === 0)
354
366
  schemaFailure();
355
367
  }
356
368
  if (Object.hasOwn(schema, "oneOf")) {
357
- const branches = schemaArray(schema.oneOf);
358
- const selected = validateDiscriminator(schema, branches, value);
359
- const matches = branches
360
- .map((branch, index) => validates(branch, value, closure, depth + 1, context) ? index : -1)
361
- .filter((index) => index >= 0);
362
- if (matches.length !== 1 || (selected !== null && matches[0] !== selected))
369
+ const matches = matchingBranches(schema, schema.oneOf, value, closure, depth, context);
370
+ if (matches.length !== 1)
363
371
  schemaFailure();
364
372
  }
365
- else if (Object.hasOwn(schema, "discriminator")) {
373
+ // A `discriminator` with no `oneOf`/`anyOf` alongside it is OpenAPI 3.0's inheritance form
374
+ // (OpenAPI 3.0.3 §4.7.24.2, Discriminator Object, "composition and inheritance"): it names the
375
+ // property a *consumer* would branch on, but the schema itself is a plain `type: object`
376
+ // (optionally under `allOf`) with no selection for us to perform. Ignore it rather than reject,
377
+ // but keep the same shape check every other supported key gets.
378
+ else if (Object.hasOwn(schema, "discriminator") && !isObject(schema.discriminator))
366
379
  schemaFailure();
367
- }
368
380
  if (Object.hasOwn(schema, "nullable")) {
369
381
  if (typeof schema.nullable !== "boolean")
370
382
  schemaFailure();
package/dist/slice.d.ts CHANGED
@@ -1,10 +1,18 @@
1
1
  export declare const HTTP_METHODS: readonly ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
2
2
  type Json = Record<string, unknown>;
3
- /** Which operations survive: any of these tags, or any of these operation ids. */
3
+ /** Which operations survive: any of these tags, or any of these operation ids. `optional` names
4
+ * properties to drop from every `required` array in the sliced output — see `dropRequired`. */
4
5
  export interface SliceSelection {
5
6
  readonly tags: readonly string[];
6
7
  readonly operations: readonly string[];
8
+ readonly optional?: readonly string[];
7
9
  }
10
+ /** Returns a new value with every `name` in `names` removed from every `required` array found
11
+ * anywhere in `document` — component schemas and inline schemas (paths, requestBodies,
12
+ * parameters) alike — deleting the `required` key entirely if that empties it. Does not mutate
13
+ * `document`. Graph's OpenAPI document marks `@odata.type` `required` on nearly every schema,
14
+ * but the service itself never requires it on request bodies. */
15
+ export declare function dropRequired(document: Json, names: ReadonlySet<string>): Json;
8
16
  /** Number of object keys in a JSON value; compare with the compiler's `maxDocumentKeys` (250,000). */
9
17
  export declare function countKeys(value: unknown): number;
10
18
  /** Describes a key count against compile-release's default `maxDocumentKeys`, and what to do if it doesn't fit. */
package/dist/slice.js CHANGED
@@ -16,6 +16,31 @@ export const HTTP_METHODS = [
16
16
  "patch",
17
17
  "trace",
18
18
  ];
19
+ /** Returns a new value with every `name` in `names` removed from every `required` array found
20
+ * anywhere in `document` — component schemas and inline schemas (paths, requestBodies,
21
+ * parameters) alike — deleting the `required` key entirely if that empties it. Does not mutate
22
+ * `document`. Graph's OpenAPI document marks `@odata.type` `required` on nearly every schema,
23
+ * but the service itself never requires it on request bodies. */
24
+ export function dropRequired(document, names) {
25
+ function walk(value) {
26
+ if (Array.isArray(value))
27
+ return value.map(walk);
28
+ if (!value || typeof value !== "object")
29
+ return value;
30
+ const result = {};
31
+ for (const [key, item] of Object.entries(value)) {
32
+ if (key === "required" && Array.isArray(item)) {
33
+ const kept = item.filter((name) => typeof name !== "string" || !names.has(name));
34
+ if (kept.length > 0)
35
+ result[key] = kept;
36
+ continue;
37
+ }
38
+ result[key] = walk(item);
39
+ }
40
+ return result;
41
+ }
42
+ return walk(document);
43
+ }
19
44
  /** Number of object keys in a JSON value; compare with the compiler's `maxDocumentKeys` (250,000). */
20
45
  export function countKeys(value) {
21
46
  if (Array.isArray(value))
@@ -193,12 +218,14 @@ export function sliceSpec(document, selection) {
193
218
  const server = document.servers?.[0]?.url;
194
219
  if (!server)
195
220
  throw new Error("The document names no server.");
196
- return {
221
+ const sliced = {
197
222
  ...document,
198
223
  servers: [{ url: new URL(server).origin }],
199
224
  paths,
200
225
  components: prunedComponents,
201
226
  };
227
+ const optional = selection.optional ?? [];
228
+ return optional.length > 0 ? dropRequired(sliced, new Set(optional)) : sliced;
202
229
  }
203
230
  /** Drops `discriminator.mapping` entries whose target is not in the kept closure, on a shallow
204
231
  * copy — the schema itself is left aliased to the source document when there is nothing to prune. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@knitli/openapi-mcp",
3
3
  "private": false,
4
- "version": "1.2.0",
4
+ "version": "1.2.1",
5
5
  "type": "module",
6
6
  "description": "Compile signed OpenAPI catalogs and serve verified search and authorized API calls over MCP.",
7
7
  "license": "MIT OR Apache-2.0",