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

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/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.18 @modelcontextprotocol/sdk@1.31.0
14
+ npm install @webdecoy/ai-protection@0.1.0-alpha.19 @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
@@ -115,6 +115,12 @@ have unknown work. Never retry writes without application/provider idempotency.
115
115
  for its runtime prerequisite and application-enforced bounds. Full product
116
116
  acceptance remains separate from this adapter's tested contract.
117
117
 
118
+ From alpha.19, every tool result also carries `_meta["webdecoy.com/action"].actionId`:
119
+ allowed results (merged with your own `_meta`), denials and failed callbacks alike.
120
+ It is the action ID on that call's reported evidence, so the caller or an operator
121
+ can find the exact action in the dashboard. It is a random identifier per call and
122
+ carries no identity, arguments or result content.
123
+
118
124
  This is an explicit tools integration, not transparent protection of an existing
119
125
  whole MCP server. See the [MCP transport specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports)
120
126
  and [authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization).
package/actions.d.mts CHANGED
@@ -115,4 +115,4 @@ export function createActionProtection<T>(options: {
115
115
  actions: Record<string, ActionDefinition>;
116
116
  /** Local best-effort observer. sharedRuntime enables independent hosted reporting. */
117
117
  onEvent?(event: ActionEvent): void | Promise<void>;
118
- }): {flush():Promise<void>;run(action: string, args: ActionInput, authenticationContext: T, options?: {signal?: AbortSignal}): Promise<unknown>};
118
+ }): {flush():Promise<void>;run(action: string, args: ActionInput, authenticationContext: T, options?: {signal?: AbortSignal; /** Receives this invocation's action ID before any check runs. */ onAction?(actionId: string): void}): Promise<unknown>};
package/actions.mjs CHANGED
@@ -82,8 +82,10 @@ export function createActionProtection(options) {
82
82
  if (!actions.size || actions.size > 128) throw Error('Expected 1–128 actions');
83
83
  const runtime=prepareActionRuntime(options,actions);
84
84
  let pendingEvents = 0;
85
- return Object.freeze({flush:async()=>{await runtime?.flush();},async run(name, input, authenticationContext, {signal} = {}) {
85
+ return Object.freeze({flush:async()=>{await runtime?.flush();},async run(name, input, authenticationContext, {signal, onAction} = {}) {
86
86
  const actionId = randomUUID();
87
+ // Lets an adapter return the action ID to its caller; never affects the decision.
88
+ try { onAction?.(actionId); } catch {}
87
89
  // Unknown caller-controlled action strings are never placed in evidence.
88
90
  const action = actions.get(name), eventAction = action ? name : 'unregistered';
89
91
  let attempted = false,completed=false,lease,work,callerEvidence;
package/mcp.mjs CHANGED
@@ -227,16 +227,23 @@ export function createProtectedMCPHandler(options) {
227
227
  throw new McpError(ErrorCode.InvalidParams, 'Tool unavailable');
228
228
  running.started = true;
229
229
  const signal = AbortSignal.any([extra.signal, disconnected.signal, running.controller.signal]);
230
+ // The action ID matches this call's reported evidence, so the caller can find it in the dashboard.
231
+ let actionId;
232
+ const action = () => ({ 'webdecoy.com/action': { actionId } });
230
233
  try {
231
- return await guard.run(request.params.name, (request.params.arguments ?? {}), caller, { signal });
234
+ const result = await guard.run(request.params.name, (request.params.arguments ?? {}), caller, { signal, onAction: id => { actionId = id; } });
235
+ if (!result || typeof result !== 'object' || Array.isArray(result))
236
+ return result;
237
+ const meta = result._meta && typeof result._meta === 'object' && !Array.isArray(result._meta) ? result._meta : {};
238
+ return { ...result, _meta: { ...meta, ...action() } };
232
239
  }
233
240
  catch (e) {
234
241
  if (signal.aborted)
235
242
  throw new McpError(ErrorCode.InternalError, 'Request cancelled');
236
243
  if (e instanceof ActionDenied)
237
244
  return { isError: true, content: [{ type: 'text', text: `Action denied: ${e.reason}` }],
238
- _meta: { 'webdecoy.com/action-error': { reason: e.reason, status: e.status, ...(e.retryAfterSeconds ? { retryAfterSeconds: e.retryAfterSeconds } : {}) } } };
239
- return { isError: true, content: [{ type: 'text', text: 'Action failed; outcome may be unknown' }] };
245
+ _meta: { ...action(), 'webdecoy.com/action-error': { reason: e.reason, status: e.status, ...(e.retryAfterSeconds ? { retryAfterSeconds: e.retryAfterSeconds } : {}) } } };
246
+ return { isError: true, content: [{ type: 'text', text: 'Action failed; outcome may be unknown' }], _meta: action() };
240
247
  }
241
248
  finally {
242
249
  cleanup();
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.18",
14
+ "version": "0.1.0-alpha.19",
15
15
  "exports": {
16
16
  "./fetch": {
17
17
  "types": "./fetch.d.mts",