@knitli/openapi-mcp 1.3.0 → 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.
@@ -111,6 +111,14 @@ export function runRuntimeConformanceSuite(factory, options) {
111
111
  useFault("catalog rejects duplicate operation transport rows", { fault: "duplicate-operation" }, async ({ store, fixture }) => {
112
112
  await assertPublicError(adapter, () => store.getOperation(fixture.catalogId, fixture.releaseA, fixture.operationId), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.operationRowAmbiguous);
113
113
  });
114
+ useFault("catalog rejects duplicate batched operation transport rows", { fault: "duplicate-operation" }, async ({ store, fixture }) => {
115
+ const getOperations = store.getOperations?.bind(store);
116
+ if (getOperations === undefined)
117
+ return;
118
+ await assertPublicError(adapter, () => getOperations(fixture.catalogId, fixture.releaseA, [
119
+ fixture.operationId,
120
+ ]), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.operationTransportTooManyRows);
121
+ });
114
122
  useFault("catalog rejects duplicate schema transport rows", { fault: "duplicate-schemas" }, async ({ store, fixture }) => {
115
123
  await assertPublicError(adapter, () => store.getSchemas(fixture.catalogId, fixture.releaseA, fixture.schemaIds), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.schemaTransportTooManyRows);
116
124
  });
@@ -128,6 +136,14 @@ export function runRuntimeConformanceSuite(factory, options) {
128
136
  useFault("catalog redacts operation driver failures", { fault: "driver-operation", injectedDriverError }, async ({ store, fixture }) => {
129
137
  await assertPublicError(adapter, () => store.getOperation(fixture.catalogId, fixture.releaseA, fixture.operationId), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.recordTransportUnavailable, injectedDriverError);
130
138
  });
139
+ useFault("catalog redacts batched operation driver failures", { fault: "driver-operation", injectedDriverError }, async ({ store, fixture }) => {
140
+ const getOperations = store.getOperations?.bind(store);
141
+ if (getOperations === undefined)
142
+ return;
143
+ await assertPublicError(adapter, () => getOperations(fixture.catalogId, fixture.releaseA, [
144
+ fixture.operationId,
145
+ ]), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.recordTransportUnavailable, injectedDriverError);
146
+ });
131
147
  useFault("catalog redacts schema driver failures", { fault: "driver-schemas", injectedDriverError }, async ({ store, fixture }) => {
132
148
  await assertPublicError(adapter, () => store.getSchemas(fixture.catalogId, fixture.releaseA, fixture.schemaIds), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.recordTransportUnavailable, injectedDriverError);
133
149
  });
@@ -137,6 +153,28 @@ export function runRuntimeConformanceSuite(factory, options) {
137
153
  adapter.equal(await store.getOperation(fixture.catalogId, "missing-release", fixture.operationId), null);
138
154
  adapter.equal(await store.getOperation(fixture.catalogId, fixture.releaseA, fixture.missingOperationId), null);
139
155
  });
156
+ use("catalog batched operations match single reads and omit absent IDs", async ({ store, fixture }) => {
157
+ const getOperations = store.getOperations?.bind(store);
158
+ if (getOperations === undefined)
159
+ return;
160
+ adapter.deepEqual(await getOperations(fixture.catalogId, fixture.releaseA, [
161
+ fixture.missingOperationId,
162
+ fixture.operationId,
163
+ fixture.operationId,
164
+ ]), [fixture.operationA]);
165
+ adapter.deepEqual(await getOperations(fixture.catalogId, fixture.releaseB, [
166
+ fixture.operationId,
167
+ ]), [fixture.operationB]);
168
+ adapter.deepEqual(await getOperations(fixture.catalogId, "missing-release", [
169
+ fixture.operationId,
170
+ ]), []);
171
+ adapter.deepEqual(await getOperations(fixture.catalogId, fixture.releaseA, []), []);
172
+ await assertPublicError(adapter, () => getOperations(fixture.catalogId, fixture.releaseA, [
173
+ "schema:conformance:not-an-operation",
174
+ ]), "INPUT_INVALID", CATALOG_STORE_PUBLIC_MESSAGES.operationIdentityInvalid);
175
+ const sparse = new Array(1);
176
+ await assertPublicError(adapter, () => getOperations(fixture.catalogId, fixture.releaseA, sparse), "INPUT_INVALID", CATALOG_STORE_PUBLIC_MESSAGES.operationRequestInvalid);
177
+ });
140
178
  use("catalog schemas are batched, unique, ordered, and empty-safe", async ({ store, fixture }) => {
141
179
  const firstSchemaId = fixture.schemaIds[0];
142
180
  if (firstSchemaId === undefined)
@@ -27,6 +27,12 @@ function manifestKey(catalogId, releaseId) {
27
27
  }
28
28
  const maximumSearchReleases = 8;
29
29
  const maximumInventorySchemaBatch = 128;
30
+ // Stores null record_json over maxRecordBytes, so one operation batch carries
31
+ // at most 16 MiB of raw row text; rows are then verified one at a time, so
32
+ // only one record's working copies are live at once. Sized for a Worker
33
+ // isolate. Schema batches keep the maxSchemaClosureBytes-derived bound
34
+ // because they serve closure resolution.
35
+ const OPERATION_BATCH_BYTE_BUDGET = 16 * 1024 * 1024;
30
36
  // State churn may require reproof, but one search may spend no more than the
31
37
  // same eight-release compatibility envelope used for manifest authentication.
32
38
  const maximumCompleteReleaseProofs = maximumSearchReleases;
@@ -140,40 +146,86 @@ async function verifyCompleteRelease(store, authenticated, limits, searchBudget)
140
146
  }
141
147
  return bytes;
142
148
  };
143
- const operationRoots = [];
144
- for (const id of operationIds) {
145
- chargeCompleteVerification(searchBudget, { work: 1, storeCalls: 1 });
146
- const row = await store.getOperation(authenticated.manifest.catalogId, authenticated.manifest.releaseId, id);
147
- if (row === null) {
148
- throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Release inventory operation is missing");
149
+ // Each response is bounded by its batch size times maxRecordBytes, and each
150
+ // request by the store's ID byte limit; one fixed JSON parameter keeps D1
151
+ // bindings flat.
152
+ const batchSizeFor = (byteBudget) => Math.max(1, Math.min(maximumInventorySchemaBatch, Math.floor(byteBudget / limits.maxRecordBytes)));
153
+ const operationBatchSize = batchSizeFor(OPERATION_BATCH_BYTE_BUDGET);
154
+ const schemaBatchSize = batchSizeFor(limits.maxSchemaClosureBytes);
155
+ const nextBatch = (ids, start, kind) => {
156
+ const batchSize = kind === "operation" ? operationBatchSize : schemaBatchSize;
157
+ const batch = [];
158
+ let requestBytes = 0;
159
+ for (let index = start; index < ids.length; index += 1) {
160
+ if (batch.length >= batchSize)
161
+ break;
162
+ const idBytes = encoder.encode(ids[index]).byteLength;
163
+ if (idBytes > limits.maxSchemaClosureBytes ||
164
+ (batch.length > 0 &&
165
+ requestBytes + idBytes > limits.maxSchemaClosureBytes))
166
+ break;
167
+ batch.push(ids[index]);
168
+ requestBytes += idBytes;
149
169
  }
150
- const operation = await verifyStoredRecord(authenticated, row, limits);
170
+ if (batch.length === 0) {
171
+ throw new OpenApiMcpError("RECORD_NOT_ADMITTED", `Release inventory ${kind} request exceeds its byte limit`);
172
+ }
173
+ return batch;
174
+ };
175
+ const operationRoots = [];
176
+ const admitOperation = (id, operation) => {
151
177
  if (!operation.id.startsWith("operation:") || operation.id !== id) {
152
178
  throw new OpenApiMcpError("RECORD_DIGEST_MISMATCH", "Release inventory operation identity does not match");
153
179
  }
154
180
  accountRecord(operation);
155
181
  operationRoots.push([...new Set(operation.schemaIds)].sort());
182
+ };
183
+ const missingOperation = () => new OpenApiMcpError("RECORD_NOT_ADMITTED", "Release inventory operation is missing");
184
+ if (store.getOperations === undefined) {
185
+ for (const id of operationIds) {
186
+ chargeCompleteVerification(searchBudget, { work: 1, storeCalls: 1 });
187
+ const row = await store.getOperation(authenticated.manifest.catalogId, authenticated.manifest.releaseId, id);
188
+ if (row === null)
189
+ throw missingOperation();
190
+ admitOperation(id, await verifyStoredRecord(authenticated, row, limits));
191
+ }
192
+ }
193
+ else {
194
+ let operationOffset = 0;
195
+ while (operationOffset < operationIds.length) {
196
+ const ids = nextBatch(operationIds, operationOffset, "operation");
197
+ operationOffset += ids.length;
198
+ chargeCompleteVerification(searchBudget, {
199
+ work: ids.length,
200
+ storeCalls: 1,
201
+ });
202
+ const result = await store.getOperations(authenticated.manifest.catalogId, authenticated.manifest.releaseId, ids);
203
+ const rows = snapshotRows(result, ids.length);
204
+ // Duplicate or unrequested rows are integrity failures, like an identity
205
+ // mismatch on the per-row path; absent rows are missing. Verify one row
206
+ // at a time: batching saves round-trips, not hashing.
207
+ const requested = new Set(ids);
208
+ const returned = new Map();
209
+ for (const row of rows) {
210
+ const record = await verifyStoredRecord(authenticated, row, limits);
211
+ if (!requested.has(record.id) || returned.has(record.id)) {
212
+ throw new OpenApiMcpError("RECORD_DIGEST_MISMATCH", "Release inventory operation rows are ambiguous");
213
+ }
214
+ returned.set(record.id, record);
215
+ }
216
+ for (const id of ids) {
217
+ const operation = returned.get(id);
218
+ if (operation === undefined)
219
+ throw missingOperation();
220
+ admitOperation(id, operation);
221
+ }
222
+ }
156
223
  }
157
- const schemaBatchSize = Math.max(1, Math.min(maximumInventorySchemaBatch, Math.floor(limits.maxSchemaClosureBytes / limits.maxRecordBytes)));
158
224
  const schemas = new Map();
159
225
  let offset = 0;
160
226
  while (offset < schemaIds.length) {
161
- const ids = [];
162
- let requestBytes = 0;
163
- while (offset < schemaIds.length && ids.length < schemaBatchSize) {
164
- const id = schemaIds[offset];
165
- const idBytes = encoder.encode(id).byteLength;
166
- if (idBytes > limits.maxSchemaClosureBytes ||
167
- (ids.length > 0 &&
168
- requestBytes + idBytes > limits.maxSchemaClosureBytes))
169
- break;
170
- ids.push(id);
171
- requestBytes += idBytes;
172
- offset += 1;
173
- }
174
- if (ids.length === 0) {
175
- throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Release inventory schema request exceeds its byte limit");
176
- }
227
+ const ids = nextBatch(schemaIds, offset, "schema");
228
+ offset += ids.length;
177
229
  chargeCompleteVerification(searchBudget, {
178
230
  work: ids.length,
179
231
  storeCalls: 1,
@@ -344,6 +344,13 @@ function validateSchema(schema, value, closure, depth, context) {
344
344
  if (schema === false || !isObject(schema))
345
345
  schemaFailure();
346
346
  for (const key of Object.keys(schema)) {
347
+ // OpenAPI Specification Extensions (`^x-`, OpenAPI 3.1 §4.9) are legal on a Schema Object and
348
+ // carry no validation semantics, so ignore the key and never walk its value -- Cloudflare's
349
+ // API document puts `x-auditable: true` on parameter schemas, and the compiler copies
350
+ // component schemas verbatim. Every other unknown keyword still fails: one with validation
351
+ // semantics (`pattern`, `contains`) must not be silently skipped.
352
+ if (key.startsWith("x-"))
353
+ continue;
347
354
  if (!supportedSchemaKeys.has(key))
348
355
  schemaFailure();
349
356
  }
@@ -28,6 +28,10 @@ export declare const CATALOG_STORE_PUBLIC_MESSAGES: Readonly<{
28
28
  recordJsonInvalid: "Stored record JSON is invalid";
29
29
  operationRowAmbiguous: "Stored operation row is ambiguous";
30
30
  operationIdentifierMismatch: "Stored operation identifier does not match request";
31
+ operationRequestLimitExceeded: "Operation request exceeds its limit";
32
+ operationRequestInvalid: "Operation request is invalid";
33
+ operationTransportTooManyRows: "Stored operation transport returned too many rows";
34
+ operationRowsInvalid: "Stored operation rows are invalid";
31
35
  schemaRequestLimitExceeded: "Schema request exceeds its limit";
32
36
  schemaRequestInvalid: "Schema request is invalid";
33
37
  schemaTransportTooManyRows: "Stored schema transport returned too many rows";
@@ -49,4 +53,4 @@ export interface D1CatalogDatabase {
49
53
  prepare(sql: string): D1CatalogPreparedStatement;
50
54
  }
51
55
  /** Construct the Worker-safe D1 implementation using only fixed SQL statements. */
52
- export declare function createD1CatalogStore(binding: D1CatalogDatabase, limits?: Partial<RuntimeLimits>): CatalogStore;
56
+ export declare function createD1CatalogStore(binding: D1CatalogDatabase, limits?: Partial<RuntimeLimits>): Required<CatalogStore>;
@@ -64,6 +64,24 @@ WHERE s.catalog_id = ? AND s.release_id = ?
64
64
  AND r.format IN (4, 5) AND r.contract = 1
65
65
  ORDER BY s.record_id COLLATE BINARY
66
66
  LIMIT ?;`;
67
+ const GET_OPERATIONS_SQL = `WITH requested(record_id) AS (
68
+ SELECT DISTINCT value FROM json_each(?) WHERE typeof(value) = 'text'
69
+ )
70
+ SELECT
71
+ CASE WHEN typeof(o.record_id) = 'text' AND length(CAST(o.record_id AS BLOB)) <= ? THEN o.record_id ELSE NULL END AS record_id,
72
+ length(CAST(o.record_id AS BLOB)) AS record_id_bytes,
73
+ CASE WHEN typeof(o.logical_digest) = 'text' AND length(CAST(o.logical_digest AS BLOB)) <= ? THEN o.logical_digest ELSE NULL END AS logical_digest,
74
+ length(CAST(o.logical_digest AS BLOB)) AS logical_digest_bytes,
75
+ CASE WHEN typeof(o.record_json) = 'text' AND length(CAST(o.record_json AS BLOB)) <= ? THEN o.record_json ELSE NULL END AS record_json,
76
+ length(CAST(o.record_json AS BLOB)) AS record_json_bytes
77
+ FROM requested
78
+ JOIN operations AS o ON o.record_id = requested.record_id
79
+ JOIN release_metadata AS r
80
+ ON r.catalog_id = o.catalog_id AND r.release_id = o.release_id
81
+ WHERE o.catalog_id = ? AND o.release_id = ?
82
+ AND r.format IN (4, 5) AND r.contract = 1
83
+ ORDER BY o.record_id COLLATE BINARY
84
+ LIMIT ?;`;
67
85
  const digestPattern = /^[0-9a-f]{64}$/;
68
86
  const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
69
87
  const signaturePattern = /^[A-Za-z0-9_-]+$/;
@@ -104,6 +122,10 @@ export const CATALOG_STORE_PUBLIC_MESSAGES = Object.freeze({
104
122
  recordJsonInvalid: "Stored record JSON is invalid",
105
123
  operationRowAmbiguous: "Stored operation row is ambiguous",
106
124
  operationIdentifierMismatch: "Stored operation identifier does not match request",
125
+ operationRequestLimitExceeded: "Operation request exceeds its limit",
126
+ operationRequestInvalid: "Operation request is invalid",
127
+ operationTransportTooManyRows: "Stored operation transport returned too many rows",
128
+ operationRowsInvalid: "Stored operation rows are invalid",
107
129
  schemaRequestLimitExceeded: "Schema request exceeds its limit",
108
130
  schemaRequestInvalid: "Schema request is invalid",
109
131
  schemaTransportTooManyRows: "Stored schema transport returned too many rows",
@@ -400,6 +422,12 @@ function snapshotSearchQuery(value, limits) {
400
422
  }
401
423
  }
402
424
  function snapshotSchemaIds(value, limits) {
425
+ return snapshotRecordIds(value, limits, requestedSchemaId, CATALOG_STORE_PUBLIC_MESSAGES.schemaRequestLimitExceeded, CATALOG_STORE_PUBLIC_MESSAGES.schemaRequestInvalid);
426
+ }
427
+ function snapshotOperationIds(value, limits) {
428
+ return snapshotRecordIds(value, limits, requestedOperationId, CATALOG_STORE_PUBLIC_MESSAGES.operationRequestLimitExceeded, CATALOG_STORE_PUBLIC_MESSAGES.operationRequestInvalid);
429
+ }
430
+ function snapshotRecordIds(value, limits, parse, limitMessage, invalidMessage) {
403
431
  try {
404
432
  if (!Array.isArray(value) ||
405
433
  Object.getPrototypeOf(value) !== Array.prototype)
@@ -413,7 +441,7 @@ function snapshotSchemaIds(value, limits) {
413
441
  length.value < 0)
414
442
  throw new Error();
415
443
  if (length.value > limits.maxManifestRecords)
416
- throw input(CATALOG_STORE_PUBLIC_MESSAGES.schemaRequestLimitExceeded);
444
+ throw input(limitMessage);
417
445
  const names = Object.getOwnPropertyNames(value);
418
446
  if (names.length !== length.value + 1 || !names.includes("length"))
419
447
  throw new Error();
@@ -423,10 +451,10 @@ function snapshotSchemaIds(value, limits) {
423
451
  const id = Object.getOwnPropertyDescriptor(value, String(index));
424
452
  if (!id?.enumerable || !("value" in id))
425
453
  throw new Error();
426
- const parsed = requestedSchemaId(id.value);
454
+ const parsed = parse(id.value);
427
455
  totalBytes += textEncoder.encode(parsed).byteLength;
428
456
  if (totalBytes > limits.maxSchemaClosureBytes)
429
- throw input(CATALOG_STORE_PUBLIC_MESSAGES.schemaRequestLimitExceeded);
457
+ throw input(limitMessage);
430
458
  ids.push(parsed);
431
459
  }
432
460
  return ids;
@@ -434,7 +462,7 @@ function snapshotSchemaIds(value, limits) {
434
462
  catch (error) {
435
463
  if (error instanceof OpenApiMcpError)
436
464
  throw error;
437
- throw input(CATALOG_STORE_PUBLIC_MESSAGES.schemaRequestInvalid);
465
+ throw input(invalidMessage);
438
466
  }
439
467
  }
440
468
  async function d1All(database, sql, values, errorCode) {
@@ -540,6 +568,37 @@ class D1CatalogStore {
540
568
  throw failure("RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.operationIdentifierMismatch);
541
569
  return row;
542
570
  }
571
+ async getOperations(catalog, release, ids) {
572
+ const catalogIdValue = catalogId(catalog);
573
+ const releaseIdValue = releaseId(release);
574
+ const operationIds = snapshotOperationIds(ids, this.limits);
575
+ if (operationIds.length === 0)
576
+ return [];
577
+ const requested = new Set(operationIds);
578
+ const rows = await d1All(this.database, GET_OPERATIONS_SQL, [
579
+ canonicalJson([...requested].sort()),
580
+ operationIdBytes,
581
+ digestBytes,
582
+ this.limits.maxRecordBytes,
583
+ catalogIdValue,
584
+ releaseIdValue,
585
+ requested.size + 1,
586
+ ], "RECORD_DIGEST_MISMATCH");
587
+ if (rows.length > requested.size)
588
+ throw failure("RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.operationTransportTooManyRows);
589
+ // Unlike getSchemas, absent IDs are omitted rather than rejected, matching
590
+ // getOperation's null; callers decide whether absence is fatal.
591
+ const records = [];
592
+ let previous = "";
593
+ for (const value of rows) {
594
+ const row = decodeRecord(value, "operation", this.limits);
595
+ if (!requested.has(row.id) || row.id <= previous)
596
+ throw failure("RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.operationRowsInvalid);
597
+ previous = row.id;
598
+ records.push(row);
599
+ }
600
+ return records;
601
+ }
543
602
  async getSchemas(catalog, release, ids) {
544
603
  const catalogIdValue = catalogId(catalog);
545
604
  const releaseIdValue = releaseId(release);
@@ -167,6 +167,13 @@ export interface CatalogStore {
167
167
  getManifest(catalogId: CatalogId, releaseId: ReleaseId): Promise<ManifestEnvelope>;
168
168
  searchCandidates(query: SearchQuery): Promise<readonly CandidateRef[]>;
169
169
  getOperation(catalogId: CatalogId, releaseId: ReleaseId, id: TypedOperationId): Promise<StoredRecord<OperationRecordV4> | null>;
170
+ /**
171
+ * Optional batched operation read used by complete-release verification.
172
+ * Returns the stored rows for the requested IDs; a missing ID is omitted
173
+ * (as `getOperation` returns null). Stores without it are read row by row.
174
+ * It has no response-size bound of its own; callers bound the batch.
175
+ */
176
+ getOperations?(catalogId: CatalogId, releaseId: ReleaseId, ids: readonly TypedOperationId[]): Promise<readonly StoredRecord<OperationRecordV4>[]>;
170
177
  getSchemas(catalogId: CatalogId, releaseId: ReleaseId, ids: readonly TypedSchemaId[]): Promise<readonly StoredRecord<SchemaRecordV4>[]>;
171
178
  }
172
179
  /** Immutable high-water data plus the release currently admitted for use. */
@@ -16,5 +16,6 @@ export declare class SqliteCatalogStore implements CatalogStore {
16
16
  getManifest(catalog: CatalogId, release: ReleaseId): Promise<ManifestEnvelope>;
17
17
  searchCandidates(query: SearchQuery): Promise<readonly import("../runtime/types.ts").CandidateRef[]>;
18
18
  getOperation(catalog: CatalogId, release: ReleaseId, id: TypedOperationId): Promise<StoredRecord<OperationRecordV4> | null>;
19
+ getOperations(catalog: CatalogId, release: ReleaseId, ids: readonly TypedOperationId[]): Promise<readonly StoredRecord<OperationRecordV4>[]>;
19
20
  getSchemas(catalog: CatalogId, release: ReleaseId, ids: readonly TypedSchemaId[]): Promise<readonly StoredRecord<SchemaRecordV4>[]>;
20
21
  }
@@ -364,6 +364,9 @@ export class SqliteCatalogStore {
364
364
  async getOperation(catalog, release, id) {
365
365
  return await this.#v4Store().getOperation(catalog, release, id);
366
366
  }
367
+ async getOperations(catalog, release, ids) {
368
+ return await this.#v4Store().getOperations(catalog, release, ids);
369
+ }
367
370
  async getSchemas(catalog, release, ids) {
368
371
  return await this.#v4Store().getSchemas(catalog, release, ids);
369
372
  }
@@ -30,6 +30,10 @@ export function createStdioSearchRuntime(catalogs, options) {
30
30
  throw new OpenApiMcpError("UPSTREAM_ERROR");
31
31
  },
32
32
  };
33
+ // Batch only when every source store can; otherwise keep per-row reads.
34
+ // Every source was just checked, so `?? []` only fails closed as missing.
35
+ if (ordered.every((entry) => entry.store.getOperations !== undefined))
36
+ store.getOperations = async (catalog, release, ids) => (await storeFor(catalog, release).getOperations?.(catalog, release, ids)) ?? [];
33
37
  return createRuntimeWithCandidateLookup({ ...options, limits, store }, async (query, tryChargeSource) => {
34
38
  const batches = [];
35
39
  let remaining = query.limit;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@knitli/openapi-mcp",
3
3
  "private": false,
4
- "version": "1.3.0",
4
+ "version": "1.4.0",
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",