agentshield-sdk 7.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/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- package/types/index.d.ts +2088 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — MCP (Model Context Protocol) Server
|
|
5
|
+
*
|
|
6
|
+
* Exposes Agent Shield capabilities as MCP tools over JSON-RPC 2.0 on stdin/stdout.
|
|
7
|
+
* Any MCP-capable AI agent (e.g. Claude) can use Agent Shield natively as a tool provider.
|
|
8
|
+
*
|
|
9
|
+
* All detection runs locally — no data ever leaves your environment.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // Start the server from CLI:
|
|
13
|
+
* // node src/mcp-server.js
|
|
14
|
+
* // node src/mcp-server.js --config ./agent-shield.json
|
|
15
|
+
*
|
|
16
|
+
* // Or programmatically:
|
|
17
|
+
* const { MCPServer } = require('./mcp-server');
|
|
18
|
+
* const server = new MCPServer({ sensitivity: 'high' });
|
|
19
|
+
* server.start();
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { AgentShield } = require('./index');
|
|
23
|
+
const { PIIRedactor } = require('./pii');
|
|
24
|
+
const { CanaryTokens } = require('./canary');
|
|
25
|
+
const { ShieldScoreCalculator } = require('./shield-score');
|
|
26
|
+
const { ThreatEncyclopedia, THREAT_ENCYCLOPEDIA } = require('./threat-encyclopedia');
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
// =========================================================================
|
|
31
|
+
// JSON-RPC 2.0 Error Codes
|
|
32
|
+
// =========================================================================
|
|
33
|
+
|
|
34
|
+
const JSON_RPC_ERRORS = {
|
|
35
|
+
PARSE_ERROR: { code: -32700, message: 'Parse error' },
|
|
36
|
+
INVALID_REQUEST: { code: -32600, message: 'Invalid Request' },
|
|
37
|
+
METHOD_NOT_FOUND: { code: -32601, message: 'Method not found' },
|
|
38
|
+
INVALID_PARAMS: { code: -32602, message: 'Invalid params' },
|
|
39
|
+
INTERNAL_ERROR: { code: -32603, message: 'Internal error' }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// =========================================================================
|
|
43
|
+
// MCP Tool Definitions
|
|
44
|
+
// =========================================================================
|
|
45
|
+
|
|
46
|
+
const MCP_TOOLS = [
|
|
47
|
+
{
|
|
48
|
+
name: 'scan_text',
|
|
49
|
+
description: 'Scan text for AI security threats including prompt injection, jailbreak attempts, data exfiltration, and 30+ other attack types. Returns threat details with severity and confidence scores.',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
text: { type: 'string', description: 'The text to scan for threats' },
|
|
54
|
+
sensitivity: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Detection sensitivity level (default: medium)' },
|
|
55
|
+
source: { type: 'string', description: 'Label describing where the text came from (e.g. "user_input", "api_response")' }
|
|
56
|
+
},
|
|
57
|
+
required: ['text']
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'scan_input',
|
|
62
|
+
description: 'Scan user input before an AI agent processes it. Applies blocking logic when threats meet the configured threshold.',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
text: { type: 'string', description: 'The user input text to scan' }
|
|
67
|
+
},
|
|
68
|
+
required: ['text']
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'scan_output',
|
|
73
|
+
description: 'Scan agent output before returning it to the user. Detects compromised agent responses, leaked system prompts, and policy violations.',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: {
|
|
77
|
+
text: { type: 'string', description: 'The agent output text to scan' }
|
|
78
|
+
},
|
|
79
|
+
required: ['text']
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'scan_tool_call',
|
|
84
|
+
description: 'Validate a tool call before execution. Checks if the tool is dangerous and scans arguments for injection attacks and sensitive file access.',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
toolName: { type: 'string', description: 'Name of the tool being called' },
|
|
89
|
+
args: { type: 'object', description: 'The tool call arguments to validate' }
|
|
90
|
+
},
|
|
91
|
+
required: ['toolName']
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'redact_pii',
|
|
96
|
+
description: 'Detect and redact personally identifiable information (PII) from text. Covers email, phone, SSN, credit card, IP address, and more.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
text: { type: 'string', description: 'The text to redact PII from' }
|
|
101
|
+
},
|
|
102
|
+
required: ['text']
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'check_canary',
|
|
107
|
+
description: 'Check text for canary token leaks. Canary tokens are tripwire strings embedded in agent context; their presence in output indicates compromise.',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
text: { type: 'string', description: 'The text to check for canary token leaks' },
|
|
112
|
+
tokens: {
|
|
113
|
+
type: 'array',
|
|
114
|
+
items: { type: 'string' },
|
|
115
|
+
description: 'List of canary token strings to check for'
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
required: ['text', 'tokens']
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'shield_score',
|
|
123
|
+
description: 'Calculate and return the current Agent Shield security score (0-100) with category breakdown, grade, and recommendations.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {},
|
|
127
|
+
required: []
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'get_threats',
|
|
132
|
+
description: 'Search the threat encyclopedia for information about AI security threats. Returns descriptions, examples, mitigations, and references.',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
query: { type: 'string', description: 'Search query — a threat name, ID (e.g. "T001"), category, or keyword' }
|
|
137
|
+
},
|
|
138
|
+
required: ['query']
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// =========================================================================
|
|
144
|
+
// MCPToolHandler
|
|
145
|
+
// =========================================================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Wraps Agent Shield components to handle MCP tool calls.
|
|
149
|
+
* Each method corresponds to one of the exposed MCP tools.
|
|
150
|
+
*/
|
|
151
|
+
class MCPToolHandler {
|
|
152
|
+
/**
|
|
153
|
+
* @param {object} [options] - Shield configuration options.
|
|
154
|
+
*/
|
|
155
|
+
constructor(options = {}) {
|
|
156
|
+
this.shield = new AgentShield(options);
|
|
157
|
+
this.piiRedactor = new PIIRedactor(options.pii || {});
|
|
158
|
+
this.canaryTokens = new CanaryTokens(options.canary || {});
|
|
159
|
+
this.scoreCalculator = new ShieldScoreCalculator({
|
|
160
|
+
sensitivity: options.sensitivity || 'high'
|
|
161
|
+
});
|
|
162
|
+
this.encyclopedia = new ThreatEncyclopedia();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Scan text for AI security threats.
|
|
167
|
+
* @param {object} params - { text, sensitivity?, source? }
|
|
168
|
+
* @returns {object} Structured scan result.
|
|
169
|
+
*/
|
|
170
|
+
scanText(params) {
|
|
171
|
+
if (!params || !params.text) {
|
|
172
|
+
throw new Error('Missing required parameter: text');
|
|
173
|
+
}
|
|
174
|
+
const result = this.shield.scan(params.text, {
|
|
175
|
+
sensitivity: params.sensitivity,
|
|
176
|
+
source: params.source || 'mcp_scan_text'
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
status: result.status,
|
|
180
|
+
threatCount: result.threats.length,
|
|
181
|
+
threats: result.threats,
|
|
182
|
+
timestamp: result.timestamp
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Scan user input before agent processing.
|
|
188
|
+
* @param {object} params - { text }
|
|
189
|
+
* @returns {object} Scan result with blocked field.
|
|
190
|
+
*/
|
|
191
|
+
scanInput(params) {
|
|
192
|
+
if (!params || !params.text) {
|
|
193
|
+
throw new Error('Missing required parameter: text');
|
|
194
|
+
}
|
|
195
|
+
const result = this.shield.scanInput(params.text, {
|
|
196
|
+
source: 'mcp_scan_input'
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
status: result.status,
|
|
200
|
+
blocked: result.blocked,
|
|
201
|
+
threatCount: result.threats.length,
|
|
202
|
+
threats: result.threats,
|
|
203
|
+
timestamp: result.timestamp
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Scan agent output before returning to user.
|
|
209
|
+
* @param {object} params - { text }
|
|
210
|
+
* @returns {object} Scan result with blocked field.
|
|
211
|
+
*/
|
|
212
|
+
scanOutput(params) {
|
|
213
|
+
if (!params || !params.text) {
|
|
214
|
+
throw new Error('Missing required parameter: text');
|
|
215
|
+
}
|
|
216
|
+
const result = this.shield.scanOutput(params.text, {
|
|
217
|
+
source: 'mcp_scan_output'
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
status: result.status,
|
|
221
|
+
blocked: result.blocked,
|
|
222
|
+
threatCount: result.threats.length,
|
|
223
|
+
threats: result.threats,
|
|
224
|
+
timestamp: result.timestamp
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Validate a tool call before execution.
|
|
230
|
+
* @param {object} params - { toolName, args? }
|
|
231
|
+
* @returns {object} Scan result with blocked, warnings, and isDangerousTool fields.
|
|
232
|
+
*/
|
|
233
|
+
scanToolCall(params) {
|
|
234
|
+
if (!params || !params.toolName) {
|
|
235
|
+
throw new Error('Missing required parameter: toolName');
|
|
236
|
+
}
|
|
237
|
+
const result = this.shield.scanToolCall(
|
|
238
|
+
params.toolName,
|
|
239
|
+
params.args || {},
|
|
240
|
+
{ source: 'mcp_scan_tool_call' }
|
|
241
|
+
);
|
|
242
|
+
return {
|
|
243
|
+
status: result.status,
|
|
244
|
+
toolName: result.toolName,
|
|
245
|
+
blocked: result.blocked,
|
|
246
|
+
isDangerousTool: result.isDangerousTool,
|
|
247
|
+
warnings: result.warnings,
|
|
248
|
+
threatCount: result.threats.length,
|
|
249
|
+
threats: result.threats,
|
|
250
|
+
timestamp: result.timestamp
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Redact PII from text.
|
|
256
|
+
* @param {object} params - { text }
|
|
257
|
+
* @returns {object} { redacted, findings, count }
|
|
258
|
+
*/
|
|
259
|
+
redactPII(params) {
|
|
260
|
+
if (!params || !params.text) {
|
|
261
|
+
throw new Error('Missing required parameter: text');
|
|
262
|
+
}
|
|
263
|
+
return this.piiRedactor.redact(params.text);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Check text for canary token leaks.
|
|
268
|
+
* @param {object} params - { text, tokens }
|
|
269
|
+
* @returns {object} { leaked, leaks }
|
|
270
|
+
*/
|
|
271
|
+
checkCanary(params) {
|
|
272
|
+
if (!params || !params.text) {
|
|
273
|
+
throw new Error('Missing required parameter: text');
|
|
274
|
+
}
|
|
275
|
+
if (!params.tokens || !Array.isArray(params.tokens) || params.tokens.length === 0) {
|
|
276
|
+
throw new Error('Missing required parameter: tokens (array of canary token strings)');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Register the provided tokens, then check for leaks
|
|
280
|
+
const registeredTokens = [];
|
|
281
|
+
for (const token of params.tokens) {
|
|
282
|
+
// Create a temporary entry for each token to check
|
|
283
|
+
const id = `check_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
284
|
+
this.canaryTokens.tokens.set(id, {
|
|
285
|
+
token,
|
|
286
|
+
label: 'mcp_check',
|
|
287
|
+
createdAt: Date.now(),
|
|
288
|
+
triggeredCount: 0
|
|
289
|
+
});
|
|
290
|
+
registeredTokens.push(id);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const result = this.canaryTokens.check(params.text, 'mcp_check_canary');
|
|
294
|
+
|
|
295
|
+
// Clean up temporary tokens
|
|
296
|
+
for (const id of registeredTokens) {
|
|
297
|
+
this.canaryTokens.tokens.delete(id);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
leaked: result.leaked,
|
|
302
|
+
leaks: result.leaks
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Calculate and return the shield security score.
|
|
308
|
+
* @returns {object} Score breakdown with grade, categories, and recommendations.
|
|
309
|
+
*/
|
|
310
|
+
getScore() {
|
|
311
|
+
const result = this.scoreCalculator.calculate();
|
|
312
|
+
return {
|
|
313
|
+
score: result.score,
|
|
314
|
+
grade: result.grade,
|
|
315
|
+
label: result.label,
|
|
316
|
+
categories: result.categories,
|
|
317
|
+
recommendations: result.recommendations,
|
|
318
|
+
elapsed: result.elapsed
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Search the threat encyclopedia.
|
|
324
|
+
* @param {object} params - { query }
|
|
325
|
+
* @returns {object} Matching threat entries.
|
|
326
|
+
*/
|
|
327
|
+
getThreats(params) {
|
|
328
|
+
if (!params || !params.query) {
|
|
329
|
+
throw new Error('Missing required parameter: query');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const query = params.query.trim();
|
|
333
|
+
|
|
334
|
+
// Try direct lookup by ID or key first
|
|
335
|
+
const direct = this.encyclopedia.get(query);
|
|
336
|
+
if (direct) {
|
|
337
|
+
return { results: [direct], count: 1 };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Fall back to search
|
|
341
|
+
const results = this.encyclopedia.search(query);
|
|
342
|
+
return { results, count: results.length };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// =========================================================================
|
|
347
|
+
// MCPServer
|
|
348
|
+
// =========================================================================
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* MCP Server for Agent Shield.
|
|
352
|
+
* Communicates via JSON-RPC 2.0 over stdin/stdout.
|
|
353
|
+
*/
|
|
354
|
+
class MCPServer {
|
|
355
|
+
/**
|
|
356
|
+
* Creates a new MCPServer instance.
|
|
357
|
+
* @param {object} [options] - Shield configuration options passed to AgentShield.
|
|
358
|
+
*/
|
|
359
|
+
constructor(options = {}) {
|
|
360
|
+
this.options = options;
|
|
361
|
+
this.handler = new MCPToolHandler(options);
|
|
362
|
+
this.running = false;
|
|
363
|
+
this._buffer = '';
|
|
364
|
+
this._onData = null;
|
|
365
|
+
|
|
366
|
+
/** @type {object} Server metadata returned in initialize response. */
|
|
367
|
+
this.serverInfo = {
|
|
368
|
+
name: 'agent-shield',
|
|
369
|
+
version: '1.0.0'
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/** @type {object} Server capabilities. */
|
|
373
|
+
this.capabilities = {
|
|
374
|
+
tools: {},
|
|
375
|
+
resources: {}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Starts listening on stdin for JSON-RPC messages.
|
|
381
|
+
* Responses are written to stdout.
|
|
382
|
+
*/
|
|
383
|
+
start() {
|
|
384
|
+
if (this.running) return;
|
|
385
|
+
this.running = true;
|
|
386
|
+
|
|
387
|
+
console.error('[Agent Shield] MCP server starting...');
|
|
388
|
+
|
|
389
|
+
this._onData = (chunk) => {
|
|
390
|
+
this._buffer += chunk.toString();
|
|
391
|
+
if (this._buffer.length > 10 * 1024 * 1024) {
|
|
392
|
+
console.error('[Agent Shield] MCP buffer exceeded 10MB, clearing');
|
|
393
|
+
this._buffer = '';
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
this._processBuffer();
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
process.stdin.setEncoding('utf8');
|
|
400
|
+
process.stdin.on('data', this._onData);
|
|
401
|
+
process.stdin.resume();
|
|
402
|
+
|
|
403
|
+
console.error('[Agent Shield] MCP server ready — listening on stdin');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Stops the server and cleans up stdin listener.
|
|
408
|
+
*/
|
|
409
|
+
stop() {
|
|
410
|
+
if (!this.running) return;
|
|
411
|
+
this.running = false;
|
|
412
|
+
|
|
413
|
+
if (this._onData) {
|
|
414
|
+
process.stdin.removeListener('data', this._onData);
|
|
415
|
+
this._onData = null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
console.error('[Agent Shield] MCP server stopped');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Process the internal buffer, extracting and handling complete JSON-RPC messages.
|
|
423
|
+
* Messages are delimited by newlines.
|
|
424
|
+
* @private
|
|
425
|
+
*/
|
|
426
|
+
_processBuffer() {
|
|
427
|
+
const lines = this._buffer.split('\n');
|
|
428
|
+
// Keep the last incomplete line in the buffer
|
|
429
|
+
this._buffer = lines.pop() || '';
|
|
430
|
+
|
|
431
|
+
for (const line of lines) {
|
|
432
|
+
const trimmed = line.trim();
|
|
433
|
+
if (!trimmed) continue;
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const message = JSON.parse(trimmed);
|
|
437
|
+
const response = this.handleMessage(message);
|
|
438
|
+
if (response) {
|
|
439
|
+
this._send(response);
|
|
440
|
+
}
|
|
441
|
+
} catch (err) {
|
|
442
|
+
// JSON parse error
|
|
443
|
+
this._send({
|
|
444
|
+
jsonrpc: '2.0',
|
|
445
|
+
id: null,
|
|
446
|
+
error: { code: JSON_RPC_ERRORS.PARSE_ERROR.code, message: JSON_RPC_ERRORS.PARSE_ERROR.message }
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Send a JSON-RPC response to stdout.
|
|
454
|
+
* @private
|
|
455
|
+
* @param {object} response - The JSON-RPC response object.
|
|
456
|
+
*/
|
|
457
|
+
_send(response) {
|
|
458
|
+
const json = JSON.stringify(response);
|
|
459
|
+
process.stdout.write(json + '\n');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Routes a JSON-RPC message to the appropriate handler.
|
|
464
|
+
*
|
|
465
|
+
* @param {object} message - Parsed JSON-RPC 2.0 request.
|
|
466
|
+
* @returns {object|null} JSON-RPC response, or null for notifications.
|
|
467
|
+
*/
|
|
468
|
+
handleMessage(message) {
|
|
469
|
+
// Validate JSON-RPC 2.0 structure
|
|
470
|
+
if (!message || message.jsonrpc !== '2.0') {
|
|
471
|
+
return {
|
|
472
|
+
jsonrpc: '2.0',
|
|
473
|
+
id: message && message.id !== null && message.id !== undefined ? message.id : null,
|
|
474
|
+
error: { code: JSON_RPC_ERRORS.INVALID_REQUEST.code, message: JSON_RPC_ERRORS.INVALID_REQUEST.message }
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const { id, method, params } = message;
|
|
479
|
+
|
|
480
|
+
// Notifications (no id) don't get responses
|
|
481
|
+
if ((id === null || id === undefined) && method !== 'initialize') {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
switch (method) {
|
|
487
|
+
case 'initialize':
|
|
488
|
+
return this._handleInitialize(id, params);
|
|
489
|
+
case 'tools/list':
|
|
490
|
+
return this._handleToolsList(id);
|
|
491
|
+
case 'tools/call':
|
|
492
|
+
return this._handleToolsCall(id, params);
|
|
493
|
+
case 'resources/list':
|
|
494
|
+
return this._handleResourcesList(id);
|
|
495
|
+
case 'resources/read':
|
|
496
|
+
return this._handleResourcesRead(id, params);
|
|
497
|
+
default:
|
|
498
|
+
return {
|
|
499
|
+
jsonrpc: '2.0',
|
|
500
|
+
id,
|
|
501
|
+
error: { code: JSON_RPC_ERRORS.METHOD_NOT_FOUND.code, message: `Method not found: ${method}` }
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
} catch (err) {
|
|
505
|
+
console.error(`[Agent Shield] MCP error handling ${method}:`, err.message);
|
|
506
|
+
return {
|
|
507
|
+
jsonrpc: '2.0',
|
|
508
|
+
id,
|
|
509
|
+
error: { code: JSON_RPC_ERRORS.INTERNAL_ERROR.code, message: err.message }
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Handle the MCP initialize request.
|
|
516
|
+
* @private
|
|
517
|
+
* @param {number|string} id - Request ID.
|
|
518
|
+
* @param {object} params - Client capabilities.
|
|
519
|
+
* @returns {object} JSON-RPC response with server info and capabilities.
|
|
520
|
+
*/
|
|
521
|
+
_handleInitialize(id, params) {
|
|
522
|
+
console.error('[Agent Shield] MCP client connected:', params && params.clientInfo ? params.clientInfo.name : 'unknown');
|
|
523
|
+
return {
|
|
524
|
+
jsonrpc: '2.0',
|
|
525
|
+
id,
|
|
526
|
+
result: {
|
|
527
|
+
protocolVersion: '2024-11-05',
|
|
528
|
+
serverInfo: this.serverInfo,
|
|
529
|
+
capabilities: this.capabilities
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Handle tools/list — return all available MCP tools.
|
|
536
|
+
* @private
|
|
537
|
+
* @param {number|string} id - Request ID.
|
|
538
|
+
* @returns {object} JSON-RPC response with tool list.
|
|
539
|
+
*/
|
|
540
|
+
_handleToolsList(id) {
|
|
541
|
+
return {
|
|
542
|
+
jsonrpc: '2.0',
|
|
543
|
+
id,
|
|
544
|
+
result: {
|
|
545
|
+
tools: MCP_TOOLS
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Handle tools/call — execute a tool and return the result.
|
|
552
|
+
* @private
|
|
553
|
+
* @param {number|string} id - Request ID.
|
|
554
|
+
* @param {object} params - { name, arguments }
|
|
555
|
+
* @returns {object} JSON-RPC response with tool result.
|
|
556
|
+
*/
|
|
557
|
+
_handleToolsCall(id, params) {
|
|
558
|
+
if (!params || !params.name) {
|
|
559
|
+
return {
|
|
560
|
+
jsonrpc: '2.0',
|
|
561
|
+
id,
|
|
562
|
+
error: { code: JSON_RPC_ERRORS.INVALID_PARAMS.code, message: 'Missing required parameter: name' }
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const toolName = params.name;
|
|
567
|
+
const toolArgs = params.arguments || {};
|
|
568
|
+
|
|
569
|
+
let result;
|
|
570
|
+
let isError = false;
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
switch (toolName) {
|
|
574
|
+
case 'scan_text':
|
|
575
|
+
result = this.handler.scanText(toolArgs);
|
|
576
|
+
break;
|
|
577
|
+
case 'scan_input':
|
|
578
|
+
result = this.handler.scanInput(toolArgs);
|
|
579
|
+
break;
|
|
580
|
+
case 'scan_output':
|
|
581
|
+
result = this.handler.scanOutput(toolArgs);
|
|
582
|
+
break;
|
|
583
|
+
case 'scan_tool_call':
|
|
584
|
+
result = this.handler.scanToolCall(toolArgs);
|
|
585
|
+
break;
|
|
586
|
+
case 'redact_pii':
|
|
587
|
+
result = this.handler.redactPII(toolArgs);
|
|
588
|
+
break;
|
|
589
|
+
case 'check_canary':
|
|
590
|
+
result = this.handler.checkCanary(toolArgs);
|
|
591
|
+
break;
|
|
592
|
+
case 'shield_score':
|
|
593
|
+
result = this.handler.getScore();
|
|
594
|
+
break;
|
|
595
|
+
case 'get_threats':
|
|
596
|
+
result = this.handler.getThreats(toolArgs);
|
|
597
|
+
break;
|
|
598
|
+
default:
|
|
599
|
+
return {
|
|
600
|
+
jsonrpc: '2.0',
|
|
601
|
+
id,
|
|
602
|
+
error: { code: JSON_RPC_ERRORS.METHOD_NOT_FOUND.code, message: `Unknown tool: ${toolName}` }
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
} catch (err) {
|
|
606
|
+
result = { error: err.message };
|
|
607
|
+
isError = true;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
jsonrpc: '2.0',
|
|
612
|
+
id,
|
|
613
|
+
result: {
|
|
614
|
+
content: [
|
|
615
|
+
{
|
|
616
|
+
type: 'text',
|
|
617
|
+
text: JSON.stringify(result, null, 2)
|
|
618
|
+
}
|
|
619
|
+
],
|
|
620
|
+
isError
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Handle resources/list — expose threat encyclopedia entries as resources.
|
|
627
|
+
* @private
|
|
628
|
+
* @param {number|string} id - Request ID.
|
|
629
|
+
* @returns {object} JSON-RPC response with resource list.
|
|
630
|
+
*/
|
|
631
|
+
_handleResourcesList(id) {
|
|
632
|
+
const resources = [];
|
|
633
|
+
|
|
634
|
+
for (const [key, threat] of Object.entries(THREAT_ENCYCLOPEDIA)) {
|
|
635
|
+
resources.push({
|
|
636
|
+
uri: `threat://${threat.id}`,
|
|
637
|
+
name: threat.name,
|
|
638
|
+
description: threat.summary,
|
|
639
|
+
mimeType: 'application/json'
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
jsonrpc: '2.0',
|
|
645
|
+
id,
|
|
646
|
+
result: { resources }
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Handle resources/read — read a specific threat resource by URI.
|
|
652
|
+
* @private
|
|
653
|
+
* @param {number|string} id - Request ID.
|
|
654
|
+
* @param {object} params - { uri }
|
|
655
|
+
* @returns {object} JSON-RPC response with resource contents.
|
|
656
|
+
*/
|
|
657
|
+
_handleResourcesRead(id, params) {
|
|
658
|
+
if (!params || !params.uri) {
|
|
659
|
+
return {
|
|
660
|
+
jsonrpc: '2.0',
|
|
661
|
+
id,
|
|
662
|
+
error: { code: JSON_RPC_ERRORS.INVALID_PARAMS.code, message: 'Missing required parameter: uri' }
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const uri = params.uri;
|
|
667
|
+
|
|
668
|
+
// Extract threat ID from URI (e.g. "threat://T001" -> "T001")
|
|
669
|
+
const match = uri.match(/^threat:\/\/(.+)$/);
|
|
670
|
+
if (!match) {
|
|
671
|
+
return {
|
|
672
|
+
jsonrpc: '2.0',
|
|
673
|
+
id,
|
|
674
|
+
error: { code: JSON_RPC_ERRORS.INVALID_PARAMS.code, message: `Invalid resource URI: ${uri}` }
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const threatId = match[1];
|
|
679
|
+
const threat = this.handler.encyclopedia.get(threatId);
|
|
680
|
+
|
|
681
|
+
if (!threat) {
|
|
682
|
+
return {
|
|
683
|
+
jsonrpc: '2.0',
|
|
684
|
+
id,
|
|
685
|
+
error: { code: JSON_RPC_ERRORS.INVALID_PARAMS.code, message: `Threat not found: ${threatId}` }
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
jsonrpc: '2.0',
|
|
691
|
+
id,
|
|
692
|
+
result: {
|
|
693
|
+
contents: [
|
|
694
|
+
{
|
|
695
|
+
uri,
|
|
696
|
+
mimeType: 'application/json',
|
|
697
|
+
text: JSON.stringify(threat, null, 2)
|
|
698
|
+
}
|
|
699
|
+
]
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// =========================================================================
|
|
706
|
+
// Standalone Entry Point
|
|
707
|
+
// =========================================================================
|
|
708
|
+
|
|
709
|
+
if (require.main === module) {
|
|
710
|
+
let config = {};
|
|
711
|
+
|
|
712
|
+
// Parse --config flag
|
|
713
|
+
const configIndex = process.argv.indexOf('--config');
|
|
714
|
+
if (configIndex !== -1 && process.argv[configIndex + 1]) {
|
|
715
|
+
const configPath = path.resolve(process.argv[configIndex + 1]);
|
|
716
|
+
try {
|
|
717
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
718
|
+
config = JSON.parse(raw);
|
|
719
|
+
console.error(`[Agent Shield] Loaded config from ${configPath}`);
|
|
720
|
+
} catch (err) {
|
|
721
|
+
console.error(`[Agent Shield] Failed to load config from ${configPath}: ${err.message}`);
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const server = new MCPServer(config);
|
|
727
|
+
server.start();
|
|
728
|
+
|
|
729
|
+
// Graceful shutdown
|
|
730
|
+
process.on('SIGINT', () => {
|
|
731
|
+
server.stop();
|
|
732
|
+
process.exit(0);
|
|
733
|
+
});
|
|
734
|
+
process.on('SIGTERM', () => {
|
|
735
|
+
server.stop();
|
|
736
|
+
process.exit(0);
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
module.exports = { MCPServer, MCPToolHandler };
|