@panguard-ai/panguard-mcp-proxy 1.7.3 → 1.8.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 ADDED
@@ -0,0 +1,18 @@
1
+ # @panguard-ai/panguard-mcp-proxy
2
+
3
+ MCP Proxy — runtime interception for AI agent tool calls using ATR (Agent Threat Rules).
4
+
5
+ Transparent tool-call evaluation:
6
+
7
+ - Intercepts MCP tool calls at runtime
8
+ - Evaluates threats against 675+ ATR rules
9
+ - Allows or blocks execution based on policy
10
+ - Integration with Claude, Cursor, and other AI agents
11
+
12
+ Part of the **PanGuard Community** suite — 100% free and open source (MIT).
13
+
14
+ ```bash
15
+ npm install @panguard-ai/panguard-mcp-proxy
16
+ ```
17
+
18
+ **License**: MIT
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @module @panguard-ai/panguard-mcp-proxy/evaluator
8
8
  */
9
+ import type { AgentEvent } from '@panguard-ai/atr';
9
10
  export interface EvalResult {
10
11
  readonly outcome: 'allow' | 'deny' | 'ask';
11
12
  readonly reason: string;
@@ -52,8 +53,21 @@ export declare class ProxyEvaluator {
52
53
  getRuleCount(): number;
53
54
  /** Flatten args into a readable string for ATR regex matching */
54
55
  private flattenArgs;
55
- /** Evaluate a tool call (PreToolUse) */
56
- evaluateToolCall(toolName: string, args: Record<string, unknown>): Promise<EvalResult>;
56
+ /**
57
+ * Evaluate a tool call (PreToolUse).
58
+ *
59
+ * `eventType` selects which ATR rules apply. The MCP proxy sees genuine
60
+ * MCP exchanges, so it keeps the 'mcp_exchange' default. The Claude-Code /
61
+ * Gemini / Codex PreToolUse hook wraps built-in tools (Bash, Edit, Write,
62
+ * WebFetch) — those are 'tool_call' events. Passing 'tool_call' makes the
63
+ * engine run the tool_call rule set (shell injection, credential theft,
64
+ * SSRF, RCE, privilege escalation) AND, via the engine's mcp-over-tool
65
+ * exception, the mcp_exchange rules — while correctly skipping
66
+ * multi_agent_comm and llm_io rules that never apply to a shell command.
67
+ * The old hardcoded 'mcp_exchange' both missed the tool_call attack rules
68
+ * and false-fired multi_agent_comm rules on ordinary commands.
69
+ */
70
+ evaluateToolCall(toolName: string, args: Record<string, unknown>, eventType?: AgentEvent['type']): Promise<EvalResult>;
57
71
  /** Evaluate a tool response (PostToolUse) */
58
72
  evaluateToolResponse(toolName: string, response: string): Promise<EvalResult>;
59
73
  private evaluate;
package/dist/evaluator.js CHANGED
@@ -122,8 +122,21 @@ export class ProxyEvaluator {
122
122
  }
123
123
  return parts.join('\n');
124
124
  }
125
- /** Evaluate a tool call (PreToolUse) */
126
- async evaluateToolCall(toolName, args) {
125
+ /**
126
+ * Evaluate a tool call (PreToolUse).
127
+ *
128
+ * `eventType` selects which ATR rules apply. The MCP proxy sees genuine
129
+ * MCP exchanges, so it keeps the 'mcp_exchange' default. The Claude-Code /
130
+ * Gemini / Codex PreToolUse hook wraps built-in tools (Bash, Edit, Write,
131
+ * WebFetch) — those are 'tool_call' events. Passing 'tool_call' makes the
132
+ * engine run the tool_call rule set (shell injection, credential theft,
133
+ * SSRF, RCE, privilege escalation) AND, via the engine's mcp-over-tool
134
+ * exception, the mcp_exchange rules — while correctly skipping
135
+ * multi_agent_comm and llm_io rules that never apply to a shell command.
136
+ * The old hardcoded 'mcp_exchange' both missed the tool_call attack rules
137
+ * and false-fired multi_agent_comm rules on ordinary commands.
138
+ */
139
+ async evaluateToolCall(toolName, args, eventType = 'mcp_exchange') {
127
140
  const start = Date.now();
128
141
  // Check Guard blocklist first (instant deny, no regex needed)
129
142
  this.refreshBlocklist();
@@ -139,7 +152,7 @@ export class ProxyEvaluator {
139
152
  // Flatten args into natural text so ATR regexes can match content like paths and commands
140
153
  const flatContent = `${toolName} ${this.flattenArgs(args)}`;
141
154
  const event = {
142
- type: 'mcp_exchange',
155
+ type: eventType,
143
156
  timestamp: new Date().toISOString(),
144
157
  content: flatContent,
145
158
  fields: {
package/dist/proxy.d.ts CHANGED
@@ -32,7 +32,7 @@ export interface ProxyConfig {
32
32
  /** The subset of ProxyEvaluator the proxy uses — injectable for testing. */
33
33
  export interface ProxyEvaluatorLike {
34
34
  loadRules(): Promise<number>;
35
- evaluateToolCall(toolName: string, args: Record<string, unknown>): Promise<EvalResult>;
35
+ evaluateToolCall(toolName: string, args: Record<string, unknown>, eventType?: string): Promise<EvalResult>;
36
36
  evaluateToolResponse(toolName: string, response: string): Promise<EvalResult>;
37
37
  }
38
38
  export declare class MCPProxy {
package/dist/proxy.js CHANGED
@@ -64,12 +64,21 @@ export class MCPProxy {
64
64
  config.failMode ??
65
65
  (envFailMode === 'open' || envFailMode === 'closed' ? envFailMode : 'closed');
66
66
  this.evalTimeout = config.evalTimeout ?? 5000;
67
- this.sessionId =
68
- config.sessionId ?? `mcp-proxy-${process.pid}-${Date.now().toString(36)}`;
67
+ this.sessionId = config.sessionId ?? `mcp-proxy-${process.pid}-${Date.now().toString(36)}`;
69
68
  this.agentId = config.agentId ?? process.env['PANGUARD_AGENT_ID'] ?? 'mcp-agent';
70
- // Sync sub-ms pre-check. Runs in front of the async evaluator so the worst
71
- // payloads (and any session the brain flags) are blocked instantly — and,
72
- // with fail-closed as the default, an unavailable async evaluator denies.
69
+ // Sync sub-ms pre-check (InlineGate.onAction) that reads riskStore: once a
70
+ // session is escalated to 'high' it fast-blocks subsequent calls before the
71
+ // async evaluator even runs. That escalation is FED by recordEvalVerdict()
72
+ // (a real ATR deny -> riskStore.set high), so the session-risk loop is live.
73
+ //
74
+ // HONEST SCOPE (Community): the async behavioral brain — RiskAnalyzer.analyze
75
+ // via guard.onSessionActivity — is NOT driven here; it needs a real
76
+ // ContentDetector + per-session event feed and, more importantly, a real
77
+ // ContainmentController to act on its verdicts (Community ships Noop). So the
78
+ // detector is a deliberate no-op, not a forgotten wire: content detection is
79
+ // the ATR engine (evaluateToolCall/evaluateToolResponse) and session
80
+ // escalation is the sync riskStore path above. Wiring the behavioral brain +
81
+ // active containment is a Pro-tier layer, tracked separately.
73
82
  this.riskStore = new InMemoryRiskStore();
74
83
  this.guard = new GuardGate({
75
84
  gate: new InlineGate(),
@@ -149,7 +158,11 @@ export class MCPProxy {
149
158
  * cannot lock out a legitimate agent.
150
159
  */
151
160
  recordEvalVerdict(verdict) {
152
- if (verdict.outcome === 'deny' && verdict.confidence >= MCPProxy.ESCALATE_CONFIDENCE) {
161
+ // Normalize confidence to a 0-100 scale before the threshold check: the ATR
162
+ // engine reports match confidence on a 0-1 scale, so comparing it raw to a
163
+ // 0-100 threshold (95) made this escalation NEVER fire for a real deny.
164
+ const conf = verdict.confidence <= 1 ? verdict.confidence * 100 : verdict.confidence;
165
+ if (verdict.outcome === 'deny' && conf >= MCPProxy.ESCALATE_CONFIDENCE) {
153
166
  this.riskStore.set(this.sessionId, { level: 'high', reasons: [...verdict.matchedRules] });
154
167
  }
155
168
  }
@@ -238,11 +251,17 @@ export class MCPProxy {
238
251
  ],
239
252
  };
240
253
  }
241
- // PreToolUse: evaluate the call
254
+ // PreToolUse: evaluate the call as a 'tool_call' event so the full
255
+ // tool_call rule family (shell injection, SSRF, SQLi, credential theft,
256
+ // privilege escalation, tool poisoning) runs — an MCP tool call carries
257
+ // exactly those payloads in its args. Via the engine's mcp-over-tool
258
+ // exception this is a superset of the mcp_exchange rules, so nothing that
259
+ // 'mcp_exchange' caught is lost. (The old default silently skipped ~44
260
+ // tool_call rules on every MCP call.)
242
261
  let preResult;
243
262
  try {
244
263
  preResult = await Promise.race([
245
- this.evaluator.evaluateToolCall(name, toolArgs),
264
+ this.evaluator.evaluateToolCall(name, toolArgs, 'tool_call'),
246
265
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), this.evalTimeout)),
247
266
  ]);
248
267
  }
@@ -287,11 +306,25 @@ export class MCPProxy {
287
306
  }
288
307
  // Forward to upstream
289
308
  const result = await client.callTool({ name, arguments: toolArgs });
290
- // PostToolUse: evaluate the response
309
+ // PostToolUse: evaluate the response. Serialize EVERY content block —
310
+ // text AND non-text (resource URIs, embedded resource.text/blob, image
311
+ // data) — so a malicious server cannot smuggle an exfil/injection payload
312
+ // through a resource/image block that a text-only extractor would silently
313
+ // drop. Cap is 256KB (not 10KB) so padding the payload past a tiny cap no
314
+ // longer evades the scan.
291
315
  const responseText = result.content
292
- ?.map((c) => c.text ?? '')
316
+ ?.map((c) => {
317
+ if (typeof c['text'] === 'string')
318
+ return c['text'];
319
+ try {
320
+ return JSON.stringify(c);
321
+ }
322
+ catch {
323
+ return '';
324
+ }
325
+ })
293
326
  .join('\n')
294
- .slice(0, 10000); // Cap at 10KB for evaluation
327
+ .slice(0, 262144);
295
328
  if (responseText) {
296
329
  let postResult;
297
330
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panguard-ai/panguard-mcp-proxy",
3
- "version": "1.7.3",
3
+ "version": "1.8.0",
4
4
  "description": "MCP Proxy — runtime interception for AI agent tool calls using ATR rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,13 +20,13 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@modelcontextprotocol/sdk": "^1.12.0",
23
- "agent-threat-rules": "^3.5.0",
24
- "@panguard-ai/atr": "1.7.3",
23
+ "agent-threat-rules": "^3.5.6",
24
+ "@panguard-ai/panguard-guard": "1.8.0",
25
25
  "@panguard-ai/containment": "0.1.0",
26
- "@panguard-ai/panguard-guard": "1.7.3"
26
+ "@panguard-ai/atr": "1.8.0"
27
27
  },
28
28
  "peerDependencies": {
29
- "@panguard-ai/atr": "1.7.3"
29
+ "@panguard-ai/atr": "1.8.0"
30
30
  },
31
31
  "files": [
32
32
  "dist",
@@ -34,6 +34,14 @@
34
34
  "README.md",
35
35
  "LICENSE"
36
36
  ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/panguard-ai/panguard-ai.git",
40
+ "directory": "packages/panguard-mcp-proxy"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
37
45
  "publishConfig": {
38
46
  "access": "public"
39
47
  },