@knitli/openapi-mcp 1.4.1 → 2.0.2

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
@@ -8,6 +8,9 @@ The executable artifact format is **5**, runtime contract **1**, and prepared-ca
8
8
  format **2**. This package is compiled TypeScript, separate from the Toolshed
9
9
  prompt plugins.
10
10
 
11
+ The standalone `serve` CLI denies actions because it has no independent operator
12
+ approval provider. Embedded hosts can supply one through `serveOpenApiStdio(config, approve)`.
13
+
11
14
  ## Install and check the runtime
12
15
 
13
16
  Local serving requires Node `>=24.16.0 <25`. Packed consumers exercise Node 24
@@ -186,7 +189,7 @@ These endpoints and client
186
189
  registration must be configured by the operator. The server uses authorization
187
190
  code PKCE with S256 and a temporary `127.0.0.1` callback listener on an ephemeral
188
191
  port at `/oauth/callback`; the identity provider must support that redirect.
189
- The client needs URL elicitation for OAuth and form elicitation for confirmation.
192
+ The client needs URL elicitation for OAuth.
190
193
  Open the offered authorization URL, finish login, then follow the client's resume
191
194
  flow. Unsupported elicitation fails closed.
192
195
 
@@ -229,15 +232,48 @@ port backed by one semantic search context, alongside `routes`, `authorizer`,
229
232
  and the same reduction-only `limits`. A single-catalog embedding can pass its
230
233
  existing runtime as `searchRuntime`; the CLI assembles shared search itself.
231
234
 
235
+ The built-in server uses stdio; HTTP hosting is not qualified by this package.
236
+ An HTTP embedder needs a fresh SDK server and transport per request or session,
237
+ tool-input and request-body limits, and token-audience validation when using
238
+ SDK bearer authentication. `createOpenApiMcpServer` does not expose the SDK's
239
+ `maxToolInputElements` option; HTTP hosting needs additional integration work.
240
+ Upgrading the SDK alone does not enable these protections; follow the upstream
241
+ [isolation](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-jm63-cq47-j5jp),
242
+ [input-limit](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-46jr-4wx8-v453), and
243
+ [audience](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-rvq5-wwqv-78pq) advisories.
244
+
232
245
  If an action reports `UPSTREAM_OUTCOME_UNKNOWN`, its error includes the exact
233
246
  `details.preparedCallDigest` and `retryable: false`. Use that digest to reconcile
234
247
  the uncertain outcome; the server has consumed the authorization and does not
235
248
  automatically retry the action.
236
249
 
237
- Actions require server-controlled authorization bound to the prepared request,
238
- credential profile, release, and argument digest. Review the elicited destination,
239
- operation, effect, and arguments, then explicitly confirm. Rejection, cancellation,
240
- expiry, a changed request, or a changed credential binding prevents dispatch.
250
+ The built-in CLI denies actions with `ACTION_DENIED`: MCP client confirmation
251
+ does not prove independent operator approval. Configured `exactPolicies` cannot
252
+ activate through client responses alone. Search and reads remain available.
253
+
254
+ An embedding can pass the same approval callback as the second argument to
255
+ `serveOpenApiStdio(config, approve)`, provide a host-owned `StatefulActionAuthorizer` through
256
+ `createOpenApiMcpServer`, or use `createStdioActionAuthorizer(templates, approve)`
257
+ from `@knitli/openapi-mcp/stdio`. The built-in elicitation authorizers require an
258
+ `approve(presentation, details)` callback that authenticates operator approval outside
259
+ the MCP requester. Without it they deny. The callback receives the frozen
260
+ presentation bound to the exact prepared-call and credential-binding digests,
261
+ including policy digest and expiry when activating standing authority. It must
262
+ return literal `true` only for that approval; copying the client response or
263
+ always returning `true` defeats this boundary. Requester confirmation is still
264
+ required, but cannot replace the independent approval. Rejection, provider
265
+ errors, expiry, changed bindings, and reused pending state prevent dispatch.
266
+
267
+ The host-only `details` contains deeply frozen normalized argument values,
268
+ the non-secret credential binding, the complete compiled policy when activating
269
+ standing authority, the approval deadline, and an abort signal. These values
270
+ remain untrusted data; the host must display them safely for informed operator
271
+ approval. They are never included in the redacted MCP presentation. Providers
272
+ must honor the signal and release prompts or other work when aborted. Expiry or
273
+ request cancellation denies promptly; providers that ignore cancellation retain
274
+ a bounded slot until they settle, and exhausted capacity fails closed.
275
+ `compileExactPolicy` and its template/constraint types are exported from
276
+ `@knitli/openapi-mcp/stdio` for constructing exact policies in embeddings.
241
277
  Destructive operations require the stronger destructive acknowledgment. An operator
242
278
  may supply narrowly bound `exactPolicies` for eligible create/update operations;
243
279
  they are not broad scopes or a general confirmation bypass. Do not construct policy
@@ -369,8 +405,9 @@ parent must be a real, owner-only directory. Generic startup errors are delibera
369
405
  to avoid exposing config secrets; validate with `parseOpenApiStdioConfig` in a
370
406
  controlled operator process. Missing environment credentials or OAuth elicitation
371
407
  must be resolved before a call can proceed. An action refused for lack of
372
- confirmation needs an elicitation-capable client or an eligible reviewed exact
373
- policy. Do not retry a changed action with old approval state.
408
+ confirmation needs an embedding with an independent operator approval provider;
409
+ the built-in CLI has no such channel. Do not retry a changed action with old
410
+ approval state.
374
411
 
375
412
  From the repository, the package checks are:
376
413
 
@@ -323,7 +323,7 @@ export interface SafeApprovalPresentation {
323
323
  preparedCallDigest: Sha256;
324
324
  }
325
325
  export interface ActionAuthorizer {
326
- authorize(call: PreparedCall, credential: CredentialAuthorizationBinding, context: AuthorizationContext): Promise<AuthorizationDecision>;
326
+ authorize(call: PreparedCall, credential: CredentialAuthorizationBinding, context: AuthorizationContext, signal?: AbortSignal): Promise<AuthorizationDecision>;
327
327
  consume(decision: AuthorizedActionDecision, call: PreparedCall, credential: CredentialAuthorizationBinding): Promise<void>;
328
328
  }
329
329
  export interface AuthorizedActionDecision {
@@ -1,5 +1,5 @@
1
1
  import { type ServerContext } from "@modelcontextprotocol/server";
2
- import type { ActionAuthorizer, VerifiedActionRequestState } from "../runtime/types.ts";
2
+ import type { ActionAuthorizer, CredentialAuthorizationBinding, JsonObject, SafeApprovalPresentation, VerifiedActionRequestState } from "../runtime/types.ts";
3
3
  import { type CompiledExactPolicy } from "./exact-policy.ts";
4
4
  interface InternalLimits {
5
5
  readonly pending: number;
@@ -16,10 +16,19 @@ export interface AuthorizerTestOptions {
16
16
  }
17
17
  export interface ClientElicitationAuthorizerOptions extends AuthorizerTestOptions {
18
18
  readonly elicitationAvailable: () => boolean;
19
+ /** Authenticate operator approval outside the MCP requester; never trust inputResponses. */
20
+ readonly approve?: (presentation: SafeApprovalPresentation, context: TrustedActionApprovalContext) => Promise<boolean>;
19
21
  }
20
- export interface ExactPolicyAuthorizerOptions extends AuthorizerTestOptions {
22
+ /** Host-only values; never include these details in an MCP response. */
23
+ export interface TrustedActionApprovalContext {
24
+ readonly normalizedArguments: Readonly<JsonObject>;
25
+ readonly credentialBinding: CredentialAuthorizationBinding;
26
+ readonly policy?: CompiledExactPolicy;
27
+ readonly expiresAt: number;
28
+ readonly signal: AbortSignal;
29
+ }
30
+ export interface ExactPolicyAuthorizerOptions extends ClientElicitationAuthorizerOptions {
21
31
  readonly templates: readonly CompiledExactPolicy[];
22
- readonly elicitationAvailable: () => boolean;
23
32
  }
24
33
  export interface StatefulActionAuthorizer extends ActionAuthorizer {
25
34
  readonly requestStateVerifier: (state: string, context?: ServerContext) => Promise<VerifiedActionRequestState>;
@@ -153,17 +153,21 @@ function snapshotContext(context) {
153
153
  class LedgerActionAuthorizer {
154
154
  requestStateVerifier;
155
155
  #elicitationAvailable;
156
+ #approve;
156
157
  #now;
157
158
  #randomBytes;
158
159
  #limits;
159
160
  #policies;
160
161
  #pending = new Map();
162
+ #approving = new WeakSet();
163
+ #approvalsInFlight = 0;
161
164
  #receipts = new Map();
162
165
  #decisions = new WeakMap();
163
166
  #activations = new Map();
164
167
  #requestState;
165
168
  constructor(options, policies = []) {
166
169
  this.#elicitationAvailable = options.elicitationAvailable;
170
+ this.#approve = options.approve;
167
171
  this.#now = options.now ?? Date.now;
168
172
  this.#randomBytes = options.randomBytes ?? defaultRandom;
169
173
  this.#limits = normalizeLimits(options.limits);
@@ -177,13 +181,15 @@ class LedgerActionAuthorizer {
177
181
  now: this.#now,
178
182
  isPending: (payload) => {
179
183
  const pending = this.#pending.get(payload.pendingId);
180
- return pending !== undefined && exactPayloadMatch(payload, pending);
184
+ return (pending !== undefined &&
185
+ !this.#approving.has(pending) &&
186
+ exactPayloadMatch(payload, pending));
181
187
  },
182
188
  });
183
189
  this.requestStateVerifier = this.#requestState.verify;
184
190
  Object.freeze(this);
185
191
  }
186
- async authorize(call, credential, context) {
192
+ async authorize(call, credential, context, signal) {
187
193
  let ownedContext;
188
194
  try {
189
195
  ownedContext = snapshotContext(context);
@@ -202,8 +208,10 @@ class LedgerActionAuthorizer {
202
208
  // Resume is deliberately handled before active-policy matching. An
203
209
  // echoed confirmation can never be bypassed by a newly active policy.
204
210
  if (ownedContext.kind === "resume") {
205
- return await this.#resume(prepared, ownedCredential, ownedContext);
211
+ return await this.#resume(prepared, ownedCredential, ownedContext, signal);
206
212
  }
213
+ if (signal?.aborted)
214
+ return deny("Action request was cancelled");
207
215
  const now = this.#now();
208
216
  this.#prune(now);
209
217
  const policy = this.#matchingPolicy(prepared, now);
@@ -264,16 +272,19 @@ class LedgerActionAuthorizer {
264
272
  throw deniedError();
265
273
  });
266
274
  }
267
- async #resume(call, credential, context) {
275
+ async #resume(call, credential, context, signal) {
268
276
  const payload = this.#requestState.read(context.requestState);
269
277
  if (payload === undefined)
270
278
  return deny("Invalid confirmation state");
271
279
  const pending = this.#pending.get(payload.pendingId);
272
- if (pending === undefined || !exactPayloadMatch(payload, pending)) {
280
+ if (pending === undefined ||
281
+ this.#approving.has(pending) ||
282
+ !exactPayloadMatch(payload, pending)) {
273
283
  return deny("Invalid confirmation state");
274
284
  }
275
285
  const now = this.#now();
276
- if (now >= pending.expiresAt ||
286
+ if (signal?.aborted ||
287
+ now >= pending.expiresAt ||
277
288
  call.preparedCallDigest !== pending.callDigest ||
278
289
  credential.bindingDigest !== pending.credentialBindingDigest) {
279
290
  this.#pending.delete(pending.pendingId);
@@ -318,37 +329,112 @@ class LedgerActionAuthorizer {
318
329
  ? this.#activationKey(policy.policyDigest, credential.bindingDigest)
319
330
  : undefined;
320
331
  const newActivation = activationKey !== undefined && !this.#activations.has(activationKey);
321
- if (this.#receipts.size >= this.#limits.receipts ||
332
+ if (this.#approvalsInFlight >= this.#limits.pending ||
333
+ this.#receipts.size >= this.#limits.receipts ||
322
334
  (newActivation && this.#activations.size >= this.#limits.activations)) {
323
- // Capacity denial is retriable and must not spend a valid human answer.
335
+ // Before independent approval, capacity denial leaves the pending request retriable.
324
336
  return deny("Authorization capacity is exhausted");
325
337
  }
326
- let decision;
338
+ // Reserve the pending slot while the independent channel is awaiting approval.
339
+ this.#approving.add(pending);
327
340
  try {
328
- decision = this.#mintReceipt(call, credential, policy ? "exact-policy" : "per-call", policy?.policyDigest, Math.min(now + RECEIPT_TTL_MS, activationExpiresAt ?? Number.MAX_SAFE_INTEGER));
341
+ if (!(await this.#awaitApproval(pending, call, credential, signal))) {
342
+ return deny("Trusted operator approval was not accepted");
343
+ }
344
+ const approvedAt = this.#now();
345
+ if (signal?.aborted ||
346
+ approvedAt >= pending.expiresAt ||
347
+ this.#pending.get(pending.pendingId) !== pending) {
348
+ return deny("Confirmation expired or no longer matches");
349
+ }
350
+ this.#prune(approvedAt);
351
+ if (activationKey !== undefined &&
352
+ !this.#activations.has(activationKey) &&
353
+ this.#activations.size >= this.#limits.activations) {
354
+ return deny("Authorization capacity is exhausted");
355
+ }
356
+ const decision = this.#mintReceipt(call, credential, policy ? "exact-policy" : "per-call", policy?.policyDigest, Math.min(approvedAt + RECEIPT_TTL_MS, activationExpiresAt ?? Number.MAX_SAFE_INTEGER));
357
+ if (policy !== undefined &&
358
+ activationKey !== undefined &&
359
+ activationExpiresAt !== undefined) {
360
+ this.#activations.set(activationKey, Object.freeze({
361
+ policyDigest: policy.policyDigest,
362
+ credentialBindingDigest: credential.bindingDigest,
363
+ expiresAt: activationExpiresAt,
364
+ }));
365
+ }
366
+ return decision;
329
367
  }
330
- catch {
331
- return deny("Authorization capacity is exhausted");
368
+ finally {
369
+ if (this.#pending.get(pending.pendingId) === pending) {
370
+ this.#pending.delete(pending.pendingId);
371
+ }
332
372
  }
333
- if (policy !== undefined &&
334
- activationKey !== undefined &&
335
- activationExpiresAt !== undefined) {
336
- this.#activations.set(activationKey, Object.freeze({
337
- policyDigest: policy.policyDigest,
338
- credentialBindingDigest: credential.bindingDigest,
339
- expiresAt: activationExpiresAt,
340
- }));
373
+ }
374
+ async #awaitApproval(pending, call, credential, signal) {
375
+ const approve = this.#approve;
376
+ const expiresAt = Math.min(pending.expiresAt, pending.activationExpiresAt ?? pending.expiresAt);
377
+ const remaining = expiresAt - this.#now();
378
+ if (approve === undefined ||
379
+ signal?.aborted ||
380
+ !Number.isFinite(remaining) ||
381
+ remaining <= 0)
382
+ return false;
383
+ const controller = new AbortController();
384
+ let stop = () => { };
385
+ const interrupted = new Promise((resolve) => {
386
+ stop = () => {
387
+ controller.abort();
388
+ resolve(false);
389
+ };
390
+ });
391
+ const timer = setTimeout(stop, remaining);
392
+ timer.unref();
393
+ signal?.addEventListener("abort", stop, { once: true });
394
+ try {
395
+ const details = Object.freeze({
396
+ normalizedArguments: call.normalizedArguments,
397
+ credentialBinding: credential,
398
+ ...(pending.policy === undefined ? {} : { policy: pending.policy }),
399
+ expiresAt,
400
+ signal: controller.signal,
401
+ });
402
+ this.#approvalsInFlight++;
403
+ let result;
404
+ try {
405
+ result = approve(pending.presentation, details);
406
+ }
407
+ catch {
408
+ this.#approvalsInFlight--;
409
+ return false;
410
+ }
411
+ // Providers that ignore cancellation still occupy a bounded slot until they settle.
412
+ const approval = Promise.resolve(result).then((accepted) => {
413
+ this.#approvalsInFlight--;
414
+ return accepted === true;
415
+ }, () => {
416
+ this.#approvalsInFlight--;
417
+ return false;
418
+ });
419
+ return await Promise.race([approval, interrupted]);
420
+ }
421
+ finally {
422
+ clearTimeout(timer);
423
+ signal?.removeEventListener("abort", stop);
424
+ controller.abort();
341
425
  }
342
- this.#pending.delete(pending.pendingId);
343
- return decision;
344
426
  }
345
427
  async #requestConfirmation(call, credential, policy, activationExpiresAt) {
428
+ if (this.#approve === undefined) {
429
+ return deny("Trusted operator approval is unavailable");
430
+ }
346
431
  if (!this.#elicitationAvailable()) {
347
432
  return deny("Client elicitation is unavailable");
348
433
  }
349
434
  const now = this.#now();
350
435
  this.#prune(now);
351
- if (this.#pending.size >= this.#limits.pending) {
436
+ if (this.#pending.size >= this.#limits.pending ||
437
+ this.#approvalsInFlight >= this.#limits.pending) {
352
438
  return deny("Authorization capacity is exhausted");
353
439
  }
354
440
  let pendingId;
@@ -365,21 +451,22 @@ class LedgerActionAuthorizer {
365
451
  if (policy !== undefined && activationExpiresAt === undefined) {
366
452
  return deny("Policy activation is invalid");
367
453
  }
454
+ const presentation = renderApproval(call, credential, policy === undefined
455
+ ? undefined
456
+ : {
457
+ policyDigest: policy.policyDigest,
458
+ expiresAt: activationExpiresAt,
459
+ template: policy.template,
460
+ });
368
461
  const pending = Object.freeze({
369
462
  pendingId,
370
463
  callDigest: call.preparedCallDigest,
371
464
  credentialBindingDigest: credential.bindingDigest,
372
465
  expiresAt,
466
+ presentation,
373
467
  ...(policy === undefined ? {} : { policy }),
374
468
  ...(activationExpiresAt === undefined ? {} : { activationExpiresAt }),
375
469
  });
376
- const presentation = renderApproval(call, credential, policy === undefined
377
- ? undefined
378
- : {
379
- policyDigest: policy.policyDigest,
380
- expiresAt: activationExpiresAt,
381
- template: policy.template,
382
- });
383
470
  this.#pending.set(pendingId, pending);
384
471
  try {
385
472
  const requestState = await this.#requestState.mint({
@@ -1,6 +1,9 @@
1
1
  import { type StdioServerHandle } from "@modelcontextprotocol/server/stdio";
2
+ import type { ClientElicitationAuthorizerOptions } from "./authorizer.ts";
2
3
  import { type OpenApiStdioConfig } from "./config.ts";
4
+ export type { StatefulActionAuthorizer, TrustedActionApprovalContext, } from "./authorizer.ts";
3
5
  export { type OpenApiStdioConfig, parseOpenApiStdioConfig } from "./config.ts";
4
- export { createOpenApiMcpServer, type OpenApiMcpServerOptions, } from "./server.ts";
6
+ export { type ArgumentConstraint, type CompiledExactPolicy, compileExactPolicy, type ExactPolicyTemplate, } from "./exact-policy.ts";
7
+ export { createOpenApiMcpServer, createStdioActionAuthorizer, type OpenApiMcpServerOptions, } from "./server.ts";
5
8
  /** Owns process-local credentials/catalogs independently of disposable SDK probes. */
6
- export declare function serveOpenApiStdio(input: OpenApiStdioConfig): Promise<StdioServerHandle>;
9
+ export declare function serveOpenApiStdio(input: OpenApiStdioConfig, approve?: ClientElicitationAuthorizerOptions["approve"]): Promise<StdioServerHandle>;
@@ -10,9 +10,10 @@ import { compileExactPolicy } from "./exact-policy.js";
10
10
  import { createStdioSearchRuntime } from "./search.js";
11
11
  import { createOpenApiMcpServer, createStdioActionAuthorizer, } from "./server.js";
12
12
  export { parseOpenApiStdioConfig } from "./config.js";
13
- export { createOpenApiMcpServer, } from "./server.js";
13
+ export { compileExactPolicy, } from "./exact-policy.js";
14
+ export { createOpenApiMcpServer, createStdioActionAuthorizer, } from "./server.js";
14
15
  /** Owns process-local credentials/catalogs independently of disposable SDK probes. */
15
- export async function serveOpenApiStdio(input) {
16
+ export async function serveOpenApiStdio(input, approve) {
16
17
  const config = parseOpenApiStdioConfig(input);
17
18
  const stores = [];
18
19
  const providers = [];
@@ -57,7 +58,7 @@ export async function serveOpenApiStdio(input) {
57
58
  // GenerationStore provides per-catalog CAS, not a cross-catalog transaction.
58
59
  for (const entry of admitted)
59
60
  await admitExecutableRelease({ store: entry.store, trust: config.trust, generations, limits }, entry.verified);
60
- const authorizer = createStdioActionAuthorizer(templates);
61
+ const authorizer = createStdioActionAuthorizer(templates, approve);
61
62
  const routes = [];
62
63
  for (const profile of config.profiles) {
63
64
  const catalogs = admitted.filter((entry) => entry.config.profileId === profile.profileId);
@@ -2,11 +2,11 @@ import { McpServer } from "@modelcontextprotocol/server";
2
2
  import type { CredentialProvider, OpenApiRuntime } from "../runtime/types.ts";
3
3
  import { type RuntimeLimits } from "../runtime/versions.ts";
4
4
  import type { createLocalDispatchBoundary } from "../sqlite/guarded-fetch.ts";
5
- import { type StatefulActionAuthorizer } from "./authorizer.ts";
5
+ import { type ClientElicitationAuthorizerOptions, type StatefulActionAuthorizer } from "./authorizer.ts";
6
6
  import type { CompiledExactPolicy } from "./exact-policy.ts";
7
7
  type Boundary = ReturnType<typeof createLocalDispatchBoundary>;
8
8
  /** One authorizer across factory instances preserves opaque continuation ownership. */
9
- export declare function createStdioActionAuthorizer(templates?: readonly CompiledExactPolicy[]): StatefulActionAuthorizer;
9
+ export declare function createStdioActionAuthorizer(templates?: readonly CompiledExactPolicy[], approve?: ClientElicitationAuthorizerOptions["approve"]): StatefulActionAuthorizer;
10
10
  export interface OpenApiServerRoute {
11
11
  readonly catalogId: string;
12
12
  readonly releaseId: string;
@@ -8,11 +8,18 @@ import { resolveRuntimeLimits, } from "../runtime/versions.js";
8
8
  import { createClientElicitationActionAuthorizer, createExactPolicyActionAuthorizer, } from "./authorizer.js";
9
9
  const requestCapabilities = new AsyncLocalStorage();
10
10
  /** One authorizer across factory instances preserves opaque continuation ownership. */
11
- export function createStdioActionAuthorizer(templates = []) {
11
+ export function createStdioActionAuthorizer(templates = [], approve) {
12
12
  const elicitationAvailable = () => requestCapabilities.getStore()?.form === true;
13
13
  return templates.length
14
- ? createExactPolicyActionAuthorizer({ templates, elicitationAvailable })
15
- : createClientElicitationActionAuthorizer({ elicitationAvailable });
14
+ ? createExactPolicyActionAuthorizer({
15
+ templates,
16
+ elicitationAvailable,
17
+ approve,
18
+ })
19
+ : createClientElicitationActionAuthorizer({
20
+ elicitationAvailable,
21
+ approve,
22
+ });
16
23
  }
17
24
  const argumentsSchema = z.strictObject({
18
25
  path: z.record(z.string(), z.json()).optional(),
@@ -264,7 +271,7 @@ async function executeAction(route, authorizer, input, ctx) {
264
271
  kind: "resume",
265
272
  requestState: state,
266
273
  inputResponses: ctx.mcpReq.inputResponses,
267
- });
274
+ }, ctx.mcpReq.signal);
268
275
  if (decision.status === "denied")
269
276
  throw new OpenApiMcpError("ACTION_DENIED");
270
277
  if (decision.status === "input-required")
@@ -335,7 +342,7 @@ export function createOpenApiMcpServer(options) {
335
342
  return outcome(await route.boundary.transport.dispatchRead(plan), snapshot);
336
343
  }));
337
344
  server.registerTool("action", {
338
- description: "Request one verified action with client approval or an explicitly activated exact policy.",
345
+ description: "Request one verified action with independent operator approval or an explicitly activated exact policy. Actions are denied when a trusted approval provider is unavailable.",
339
346
  inputSchema: actionSchema,
340
347
  }, (input, ctx) => handle(ctx, () => executeAction(routeFor(input.operation), options.authorizer, input, ctx)));
341
348
  return server;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@knitli/openapi-mcp",
3
3
  "private": false,
4
- "version": "1.4.1",
4
+ "version": "2.0.2",
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",
@@ -71,7 +71,7 @@
71
71
  "check": "bunx biome check ."
72
72
  },
73
73
  "dependencies": {
74
- "@modelcontextprotocol/server": "2.0.0",
74
+ "@modelcontextprotocol/server": "2.3.1",
75
75
  "undici": "8.10.2",
76
76
  "yaml": "2.9.0",
77
77
  "zod": "4.5.4"