@webdecoy/ai-protection 0.1.0-alpha.17 → 0.1.0-alpha.18

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.
Files changed (4) hide show
  1. package/MCP.md +12 -4
  2. package/mcp.d.mts +1 -1
  3. package/mcp.mjs +8 -6
  4. package/package.json +1 -1
package/MCP.md CHANGED
@@ -11,7 +11,7 @@ resource authorization.
11
11
  Available in `0.1.0-alpha.5` and later compatible alpha releases:
12
12
 
13
13
  ```sh
14
- npm install @webdecoy/ai-protection@0.1.0-alpha.17 @modelcontextprotocol/sdk@1.31.0
14
+ npm install @webdecoy/ai-protection@0.1.0-alpha.18 @modelcontextprotocol/sdk@1.31.0
15
15
  ```
16
16
 
17
17
  Requires Node 22.22.3+ and MCP SDK **1.31.0**. The MCP SDK is an optional peer, so
@@ -161,9 +161,17 @@ Discovery uses schema-3 reports on the existing reporting endpoint; deploy a
161
161
  compatible hosted runtime first. Old runtimes reject these optional reports
162
162
  without affecting tool listing. Reporting has bounded pending work and a deadline,
163
163
  never blocks authorization, and does not retry. Call `await handler.flush()` at a
164
- host shutdown/lifecycle boundary to drain pending discovery reports; it does not
165
- wait for active tool calls or action reports. Abruptly terminated hosts can lose
166
- reports. Discovery advertisements never increment tool action or request counts.
164
+ host shutdown/lifecycle boundary to drain discovery and tool-action reports already
165
+ queued when flush begins. From alpha.18, the action reporting queue is shared by
166
+ all calls on one handler and is bounded by `sharedRuntime.maxPendingReports`
167
+ (default 100); discovery uses a separately bounded queue. Full queues drop reports
168
+ without retrying or blocking tool execution.
169
+
170
+ Stop accepting new requests and let active tools complete or cancel before the
171
+ final flush. Flush does not wait for running tools, future reports, arbitrary
172
+ `onEvent`/`onReport` callbacks or dashboard persistence. Reporting deadlines still
173
+ apply; an unavailable/timeout/drop is not delivery success. Abruptly terminated
174
+ hosts can lose reports. Discovery advertisements never increment tool action or request counts.
167
175
 
168
176
 
169
177
  ### Calls before discovery (alpha.8+)
package/mcp.d.mts CHANGED
@@ -44,6 +44,6 @@ export interface ProtectedMCPOptions {
44
44
  */
45
45
  export function createProtectedMCPHandler(options: ProtectedMCPOptions):
46
46
  ((request: IncomingMessage, response: ServerResponse) => Promise<void>) & {
47
- /** Drain pending discovery reports at shutdown. Does not wait for running tools. */
47
+ /** Drain already queued discovery/action reports within reporting deadlines. Does not wait for running tools, future reports or observer callbacks. */
48
48
  flush(): Promise<void>;
49
49
  };
package/mcp.mjs CHANGED
@@ -50,7 +50,11 @@ export function createProtectedMCPHandler(options) {
50
50
  if (options.discovery) for (const [name, tool] of Object.entries(tools))
51
51
  tool.toolSchema = Object.freeze({serverId:options.discovery.serverId,hash:hashes[name],...(decoys.has(name)?{decoy:decoys.get(name)}:{effect:inferToolEffect(name,tool.inputSchema,tool.annotations),permissions:Object.freeze({schema:1,required_scopes:tool.requiredScopes.length,application_authorization:true,additional_policy:typeof tool.policy==='function'})})});
52
52
  // Validate after discovery supplies the schema required by tool pauses.
53
- createActionProtection({ policyVersion: options.policyVersion, authenticate: options.authenticate, actions: tools, sharedRuntime: options.sharedRuntime });
53
+ if (typeof options.authenticate !== 'function') throw Error('Invalid MCP authenticator');
54
+ // One bounded reporting runtime per handler. The caller is passed per run,
55
+ // never held in mutable shared request state. HTTP authentication stays above
56
+ // dispatch; the action boundary independently validates and snapshots it.
57
+ const guard = createActionProtection({ policyVersion: options.policyVersion, authenticate: caller => caller, actions: tools, sharedRuntime: options.sharedRuntime, onEvent: options.onEvent });
54
58
  const runtime = options.sharedRuntime && { ...options.sharedRuntime };
55
59
  const serverId = options.discovery?.serverId;
56
60
  const catalogReporter = options.discovery ? createReporter({
@@ -177,7 +181,6 @@ export function createProtectedMCPHandler(options) {
177
181
  res.end();
178
182
  return;
179
183
  }
180
- const guard = createActionProtection({ policyVersion: options.policyVersion, authenticate: () => caller, actions: tools, sharedRuntime: options.sharedRuntime, onEvent: options.onEvent });
181
184
  // Scope escalation belongs at HTTP level, before the SDK opens an SSE stream.
182
185
  if (message.method === 'tools/call' && typeof message.params?.name === 'string') {
183
186
  const definition = Object.hasOwn(tools, message.params.name) ? tools[message.params.name] : undefined;
@@ -185,7 +188,7 @@ export function createProtectedMCPHandler(options) {
185
188
  // Run the same admission path for its sanitized denial evidence. It cannot
186
189
  // dispatch with a missing required scope, regardless of discovery results.
187
190
  try {
188
- await guard.run(message.params.name, {}, null, { signal: disconnected.signal });
191
+ await guard.run(message.params.name, {}, caller, { signal: disconnected.signal });
189
192
  }
190
193
  catch (e) {
191
194
  if (!(e instanceof ActionDenied))
@@ -225,7 +228,7 @@ export function createProtectedMCPHandler(options) {
225
228
  running.started = true;
226
229
  const signal = AbortSignal.any([extra.signal, disconnected.signal, running.controller.signal]);
227
230
  try {
228
- return await guard.run(request.params.name, (request.params.arguments ?? {}), null, { signal });
231
+ return await guard.run(request.params.name, (request.params.arguments ?? {}), caller, { signal });
229
232
  }
230
233
  catch (e) {
231
234
  if (signal.aborted)
@@ -237,7 +240,6 @@ export function createProtectedMCPHandler(options) {
237
240
  }
238
241
  finally {
239
242
  cleanup();
240
- void guard.flush();
241
243
  }
242
244
  });
243
245
  await server.connect(transport);
@@ -250,5 +252,5 @@ export function createProtectedMCPHandler(options) {
250
252
  res.destroy();
251
253
  }
252
254
  };
253
- return Object.assign(handle, { flush: async () => { await catalogReporter?.flush(); } });
255
+ return Object.assign(handle, { flush: async () => { await Promise.all([catalogReporter?.flush(), guard.flush()]); } });
254
256
  }
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "test:types": "tsc --strict --noEmit --module nodenext --target es2022 test/types.mts test/mcp-types.mts",
12
12
  "test:workers": "node --test test/workers/*.test.mjs"
13
13
  },
14
- "version": "0.1.0-alpha.17",
14
+ "version": "0.1.0-alpha.18",
15
15
  "exports": {
16
16
  "./fetch": {
17
17
  "types": "./fetch.d.mts",