@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 +18 -0
- package/dist/evaluator.d.ts +16 -2
- package/dist/evaluator.js +16 -3
- package/dist/proxy.d.ts +1 -1
- package/dist/proxy.js +44 -11
- package/package.json +13 -5
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
|
package/dist/evaluator.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
56
|
-
|
|
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
|
-
/**
|
|
126
|
-
|
|
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:
|
|
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
|
|
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.
|
|
71
|
-
//
|
|
72
|
-
//
|
|
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
|
-
|
|
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) =>
|
|
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,
|
|
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.
|
|
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.
|
|
24
|
-
"@panguard-ai/
|
|
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/
|
|
26
|
+
"@panguard-ai/atr": "1.8.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@panguard-ai/atr": "1.
|
|
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
|
},
|