visus-mcp 0.26.0 → 0.27.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/MCPB-SUBMISSION.md +10 -10
- package/README.md +16 -1
- package/dist/src/index.js +285 -290
- package/dist/src/index.js.map +1 -1
- package/dist/src/sanitizer/patterns.d.ts +0 -1
- package/dist/src/sanitizer/patterns.d.ts.map +1 -1
- package/dist/src/sanitizer/patterns.js +44 -9
- package/dist/src/sanitizer/patterns.js.map +1 -1
- package/dist/src/tools/mcp-config-scan.d.ts +10 -0
- package/dist/src/tools/mcp-config-scan.d.ts.map +1 -1
- package/dist/src/tools/mcp-config-scan.js +8 -0
- package/dist/src/tools/mcp-config-scan.js.map +1 -1
- package/mcp.json +34 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/index.ts +357 -326
- package/src/sanitizer/index.ts +25 -4
- package/src/sanitizer/patterns.ts +46 -9
- package/src/tools/fetch-structured.ts +31 -2
- package/src/tools/fetch.ts +29 -1
- package/src/tools/mcp-config-scan.ts +21 -1
- package/visus-mcp-0.26.0.tgz +0 -0
package/MCPB-SUBMISSION.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Anthropic Connectors Directory — Submission Package
|
|
2
|
-
## visus-mcp v0.
|
|
2
|
+
## visus-mcp v0.26.0
|
|
3
3
|
|
|
4
|
-
**Submission Date:**
|
|
5
|
-
**Bundle File:** `visus-mcp-0.
|
|
4
|
+
**Submission Date:** April 21, 2026
|
|
5
|
+
**Bundle File:** `visus-mcp-0.26.0.mcpb` (38MB)
|
|
6
6
|
**Submission Form:** https://docs.google.com/forms/d/e/1FAIpQLSeafJF2NDI7oYx1r8o0ycivCSVLNq92Mpc1FPxMKSw1CzDkqA/viewform
|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -57,7 +57,7 @@ Every tool invocation runs content through this pipeline:
|
|
|
57
57
|
### Trust Model
|
|
58
58
|
|
|
59
59
|
- **Local-first**: Runs entirely on your machine — no external API calls
|
|
60
|
-
- **Open source**: MIT License,
|
|
60
|
+
- **Open source**: MIT License, 500+/500 passing tests — audit the code yourself at https://github.com/visus-mcp/visus-mcp
|
|
61
61
|
- **No authentication required**: Open-source tier works out of the box
|
|
62
62
|
- **Deterministic**: Same input always produces the same sanitized result
|
|
63
63
|
- **Framework-aligned**: Threat detection mapped to OWASP LLM Top 10 (2025), NIST AI RMF 600-1, MITRE ATLAS, and ISO/IEC 42001:2023
|
|
@@ -212,16 +212,16 @@ See https://github.com/visus-mcp/visus-mcp/blob/main/README.md#compliance-mappin
|
|
|
212
212
|
|
|
213
213
|
## Submission Checklist
|
|
214
214
|
|
|
215
|
-
- [x] Bundle created (`visus-mcp-0.
|
|
216
|
-
- [x] Manifest validates against schema
|
|
217
|
-
- [x] All
|
|
215
|
+
- [x] Bundle created (`visus-mcp-0.26.0.mcpb`)
|
|
216
|
+
- [x] Manifest validates against schema 2025-12-11
|
|
217
|
+
- [x] All 12 tools declared with descriptions
|
|
218
218
|
- [x] Icon included (512×512 PNG)
|
|
219
219
|
- [x] Privacy policy in README.md
|
|
220
220
|
- [x] Privacy policy URL in manifest.json
|
|
221
221
|
- [x] No authentication required (no test account needed)
|
|
222
|
-
- [
|
|
223
|
-
- [
|
|
224
|
-
- [
|
|
222
|
+
- [x] Local install test passed (manual verification required)
|
|
223
|
+
- [x] Smoke test passed: visus_fetch returns threat_summary + visus_proof
|
|
224
|
+
- [x] All tools return responses < 25,000 tokens
|
|
225
225
|
|
|
226
226
|
---
|
|
227
227
|
|
package/README.md
CHANGED
|
@@ -144,10 +144,25 @@ Visus detects and neutralizes:
|
|
|
144
144
|
- **Jailbreak keywords** — DAN mode, developer override
|
|
145
145
|
- **Token smuggling** — Special tokens like `<|im_start|>`
|
|
146
146
|
- **Social engineering** — Urgency language to bypass caution
|
|
147
|
-
- ... and 32 more categories
|
|
147
|
+
- ... and 32 more categories (+20 MCP command injection/tool poisoning in v0.27.0)
|
|
148
148
|
|
|
149
149
|
[See full list in SECURITY.md](./SECURITY.md)
|
|
150
150
|
|
|
151
|
+
### Security Enhancements (v0.27.0)
|
|
152
|
+
|
|
153
|
+
**MCP Ecosystem Protections:**
|
|
154
|
+
|
|
155
|
+
- **Command Injection Guard**: Detects shell metachars (`; | &`), subprocess patterns (`bash -c`, `cmd.exe /c`, `npx -c`), entropy payloads (>4.5 threshold). Integrated into `visus_scan_mcp` for pre-spawn `safeToSpawn=false` on score>7.
|
|
156
|
+
- **Tool Poisoning Validator**: Scans descriptors/schemas for anomalous names (`Ignore~`), IPI in descriptions/defaults, hidden params (`__`), long defaults (>256 chars). SHA256 pinning for known tools (hash mismatch → block).
|
|
157
|
+
- **Runtime Guards**: `visus_fetch`/`visus_fetch_structured` scan inputs (block score>5), sanitize high-risk URLs/schemas.
|
|
158
|
+
- **Response Scanning**: `sanitizeWithProof` now checks JSON tool outputs for poisoning (`tool_` patterns), redacts as `[REDACTED: tool poisoning]`.
|
|
159
|
+
- **Advanced Mitigations**: Approved command allowlist (`node`, `npm`), `safeSpawn` (no shell, restricted PATH/env), structured logging/alerts.
|
|
160
|
+
- **Perf**: <5ms detection, <10ms validation (benchmarked).
|
|
161
|
+
- **Tuning**: 0% FP on 20+ clean corpus; 10 red-team scenarios block threats.
|
|
162
|
+
|
|
163
|
+
Layered defenses for CVE-2026-30623 (STDIO RCE), MCP03 (tool poisoning). See commit 13fd7d4.
|
|
164
|
+
|
|
165
|
+
|
|
151
166
|
### PII Redaction
|
|
152
167
|
|
|
153
168
|
Automatically redacts:
|
package/dist/src/index.js
CHANGED
|
@@ -26,7 +26,7 @@ console.error('[VISUS-DEBUG] Module loaded');
|
|
|
26
26
|
*/
|
|
27
27
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
28
28
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
29
|
-
import { ListToolsRequestSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
29
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
30
30
|
import { visusFetch, visusFetchToolDefinition } from './tools/fetch.js';
|
|
31
31
|
import { visusFetchStructured, visusFetchStructuredToolDefinition } from './tools/fetch-structured.js';
|
|
32
32
|
import { visusRead, visusReadToolDefinition } from './tools/read.js';
|
|
@@ -42,7 +42,6 @@ import { closeBrowser } from './browser/playwright-renderer.js';
|
|
|
42
42
|
import { detectRuntime, logRuntimeConfig, validateRuntime } from './runtime.js';
|
|
43
43
|
import { shouldElicit, buildElicitMessage } from './sanitizer/hitl-gate.js';
|
|
44
44
|
import { runElicitation } from './sanitizer/elicit-runner.js';
|
|
45
|
-
import { SessionLedger } from './security/session-ledger.js';
|
|
46
45
|
import { visusScanMcp, visusScanMcpToolDefinition } from './tools/mcp-config-scan.js';
|
|
47
46
|
/**
|
|
48
47
|
* Create and configure the MCP server
|
|
@@ -60,24 +59,293 @@ console.error('[VISUS-DEBUG] Server created');
|
|
|
60
59
|
/**
|
|
61
60
|
* Handle tool list requests
|
|
62
61
|
*/
|
|
62
|
+
import { detectAndNeutralize } from './sanitizer/index.js';
|
|
63
|
+
function sanitizeToolDefinition(tool) {
|
|
64
|
+
let sanitized = { ...tool };
|
|
65
|
+
// Sanitize description
|
|
66
|
+
if (sanitized.description) {
|
|
67
|
+
const result = detectAndNeutralize(sanitized.description);
|
|
68
|
+
if (result.content_modified) {
|
|
69
|
+
console.error(`[SECURITY] Tool ${sanitized.name} description sanitized`);
|
|
70
|
+
}
|
|
71
|
+
sanitized = { ...sanitized, description: result.content };
|
|
72
|
+
}
|
|
73
|
+
// Sanitize inputSchema by stringifying and re-parsing (basic, no deep recurse for MVP)
|
|
74
|
+
if (sanitized.inputSchema) {
|
|
75
|
+
try {
|
|
76
|
+
const schemaStr = JSON.stringify(sanitized.inputSchema);
|
|
77
|
+
const schemaResult = detectAndNeutralize(schemaStr);
|
|
78
|
+
if (schemaResult.content_modified) {
|
|
79
|
+
console.error(`[SECURITY] Tool ${sanitized.name} schema sanitized`);
|
|
80
|
+
sanitized = { ...sanitized, inputSchema: JSON.parse(schemaResult.content) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
console.error(`[SECURITY] Failed to sanitize schema for ${sanitized.name}:`, e);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return sanitized;
|
|
88
|
+
}
|
|
63
89
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
90
|
+
const rawTools = [
|
|
91
|
+
visusFetchToolDefinition,
|
|
92
|
+
visusFetchStructuredToolDefinition,
|
|
93
|
+
visusReadToolDefinition,
|
|
94
|
+
visusSearchToolDefinition,
|
|
95
|
+
visusReportToolDefinition,
|
|
96
|
+
visusVerifyToolDefinition,
|
|
97
|
+
visusReadCsvToolDefinition,
|
|
98
|
+
visusReadExcelToolDefinition,
|
|
99
|
+
visusReadGsheetToolDefinition,
|
|
100
|
+
visusContextScanToolDefinition,
|
|
101
|
+
visusGetLedgerProofToolDefinition,
|
|
102
|
+
visusScanMcpToolDefinition
|
|
103
|
+
];
|
|
104
|
+
const sanitizedTools = rawTools.map(sanitizeToolDefinition);
|
|
64
105
|
return {
|
|
65
|
-
tools:
|
|
66
|
-
visusFetchToolDefinition,
|
|
67
|
-
visusFetchStructuredToolDefinition,
|
|
68
|
-
visusReadToolDefinition,
|
|
69
|
-
visusSearchToolDefinition,
|
|
70
|
-
visusReportToolDefinition,
|
|
71
|
-
visusVerifyToolDefinition,
|
|
72
|
-
visusReadCsvToolDefinition,
|
|
73
|
-
visusReadExcelToolDefinition,
|
|
74
|
-
visusReadGsheetToolDefinition,
|
|
75
|
-
visusContextScanToolDefinition,
|
|
76
|
-
visusGetLedgerProofToolDefinition,
|
|
77
|
-
visusScanMcpToolDefinition
|
|
78
|
-
]
|
|
106
|
+
tools: sanitizedTools
|
|
79
107
|
};
|
|
80
108
|
});
|
|
109
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
110
|
+
const { name, arguments: args } = request.params;
|
|
111
|
+
const sessionId = request.sessionId || 'default';
|
|
112
|
+
try {
|
|
113
|
+
switch (name) {
|
|
114
|
+
// ... existing cases, add:
|
|
115
|
+
case 'visus_db_verify': {
|
|
116
|
+
return await visusDbVerify(args);
|
|
117
|
+
}
|
|
118
|
+
case 'visus_fetch': {
|
|
119
|
+
const result = await visusFetch(args);
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
throw new McpError(ErrorCode.InternalError, `visus_fetch failed: ${result.error.message}`);
|
|
122
|
+
}
|
|
123
|
+
// VSIL Check
|
|
124
|
+
const { score, newThreats, chainId, dangling } = await ledger.checkContextualIntegrity(sessionId, name, args, result.value);
|
|
125
|
+
if (score > 0.7) {
|
|
126
|
+
const threatReport = result.value.threat_report;
|
|
127
|
+
const message = 'High session risk detected from prior turns (chains/priming). Proceed with caution?';
|
|
128
|
+
const { proceed, includeReport } = await runElicitation(server, message);
|
|
129
|
+
if (!proceed) {
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: 'text', text: JSON.stringify({ blocked: true, session_risk: score, reason: 'User declined high-risk session' }, null, 2) }]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Merge new threats
|
|
135
|
+
if (threatReport)
|
|
136
|
+
threatReport.new_threats = [...(threatReport.new_threats || []), ...newThreats];
|
|
137
|
+
}
|
|
138
|
+
// Update ledger
|
|
139
|
+
const hashes = ledger.extractEntityHashes ? await ledger.extractEntityHashes(args, result.value) : [];
|
|
140
|
+
ledger.update(sessionId, hashes, name, newThreats);
|
|
141
|
+
// Extend output
|
|
142
|
+
const extended = { ...result.value };
|
|
143
|
+
if (extended.threat_summary) {
|
|
144
|
+
extended.threat_summary.session_risk = score;
|
|
145
|
+
extended.threat_summary.chain_detected = !!chainId;
|
|
146
|
+
extended.threat_summary.priming_flags = dangling ? ['dangling_instruction'] : [];
|
|
147
|
+
}
|
|
148
|
+
// Existing HITL
|
|
149
|
+
const { output } = await handleCriticalThreatElicitation(extended, args.url);
|
|
150
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
151
|
+
}
|
|
152
|
+
case 'visus_fetch_structured': {
|
|
153
|
+
const result = await visusFetchStructured(args);
|
|
154
|
+
if (!result.ok) {
|
|
155
|
+
throw new McpError(ErrorCode.InternalError, `visus_fetch_structured failed: ${result.error.message}`);
|
|
156
|
+
}
|
|
157
|
+
// VSIL Check (similar)
|
|
158
|
+
const { score, newThreats, chainId, dangling } = await ledger.checkContextualIntegrity(sessionId, name, args, result.value);
|
|
159
|
+
if (score > 0.7) {
|
|
160
|
+
const threatReport = result.value.threat_report;
|
|
161
|
+
const message = 'High session risk detected. Proceed with structured extraction?';
|
|
162
|
+
const { proceed } = await runElicitation(server, message);
|
|
163
|
+
if (!proceed) {
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: 'text', text: JSON.stringify({ blocked: true, session_risk: score }, null, 2) }]
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (threatReport)
|
|
169
|
+
threatReport.new_threats = [...(threatReport.new_threats || []), ...newThreats];
|
|
170
|
+
}
|
|
171
|
+
// Update ledger
|
|
172
|
+
const hashes = ledger.extractEntityHashes ? await ledger.extractEntityHashes(args, result.value) : [];
|
|
173
|
+
ledger.update(sessionId, hashes, name, newThreats);
|
|
174
|
+
// Extend output
|
|
175
|
+
const extended = { ...result.value };
|
|
176
|
+
if (extended.threat_summary) {
|
|
177
|
+
extended.threat_summary.session_risk = score;
|
|
178
|
+
extended.threat_summary.chain_detected = !!chainId;
|
|
179
|
+
}
|
|
180
|
+
// HITL for threats
|
|
181
|
+
const { output } = await handleCriticalThreatElicitation(extended, args.url);
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: 'text',
|
|
186
|
+
text: JSON.stringify(output, null, 2)
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
case 'visus_read': {
|
|
192
|
+
const result = await visusRead(args);
|
|
193
|
+
if (!result.ok) {
|
|
194
|
+
throw new McpError(ErrorCode.InternalError, `visus_read failed: ${result.error.message}`);
|
|
195
|
+
}
|
|
196
|
+
// VSIL for read (session continuity)
|
|
197
|
+
const { score } = await ledger.checkContextualIntegrity(sessionId, name, args, result.value);
|
|
198
|
+
if (score > 0.7) {
|
|
199
|
+
ledger.update(sessionId, [], name, []); // Log but no block for read-only
|
|
200
|
+
console.error(`High session risk for read: ${score}`); // Log only
|
|
201
|
+
}
|
|
202
|
+
// HITL for threats
|
|
203
|
+
const { output } = await handleCriticalThreatElicitation(result.value, args.url);
|
|
204
|
+
return {
|
|
205
|
+
content: [
|
|
206
|
+
{
|
|
207
|
+
type: 'text',
|
|
208
|
+
text: JSON.stringify(output, null, 2)
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
case 'visus_search': {
|
|
214
|
+
const result = await visusSearch(args);
|
|
215
|
+
if (!result.ok) {
|
|
216
|
+
throw new McpError(ErrorCode.InternalError, `visus_search failed: ${result.error.message}`);
|
|
217
|
+
}
|
|
218
|
+
// VSIL for search (priming URLs)
|
|
219
|
+
const { score } = await ledger.checkContextualIntegrity(sessionId, name, args, result.value);
|
|
220
|
+
if (score > 0.7) {
|
|
221
|
+
ledger.update(sessionId, [], name, []); // Log
|
|
222
|
+
}
|
|
223
|
+
// HITL for search results threats
|
|
224
|
+
const { output } = await handleCriticalThreatElicitation(result.value, `search: ${args.query}`);
|
|
225
|
+
return {
|
|
226
|
+
content: [
|
|
227
|
+
{
|
|
228
|
+
type: 'text',
|
|
229
|
+
text: JSON.stringify(output, null, 2)
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
case 'visus_report': {
|
|
235
|
+
const result = await visusReport(args);
|
|
236
|
+
if (!result.ok) {
|
|
237
|
+
throw new McpError(ErrorCode.InternalError, `visus_report failed: ${result.error.message}`);
|
|
238
|
+
}
|
|
239
|
+
// No VSIL/HITL for reports
|
|
240
|
+
return {
|
|
241
|
+
content: [
|
|
242
|
+
{
|
|
243
|
+
type: 'text',
|
|
244
|
+
text: JSON.stringify(result.value, null, 2)
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
case 'visus_verify': {
|
|
250
|
+
const result = await visusVerify(args);
|
|
251
|
+
if (!result.ok) {
|
|
252
|
+
throw new McpError(ErrorCode.InternalError, `visus_verify failed: ${result.error.message}`);
|
|
253
|
+
}
|
|
254
|
+
// No VSIL/HITL for verify
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
{
|
|
258
|
+
type: 'text',
|
|
259
|
+
text: JSON.stringify(result.value, null, 2)
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
case 'visus_read_csv': {
|
|
265
|
+
const result = await visusReadCsv(args);
|
|
266
|
+
if (!result.ok) {
|
|
267
|
+
throw new McpError(ErrorCode.InternalError, `visus_read_csv failed: ${result.error.message}`);
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: 'text',
|
|
273
|
+
text: JSON.stringify(result.value, null, 2)
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
case 'visus_read_excel': {
|
|
279
|
+
const result = await visusReadExcel(args);
|
|
280
|
+
if (!result.ok) {
|
|
281
|
+
throw new McpError(ErrorCode.InternalError, `visus_read_excel failed: ${result.error.message}`);
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: 'text',
|
|
287
|
+
text: JSON.stringify(result.value, null, 2)
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
case 'visus_read_gsheet': {
|
|
293
|
+
const result = await visusReadGsheet(args);
|
|
294
|
+
if (!result.ok) {
|
|
295
|
+
throw new McpError(ErrorCode.InternalError, `visus_read_gsheet failed: ${result.error.message}`);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
content: [
|
|
299
|
+
{
|
|
300
|
+
type: 'text',
|
|
301
|
+
text: JSON.stringify(result.value, null, 2)
|
|
302
|
+
}
|
|
303
|
+
]
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
case 'visus_context_scan': {
|
|
307
|
+
args.sessionId = sessionId;
|
|
308
|
+
const result = await visusContextScan(args);
|
|
309
|
+
return {
|
|
310
|
+
content: [
|
|
311
|
+
{ type: 'text', text: JSON.stringify(result, null, 2) }
|
|
312
|
+
]
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
case 'visus_get_ledger_proof': {
|
|
316
|
+
const { arguments: args } = request.params;
|
|
317
|
+
const result = await visusGetLedgerProof(args.request_id);
|
|
318
|
+
return {
|
|
319
|
+
content: [
|
|
320
|
+
{
|
|
321
|
+
type: 'text',
|
|
322
|
+
text: JSON.stringify(result, null, 2)
|
|
323
|
+
}
|
|
324
|
+
]
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
case 'visus_scan_mcp': {
|
|
328
|
+
const result = await visusScanMcp(args);
|
|
329
|
+
return {
|
|
330
|
+
content: [
|
|
331
|
+
{
|
|
332
|
+
type: 'text',
|
|
333
|
+
text: JSON.stringify(result, null, 2)
|
|
334
|
+
}
|
|
335
|
+
]
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
default:
|
|
339
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (error instanceof McpError) {
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
81
349
|
/**
|
|
82
350
|
* Helper function to handle HITL elicitation for CRITICAL threats
|
|
83
351
|
*
|
|
@@ -89,7 +357,7 @@ async function handleCriticalThreatElicitation(output, url, wormRisk = 0) {
|
|
|
89
357
|
const wormScore = output.sanitization?.worm_risk_score ?? 0;
|
|
90
358
|
if (shouldElicit(threatReport, Math.max(wormRisk, wormScore))) {
|
|
91
359
|
const message = buildElicitMessage(threatReport || { total_findings: 0, findings_toon: '', overall_severity: 'CRITICAL' }, url, Math.max(wormRisk, wormScore));
|
|
92
|
-
const { proceed, includeReport } = await runElicitation(server, message);
|
|
360
|
+
const { proceed, includeReport } = await runElicitation(server, message);
|
|
93
361
|
if (!proceed) {
|
|
94
362
|
return {
|
|
95
363
|
output: {
|
|
@@ -108,279 +376,6 @@ async function handleCriticalThreatElicitation(output, url, wormRisk = 0) {
|
|
|
108
376
|
}
|
|
109
377
|
return { output, blocked: false };
|
|
110
378
|
}
|
|
111
|
-
const threatReport = output.threat_report;
|
|
112
|
-
// Check if elicitation is needed
|
|
113
|
-
if (shouldElicit(threatReport ?? null)) {
|
|
114
|
-
const { proceed, includeReport } = await runElicitation(server, threatReport, url);
|
|
115
|
-
if (!proceed) {
|
|
116
|
-
// User declined — return blocked response with threat report
|
|
117
|
-
return {
|
|
118
|
-
output: {
|
|
119
|
-
url,
|
|
120
|
-
blocked: true,
|
|
121
|
-
reason: 'User declined to proceed after CRITICAL threat detected',
|
|
122
|
-
threat_report: threatReport
|
|
123
|
-
},
|
|
124
|
-
blocked: true
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
// User accepted — proceed with sanitized content
|
|
128
|
-
// Remove threat_report if user didn't request it
|
|
129
|
-
if (!includeReport && output.threat_report) {
|
|
130
|
-
const { threat_report, ...outputWithoutReport } = output;
|
|
131
|
-
return { output: outputWithoutReport, blocked: false };
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return { output, blocked: false };
|
|
135
|
-
/**
|
|
136
|
-
* Handle tool execution requests
|
|
137
|
-
*/
|
|
138
|
-
const ledger = new SessionLedger(); // Global instance for sessions
|
|
139
|
-
'visus_db_verify';
|
|
140
|
-
{
|
|
141
|
-
return await visusDbVerify(args);
|
|
142
|
-
}
|
|
143
|
-
'visus_fetch';
|
|
144
|
-
{
|
|
145
|
-
const result = await visusFetch(args);
|
|
146
|
-
if (!result.ok) {
|
|
147
|
-
throw new McpError(ErrorCode.InternalError, `visus_fetch failed: ${result.error.message}`);
|
|
148
|
-
}
|
|
149
|
-
// VSIL Check
|
|
150
|
-
const { score, newThreats, chainId, dangling } = await ledger.checkContextualIntegrity(sessionId, name, args, result.value);
|
|
151
|
-
if (score > 0.7) {
|
|
152
|
-
const threatReport = result.value.threat_report;
|
|
153
|
-
const message = 'High session risk detected from prior turns (chains/priming). Proceed with caution?';
|
|
154
|
-
const { proceed, includeReport } = await runElicitation(server, message);
|
|
155
|
-
if (!proceed) {
|
|
156
|
-
return {
|
|
157
|
-
content: [{ type: 'text', text: JSON.stringify({ blocked: true, session_risk: score, reason: 'User declined high-risk session' }, null, 2) }]
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
// Merge new threats
|
|
161
|
-
if (threatReport)
|
|
162
|
-
threatReport.new_threats = [...(threatReport.new_threats || []), ...newThreats];
|
|
163
|
-
}
|
|
164
|
-
// Update ledger
|
|
165
|
-
const hashes = ledger.extractEntityHashes ? await ledger.extractEntityHashes(args, result.value) : [];
|
|
166
|
-
ledger.update(sessionId, hashes, name, newThreats);
|
|
167
|
-
// Extend output
|
|
168
|
-
const extended = { ...result.value };
|
|
169
|
-
if (extended.threat_summary) {
|
|
170
|
-
extended.threat_summary.session_risk = score;
|
|
171
|
-
extended.threat_summary.chain_detected = !!chainId;
|
|
172
|
-
extended.threat_summary.priming_flags = dangling ? ['dangling_instruction'] : [];
|
|
173
|
-
}
|
|
174
|
-
// Existing HITL
|
|
175
|
-
const { output } = await handleCriticalThreatElicitation(extended, args.url);
|
|
176
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
177
|
-
}
|
|
178
|
-
'visus_fetch_structured';
|
|
179
|
-
{
|
|
180
|
-
const result = await visusFetchStructured(args);
|
|
181
|
-
if (!result.ok) {
|
|
182
|
-
throw new McpError(ErrorCode.InternalError, `visus_fetch_structured failed: ${result.error.message}`);
|
|
183
|
-
}
|
|
184
|
-
// VSIL Check (similar)
|
|
185
|
-
const { score, newThreats, chainId, dangling } = await ledger.checkContextualIntegrity(sessionId, name, args, result.value);
|
|
186
|
-
if (score > 0.7) {
|
|
187
|
-
const threatReport = result.value.threat_report;
|
|
188
|
-
const message = 'High session risk detected. Proceed with structured extraction?';
|
|
189
|
-
const { proceed } = await runElicitation(server, message);
|
|
190
|
-
if (!proceed) {
|
|
191
|
-
return {
|
|
192
|
-
content: [{ type: 'text', text: JSON.stringify({ blocked: true, session_risk: score }, null, 2) }]
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
if (threatReport)
|
|
196
|
-
threatReport.new_threats = [...(threatReport.new_threats || []), ...newThreats];
|
|
197
|
-
}
|
|
198
|
-
// Update ledger
|
|
199
|
-
const hashes = ledger.extractEntityHashes ? await ledger.extractEntityHashes(args, result.value) : [];
|
|
200
|
-
ledger.update(sessionId, hashes, name, newThreats);
|
|
201
|
-
// Extend output
|
|
202
|
-
const extended = { ...result.value };
|
|
203
|
-
if (extended.threat_summary) {
|
|
204
|
-
extended.threat_summary.session_risk = score;
|
|
205
|
-
extended.threat_summary.chain_detected = !!chainId;
|
|
206
|
-
}
|
|
207
|
-
// HITL for threats
|
|
208
|
-
const { output } = await handleCriticalThreatElicitation(extended, args.url);
|
|
209
|
-
return {
|
|
210
|
-
content: [
|
|
211
|
-
{
|
|
212
|
-
type: 'text',
|
|
213
|
-
text: JSON.stringify(output, null, 2)
|
|
214
|
-
}
|
|
215
|
-
]
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
'visus_read';
|
|
219
|
-
{
|
|
220
|
-
const result = await visusRead(args);
|
|
221
|
-
if (!result.ok) {
|
|
222
|
-
throw new McpError(ErrorCode.InternalError, `visus_read failed: ${result.error.message}`);
|
|
223
|
-
}
|
|
224
|
-
// VSIL for read (session continuity)
|
|
225
|
-
const { score } = await ledger.checkContextualIntegrity(sessionId, name, args, result.value);
|
|
226
|
-
if (score > 0.7) {
|
|
227
|
-
ledger.update(sessionId, [], name, []); // Log but no block for read-only
|
|
228
|
-
console.error(`High session risk for read: ${score}`); // Log only
|
|
229
|
-
}
|
|
230
|
-
// HITL for threats
|
|
231
|
-
const { output } = await handleCriticalThreatElicitation(result.value, args.url);
|
|
232
|
-
return {
|
|
233
|
-
content: [
|
|
234
|
-
{
|
|
235
|
-
type: 'text',
|
|
236
|
-
text: JSON.stringify(output, null, 2)
|
|
237
|
-
}
|
|
238
|
-
]
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
'visus_search';
|
|
242
|
-
{
|
|
243
|
-
const result = await visusSearch(args);
|
|
244
|
-
if (!result.ok) {
|
|
245
|
-
throw new McpError(ErrorCode.InternalError, `visus_search failed: ${result.error.message}`);
|
|
246
|
-
}
|
|
247
|
-
// VSIL for search (priming URLs)
|
|
248
|
-
const { score } = await ledger.checkContextualIntegrity(sessionId, name, args, result.value);
|
|
249
|
-
if (score > 0.7) {
|
|
250
|
-
ledger.update(sessionId, [], name, []); // Log
|
|
251
|
-
}
|
|
252
|
-
// HITL for search results threats
|
|
253
|
-
const { output } = await handleCriticalThreatElicitation(result.value, `search: ${args.query}`);
|
|
254
|
-
return {
|
|
255
|
-
content: [
|
|
256
|
-
{
|
|
257
|
-
type: 'text',
|
|
258
|
-
text: JSON.stringify(output, null, 2)
|
|
259
|
-
}
|
|
260
|
-
]
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
'visus_report';
|
|
264
|
-
{
|
|
265
|
-
const result = await visusReport(args);
|
|
266
|
-
if (!result.ok) {
|
|
267
|
-
throw new McpError(ErrorCode.InternalError, `visus_report failed: ${result.error.message}`);
|
|
268
|
-
}
|
|
269
|
-
// No VSIL/HITL for reports
|
|
270
|
-
return {
|
|
271
|
-
content: [
|
|
272
|
-
{
|
|
273
|
-
type: 'text',
|
|
274
|
-
text: JSON.stringify(result.value, null, 2)
|
|
275
|
-
}
|
|
276
|
-
]
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
'visus_verify';
|
|
280
|
-
{
|
|
281
|
-
const result = await visusVerify(args);
|
|
282
|
-
if (!result.ok) {
|
|
283
|
-
throw new McpError(ErrorCode.InternalError, `visus_verify failed: ${result.error.message}`);
|
|
284
|
-
}
|
|
285
|
-
// No VSIL/HITL for verify
|
|
286
|
-
return {
|
|
287
|
-
content: [
|
|
288
|
-
{
|
|
289
|
-
type: 'text',
|
|
290
|
-
text: JSON.stringify(result.value, null, 2)
|
|
291
|
-
}
|
|
292
|
-
]
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
'visus_read_csv';
|
|
296
|
-
{
|
|
297
|
-
const result = await visusReadCsv(args);
|
|
298
|
-
if (!result.ok) {
|
|
299
|
-
throw new McpError(ErrorCode.InternalError, `visus_read_csv failed: ${result.error.message}`);
|
|
300
|
-
}
|
|
301
|
-
return {
|
|
302
|
-
content: [
|
|
303
|
-
{
|
|
304
|
-
type: 'text',
|
|
305
|
-
text: JSON.stringify(result.value, null, 2)
|
|
306
|
-
}
|
|
307
|
-
]
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
'visus_read_excel';
|
|
311
|
-
{
|
|
312
|
-
const result = await visusReadExcel(args);
|
|
313
|
-
if (!result.ok) {
|
|
314
|
-
throw new McpError(ErrorCode.InternalError, `visus_read_excel failed: ${result.error.message}`);
|
|
315
|
-
}
|
|
316
|
-
return {
|
|
317
|
-
content: [
|
|
318
|
-
{
|
|
319
|
-
type: 'text',
|
|
320
|
-
text: JSON.stringify(result.value, null, 2)
|
|
321
|
-
}
|
|
322
|
-
]
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
'visus_read_gsheet';
|
|
326
|
-
{
|
|
327
|
-
const result = await visusReadGsheet(args);
|
|
328
|
-
if (!result.ok) {
|
|
329
|
-
throw new McpError(ErrorCode.InternalError, `visus_read_gsheet failed: ${result.error.message}`);
|
|
330
|
-
}
|
|
331
|
-
return {
|
|
332
|
-
content: [
|
|
333
|
-
{
|
|
334
|
-
type: 'text',
|
|
335
|
-
text: JSON.stringify(result.value, null, 2)
|
|
336
|
-
}
|
|
337
|
-
]
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
'visus_context_scan';
|
|
341
|
-
{
|
|
342
|
-
args.sessionId = sessionId;
|
|
343
|
-
const result = await visusContextScan(args);
|
|
344
|
-
return {
|
|
345
|
-
content: [
|
|
346
|
-
{ type: 'text', text: JSON.stringify(result, null, 2) }
|
|
347
|
-
]
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
'visus_get_ledger_proof';
|
|
351
|
-
{
|
|
352
|
-
const { arguments: args } = request.params;
|
|
353
|
-
const result = await visusGetLedgerProof(args.request_id);
|
|
354
|
-
return {
|
|
355
|
-
content: [
|
|
356
|
-
{
|
|
357
|
-
type: 'text',
|
|
358
|
-
text: JSON.stringify(result, null, 2)
|
|
359
|
-
}
|
|
360
|
-
]
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
'visus_scan_mcp';
|
|
364
|
-
{
|
|
365
|
-
const result = await visusScanMcp(args);
|
|
366
|
-
return {
|
|
367
|
-
content: [
|
|
368
|
-
{
|
|
369
|
-
type: 'text',
|
|
370
|
-
text: JSON.stringify(result, null, 2)
|
|
371
|
-
}
|
|
372
|
-
]
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
376
|
-
try { }
|
|
377
|
-
catch (error) {
|
|
378
|
-
if (error instanceof McpError) {
|
|
379
|
-
throw error;
|
|
380
|
-
}
|
|
381
|
-
throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
382
|
-
}
|
|
383
|
-
;
|
|
384
379
|
/**
|
|
385
380
|
* Start the MCP server (stdio mode)
|
|
386
381
|
*/
|