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.
Files changed (84) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/README.md +975 -0
  4. package/bin/agent-shield.js +680 -0
  5. package/package.json +118 -0
  6. package/src/adaptive.js +330 -0
  7. package/src/agent-protocol.js +998 -0
  8. package/src/alert-tuning.js +480 -0
  9. package/src/allowlist.js +603 -0
  10. package/src/audit-immutable.js +914 -0
  11. package/src/audit-streaming.js +469 -0
  12. package/src/badges.js +196 -0
  13. package/src/behavior-profiling.js +289 -0
  14. package/src/benchmark-harness.js +804 -0
  15. package/src/canary.js +271 -0
  16. package/src/certification.js +563 -0
  17. package/src/circuit-breaker.js +321 -0
  18. package/src/compliance.js +617 -0
  19. package/src/confidence-tuning.js +324 -0
  20. package/src/confused-deputy.js +624 -0
  21. package/src/context-scoring.js +360 -0
  22. package/src/conversation.js +494 -0
  23. package/src/cost-optimizer.js +1024 -0
  24. package/src/ctf.js +462 -0
  25. package/src/detector-core.js +1999 -0
  26. package/src/distributed.js +359 -0
  27. package/src/document-scanner.js +795 -0
  28. package/src/embedding.js +307 -0
  29. package/src/encoding.js +429 -0
  30. package/src/enterprise.js +405 -0
  31. package/src/errors.js +100 -0
  32. package/src/eu-ai-act.js +523 -0
  33. package/src/fuzzer.js +764 -0
  34. package/src/honeypot.js +328 -0
  35. package/src/i18n-patterns.js +523 -0
  36. package/src/index.js +430 -0
  37. package/src/integrations.js +528 -0
  38. package/src/llm-redteam.js +670 -0
  39. package/src/main.js +741 -0
  40. package/src/main.mjs +38 -0
  41. package/src/mcp-bridge.js +542 -0
  42. package/src/mcp-certification.js +846 -0
  43. package/src/mcp-sdk-integration.js +355 -0
  44. package/src/mcp-security-runtime.js +741 -0
  45. package/src/mcp-server.js +740 -0
  46. package/src/middleware.js +208 -0
  47. package/src/model-finetuning.js +884 -0
  48. package/src/model-fingerprint.js +1042 -0
  49. package/src/multi-agent-trust.js +453 -0
  50. package/src/multi-agent.js +404 -0
  51. package/src/multimodal.js +296 -0
  52. package/src/nist-mapping.js +505 -0
  53. package/src/observability.js +330 -0
  54. package/src/openclaw.js +450 -0
  55. package/src/otel.js +544 -0
  56. package/src/owasp-2025.js +483 -0
  57. package/src/pii.js +390 -0
  58. package/src/plugin-marketplace.js +628 -0
  59. package/src/plugin-system.js +349 -0
  60. package/src/policy-dsl.js +775 -0
  61. package/src/policy-extended.js +635 -0
  62. package/src/policy.js +443 -0
  63. package/src/presets.js +409 -0
  64. package/src/production.js +557 -0
  65. package/src/prompt-leakage.js +321 -0
  66. package/src/rag-vulnerability.js +579 -0
  67. package/src/redteam.js +475 -0
  68. package/src/response-handler.js +429 -0
  69. package/src/scanners.js +357 -0
  70. package/src/self-healing.js +363 -0
  71. package/src/semantic.js +339 -0
  72. package/src/shield-score.js +250 -0
  73. package/src/sso-saml.js +897 -0
  74. package/src/stream-scanner.js +806 -0
  75. package/src/testing.js +505 -0
  76. package/src/threat-encyclopedia.js +629 -0
  77. package/src/threat-intel-network.js +1017 -0
  78. package/src/token-analysis.js +467 -0
  79. package/src/tool-guard.js +412 -0
  80. package/src/tool-output-validator.js +354 -0
  81. package/src/utils.js +83 -0
  82. package/src/watermark.js +235 -0
  83. package/src/worker-scanner.js +601 -0
  84. 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 };