@knitli/openapi-mcp 1.4.1 → 2.0.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 +34 -7
- package/dist/runtime/types.d.ts +1 -1
- package/dist/stdio/authorizer.d.ts +12 -3
- package/dist/stdio/authorizer.js +117 -30
- package/dist/stdio/index.d.ts +5 -2
- package/dist/stdio/index.js +4 -3
- package/dist/stdio/server.d.ts +2 -2
- package/dist/stdio/server.js +12 -5
- package/package.json +1 -1
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
|
|
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
|
|
|
@@ -234,10 +237,33 @@ If an action reports `UPSTREAM_OUTCOME_UNKNOWN`, its error includes the exact
|
|
|
234
237
|
the uncertain outcome; the server has consumed the authorization and does not
|
|
235
238
|
automatically retry the action.
|
|
236
239
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
The built-in CLI denies actions with `ACTION_DENIED`: MCP client confirmation
|
|
241
|
+
does not prove independent operator approval. Configured `exactPolicies` cannot
|
|
242
|
+
activate through client responses alone. Search and reads remain available.
|
|
243
|
+
|
|
244
|
+
An embedding can pass the same approval callback as the second argument to
|
|
245
|
+
`serveOpenApiStdio(config, approve)`, provide a host-owned `StatefulActionAuthorizer` through
|
|
246
|
+
`createOpenApiMcpServer`, or use `createStdioActionAuthorizer(templates, approve)`
|
|
247
|
+
from `@knitli/openapi-mcp/stdio`. The built-in elicitation authorizers require an
|
|
248
|
+
`approve(presentation, details)` callback that authenticates operator approval outside
|
|
249
|
+
the MCP requester. Without it they deny. The callback receives the frozen
|
|
250
|
+
presentation bound to the exact prepared-call and credential-binding digests,
|
|
251
|
+
including policy digest and expiry when activating standing authority. It must
|
|
252
|
+
return literal `true` only for that approval; copying the client response or
|
|
253
|
+
always returning `true` defeats this boundary. Requester confirmation is still
|
|
254
|
+
required, but cannot replace the independent approval. Rejection, provider
|
|
255
|
+
errors, expiry, changed bindings, and reused pending state prevent dispatch.
|
|
256
|
+
|
|
257
|
+
The host-only `details` contains deeply frozen normalized argument values,
|
|
258
|
+
the non-secret credential binding, the complete compiled policy when activating
|
|
259
|
+
standing authority, the approval deadline, and an abort signal. These values
|
|
260
|
+
remain untrusted data; the host must display them safely for informed operator
|
|
261
|
+
approval. They are never included in the redacted MCP presentation. Providers
|
|
262
|
+
must honor the signal and release prompts or other work when aborted. Expiry or
|
|
263
|
+
request cancellation denies promptly; providers that ignore cancellation retain
|
|
264
|
+
a bounded slot until they settle, and exhausted capacity fails closed.
|
|
265
|
+
`compileExactPolicy` and its template/constraint types are exported from
|
|
266
|
+
`@knitli/openapi-mcp/stdio` for constructing exact policies in embeddings.
|
|
241
267
|
Destructive operations require the stronger destructive acknowledgment. An operator
|
|
242
268
|
may supply narrowly bound `exactPolicies` for eligible create/update operations;
|
|
243
269
|
they are not broad scopes or a general confirmation bypass. Do not construct policy
|
|
@@ -369,8 +395,9 @@ parent must be a real, owner-only directory. Generic startup errors are delibera
|
|
|
369
395
|
to avoid exposing config secrets; validate with `parseOpenApiStdioConfig` in a
|
|
370
396
|
controlled operator process. Missing environment credentials or OAuth elicitation
|
|
371
397
|
must be resolved before a call can proceed. An action refused for lack of
|
|
372
|
-
confirmation needs an
|
|
373
|
-
|
|
398
|
+
confirmation needs an embedding with an independent operator approval provider;
|
|
399
|
+
the built-in CLI has no such channel. Do not retry a changed action with old
|
|
400
|
+
approval state.
|
|
374
401
|
|
|
375
402
|
From the repository, the package checks are:
|
|
376
403
|
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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>;
|
package/dist/stdio/authorizer.js
CHANGED
|
@@ -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 &&
|
|
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 ||
|
|
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 (
|
|
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.#
|
|
332
|
+
if (this.#approvalsInFlight >= this.#limits.pending ||
|
|
333
|
+
this.#receipts.size >= this.#limits.receipts ||
|
|
322
334
|
(newActivation && this.#activations.size >= this.#limits.activations)) {
|
|
323
|
-
//
|
|
335
|
+
// Before independent approval, capacity denial leaves the pending request retriable.
|
|
324
336
|
return deny("Authorization capacity is exhausted");
|
|
325
337
|
}
|
|
326
|
-
|
|
338
|
+
// Reserve the pending slot while the independent channel is awaiting approval.
|
|
339
|
+
this.#approving.add(pending);
|
|
327
340
|
try {
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
|
|
368
|
+
finally {
|
|
369
|
+
if (this.#pending.get(pending.pendingId) === pending) {
|
|
370
|
+
this.#pending.delete(pending.pendingId);
|
|
371
|
+
}
|
|
332
372
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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({
|
package/dist/stdio/index.d.ts
CHANGED
|
@@ -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 {
|
|
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>;
|
package/dist/stdio/index.js
CHANGED
|
@@ -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 {
|
|
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);
|
package/dist/stdio/server.d.ts
CHANGED
|
@@ -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;
|
package/dist/stdio/server.js
CHANGED
|
@@ -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({
|
|
15
|
-
|
|
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
|
|
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