@reaatech/pi-bench-mcp-server 1.0.1
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/LICENSE +21 -0
- package/README.md +228 -0
- package/dist/index.cjs +797 -0
- package/dist/index.d.cts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +770 -0
- package/package.json +55 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BenchmarkMCPServer: () => BenchmarkMCPServer,
|
|
24
|
+
SeedManager: () => SeedManager,
|
|
25
|
+
createMCPServer: () => createMCPServer,
|
|
26
|
+
createSeedManager: () => createSeedManager,
|
|
27
|
+
normalizeReportData: () => normalizeReportData
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/mcp-server.ts
|
|
32
|
+
var import_promises = require("fs/promises");
|
|
33
|
+
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
34
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
35
|
+
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
36
|
+
var import_pi_bench_adapters = require("@reaatech/pi-bench-adapters");
|
|
37
|
+
var import_pi_bench_adapters2 = require("@reaatech/pi-bench-adapters");
|
|
38
|
+
var import_pi_bench_adapters3 = require("@reaatech/pi-bench-adapters");
|
|
39
|
+
var import_pi_bench_adapters4 = require("@reaatech/pi-bench-adapters");
|
|
40
|
+
var import_pi_bench_adapters5 = require("@reaatech/pi-bench-adapters");
|
|
41
|
+
var import_pi_bench_adapters6 = require("@reaatech/pi-bench-adapters");
|
|
42
|
+
var import_pi_bench_core = require("@reaatech/pi-bench-core");
|
|
43
|
+
var import_pi_bench_core2 = require("@reaatech/pi-bench-core");
|
|
44
|
+
var import_pi_bench_corpus = require("@reaatech/pi-bench-corpus");
|
|
45
|
+
var import_pi_bench_leaderboard = require("@reaatech/pi-bench-leaderboard");
|
|
46
|
+
var import_pi_bench_leaderboard2 = require("@reaatech/pi-bench-leaderboard");
|
|
47
|
+
var import_pi_bench_observability = require("@reaatech/pi-bench-observability");
|
|
48
|
+
var import_pi_bench_runner = require("@reaatech/pi-bench-runner");
|
|
49
|
+
var import_pi_bench_runner2 = require("@reaatech/pi-bench-runner");
|
|
50
|
+
var import_pi_bench_runner3 = require("@reaatech/pi-bench-runner");
|
|
51
|
+
|
|
52
|
+
// src/report-data.ts
|
|
53
|
+
var import_pi_bench_scoring = require("@reaatech/pi-bench-scoring");
|
|
54
|
+
function isObject(value) {
|
|
55
|
+
return typeof value === "object" && value !== null;
|
|
56
|
+
}
|
|
57
|
+
function isBenchmarkResult(value) {
|
|
58
|
+
return isObject(value) && Array.isArray(value.attackResults) && Array.isArray(value.benignResults) && typeof value.defense === "string" && typeof value.defenseVersion === "string";
|
|
59
|
+
}
|
|
60
|
+
function toCategoryBreakdown(categoryScores) {
|
|
61
|
+
if (!categoryScores) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
return Object.fromEntries(
|
|
65
|
+
Object.entries(categoryScores).map(([category, score]) => [
|
|
66
|
+
category,
|
|
67
|
+
{
|
|
68
|
+
detectionRate: score.detectionRate,
|
|
69
|
+
totalAttacks: score.totalAttacks
|
|
70
|
+
}
|
|
71
|
+
])
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
function normalizeReportData(results) {
|
|
75
|
+
if (!isObject(results)) {
|
|
76
|
+
throw new Error("Results must be a JSON object");
|
|
77
|
+
}
|
|
78
|
+
const score = isObject(results.score) ? results.score : void 0;
|
|
79
|
+
const overallMetrics = isObject(results.overallMetrics) ? results.overallMetrics : void 0;
|
|
80
|
+
const categoryBreakdown = isObject(results.categoryBreakdown) ? results.categoryBreakdown : void 0;
|
|
81
|
+
if (score && overallMetrics) {
|
|
82
|
+
return {
|
|
83
|
+
defense: typeof results.defense === "string" ? results.defense : score.defense,
|
|
84
|
+
version: typeof results.defenseVersion === "string" ? results.defenseVersion : score.version,
|
|
85
|
+
corpusVersion: typeof results.corpusVersion === "string" ? results.corpusVersion : isObject(results.metadata) && typeof results.metadata.corpusVersion === "string" ? results.metadata.corpusVersion : void 0,
|
|
86
|
+
generatedAt: isObject(results.metadata) && typeof results.metadata.generatedAt === "string" ? results.metadata.generatedAt : typeof results.timestamp === "string" ? results.timestamp : void 0,
|
|
87
|
+
detectionRate: typeof overallMetrics.detectionRate === "number" ? overallMetrics.detectionRate : 1 - score.attackSuccessRate,
|
|
88
|
+
falsePositiveRate: typeof overallMetrics.fpr === "number" ? overallMetrics.fpr : score.falsePositiveRate,
|
|
89
|
+
totalAttacks: typeof overallMetrics.totalAttacks === "number" ? overallMetrics.totalAttacks : score.totalSamples,
|
|
90
|
+
avgLatencyMs: typeof overallMetrics.avgLatencyMs === "number" ? overallMetrics.avgLatencyMs : score.avgLatencyMs,
|
|
91
|
+
categoryBreakdown: categoryBreakdown || toCategoryBreakdown(score.categoryScores)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (isBenchmarkResult(results)) {
|
|
95
|
+
const derivedScore = (0, import_pi_bench_scoring.calculateDefenseScore)(results);
|
|
96
|
+
return {
|
|
97
|
+
defense: results.defense,
|
|
98
|
+
version: results.defenseVersion,
|
|
99
|
+
corpusVersion: results.corpusVersion,
|
|
100
|
+
generatedAt: results.timestamp,
|
|
101
|
+
detectionRate: 1 - derivedScore.attackSuccessRate,
|
|
102
|
+
falsePositiveRate: derivedScore.falsePositiveRate,
|
|
103
|
+
totalAttacks: derivedScore.totalSamples,
|
|
104
|
+
avgLatencyMs: derivedScore.avgLatencyMs,
|
|
105
|
+
categoryBreakdown: toCategoryBreakdown(derivedScore.categoryScores)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (typeof results.detectionRate === "number" && typeof results.falsePositiveRate === "number" && typeof results.totalAttacks === "number" && typeof results.avgLatencyMs === "number") {
|
|
109
|
+
return {
|
|
110
|
+
defense: typeof results.defense === "string" ? results.defense : void 0,
|
|
111
|
+
version: typeof results.version === "string" ? results.version : void 0,
|
|
112
|
+
corpusVersion: typeof results.corpusVersion === "string" ? results.corpusVersion : void 0,
|
|
113
|
+
generatedAt: typeof results.timestamp === "string" ? results.timestamp : void 0,
|
|
114
|
+
detectionRate: results.detectionRate,
|
|
115
|
+
falsePositiveRate: results.falsePositiveRate,
|
|
116
|
+
totalAttacks: results.totalAttacks,
|
|
117
|
+
avgLatencyMs: results.avgLatencyMs,
|
|
118
|
+
categoryBreakdown: categoryBreakdown || {}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
throw new Error("Results file is not a recognized benchmark or report format");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/mcp-server.ts
|
|
125
|
+
var logger = (0, import_pi_bench_observability.createLogger)();
|
|
126
|
+
var DEFAULT_CONFIG = {
|
|
127
|
+
name: "prompt-injection-bench",
|
|
128
|
+
version: "1.0.0"
|
|
129
|
+
};
|
|
130
|
+
function validateFilePath(filePath) {
|
|
131
|
+
if (!filePath || filePath.trim().length === 0) {
|
|
132
|
+
throw new Error("File path cannot be empty");
|
|
133
|
+
}
|
|
134
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
135
|
+
if (normalized.includes("../") || normalized.includes("..\\")) {
|
|
136
|
+
throw new Error("File path cannot contain path traversal sequences");
|
|
137
|
+
}
|
|
138
|
+
if (normalized.startsWith("/")) {
|
|
139
|
+
throw new Error("Absolute file paths are not allowed");
|
|
140
|
+
}
|
|
141
|
+
if (normalized.includes("~")) {
|
|
142
|
+
throw new Error("Home directory shortcuts are not allowed");
|
|
143
|
+
}
|
|
144
|
+
if (/^\.\./.test(normalized)) {
|
|
145
|
+
throw new Error("File path cannot start with parent directory reference");
|
|
146
|
+
}
|
|
147
|
+
if (normalized.includes("//")) {
|
|
148
|
+
throw new Error("File path cannot contain double slashes");
|
|
149
|
+
}
|
|
150
|
+
if (/^\s/.test(normalized) || /\s$/.test(normalized)) {
|
|
151
|
+
throw new Error("File path cannot have leading or trailing whitespace");
|
|
152
|
+
}
|
|
153
|
+
if (/\.\./.test(normalized)) {
|
|
154
|
+
const parts = normalized.split("/");
|
|
155
|
+
for (const part of parts) {
|
|
156
|
+
if (part === ".." || part.endsWith("..")) {
|
|
157
|
+
throw new Error("File path contains parent directory references");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const pathSegments = normalized.split("/").filter(Boolean);
|
|
162
|
+
for (const segment of pathSegments) {
|
|
163
|
+
if (segment === ".." || segment.includes("..")) {
|
|
164
|
+
throw new Error("File path contains invalid segments");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function loadAdapter(name) {
|
|
169
|
+
switch (name.toLowerCase()) {
|
|
170
|
+
case "mock":
|
|
171
|
+
return (0, import_pi_bench_adapters.createMockAdapter)(0.85, 0.05);
|
|
172
|
+
case "rebuff":
|
|
173
|
+
return (0, import_pi_bench_adapters2.createRebuffAdapter)();
|
|
174
|
+
case "lakera":
|
|
175
|
+
return (0, import_pi_bench_adapters3.createLakeraAdapter)();
|
|
176
|
+
case "llm-guard":
|
|
177
|
+
return (0, import_pi_bench_adapters4.createLLMGuardAdapter)();
|
|
178
|
+
case "garak":
|
|
179
|
+
return (0, import_pi_bench_adapters5.createGarakAdapter)();
|
|
180
|
+
case "moderation-openai":
|
|
181
|
+
return (0, import_pi_bench_adapters6.createModerationAdapter)({ provider: "openai" });
|
|
182
|
+
case "moderation-azure":
|
|
183
|
+
return (0, import_pi_bench_adapters6.createModerationAdapter)({ provider: "azure" });
|
|
184
|
+
case "moderation-anthropic":
|
|
185
|
+
return (0, import_pi_bench_adapters6.createModerationAdapter)({ provider: "anthropic" });
|
|
186
|
+
case "moderation-cohere":
|
|
187
|
+
return (0, import_pi_bench_adapters6.createModerationAdapter)({ provider: "cohere" });
|
|
188
|
+
default:
|
|
189
|
+
throw new Error(`Unknown defense adapter: ${name}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
var BenchmarkMCPServer = class {
|
|
193
|
+
server;
|
|
194
|
+
config;
|
|
195
|
+
constructor(config = {}) {
|
|
196
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
197
|
+
this.server = new import_server.Server(
|
|
198
|
+
{
|
|
199
|
+
name: this.config.name,
|
|
200
|
+
version: this.config.version
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
capabilities: {
|
|
204
|
+
tools: {}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
this.setupToolHandlers();
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get the list of available tools (exposed for testing)
|
|
212
|
+
*/
|
|
213
|
+
getToolDefinitions() {
|
|
214
|
+
return [
|
|
215
|
+
{
|
|
216
|
+
name: "run_benchmark",
|
|
217
|
+
description: "Execute a full benchmark against defenses",
|
|
218
|
+
inputSchema: {
|
|
219
|
+
type: "object",
|
|
220
|
+
properties: {
|
|
221
|
+
defense: { type: "string", description: "Defense adapter name" },
|
|
222
|
+
corpus: { type: "string", description: "Corpus version to use", default: "2026.04" },
|
|
223
|
+
categories: {
|
|
224
|
+
type: "array",
|
|
225
|
+
items: { type: "string" },
|
|
226
|
+
description: "Attack categories to include"
|
|
227
|
+
},
|
|
228
|
+
parallel: { type: "number", description: "Number of parallel executions", default: 10 },
|
|
229
|
+
timeout_ms: { type: "number", description: "Timeout per attack in ms", default: 3e4 }
|
|
230
|
+
},
|
|
231
|
+
required: ["defense"]
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "compare_defenses",
|
|
236
|
+
description: "Compare multiple defense results",
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
results: {
|
|
241
|
+
type: "array",
|
|
242
|
+
items: { type: "string" },
|
|
243
|
+
description: "Paths to result files to compare"
|
|
244
|
+
},
|
|
245
|
+
significance_level: {
|
|
246
|
+
type: "number",
|
|
247
|
+
description: "Statistical significance level",
|
|
248
|
+
default: 0.05
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
required: ["results"]
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "generate_report",
|
|
256
|
+
description: "Generate HTML/JSON reports from benchmark results",
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {
|
|
260
|
+
results: { type: "string", description: "Path to results file" },
|
|
261
|
+
format: {
|
|
262
|
+
type: "string",
|
|
263
|
+
enum: ["json", "html", "markdown"],
|
|
264
|
+
description: "Output format",
|
|
265
|
+
default: "json"
|
|
266
|
+
},
|
|
267
|
+
include_categories: {
|
|
268
|
+
type: "boolean",
|
|
269
|
+
description: "Include category breakdown",
|
|
270
|
+
default: true
|
|
271
|
+
},
|
|
272
|
+
output: { type: "string", description: "Output file path" }
|
|
273
|
+
},
|
|
274
|
+
required: ["results"]
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: "submit_results",
|
|
279
|
+
description: "Submit results to the public leaderboard",
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: "object",
|
|
282
|
+
properties: {
|
|
283
|
+
results: { type: "string", description: "Path to results file" },
|
|
284
|
+
defense_name: { type: "string", description: "Name of the defense" },
|
|
285
|
+
defense_version: { type: "string", description: "Version of the defense" },
|
|
286
|
+
reproducibility_proof: {
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: {
|
|
289
|
+
seed: { type: "string", description: "Seed used for the run" },
|
|
290
|
+
corpus_version: { type: "string", description: "Corpus version used" },
|
|
291
|
+
adapter_versions: {
|
|
292
|
+
type: "object",
|
|
293
|
+
additionalProperties: { type: "string" },
|
|
294
|
+
description: "Versions of adapters used"
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
description: "Reproducibility proof data"
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
required: ["results", "defense_name"]
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Set up tool request handlers
|
|
307
|
+
*/
|
|
308
|
+
setupToolHandlers() {
|
|
309
|
+
this.server.setRequestHandler(import_types.ListToolsRequestSchema, async () => {
|
|
310
|
+
return {
|
|
311
|
+
tools: this.getToolDefinitions()
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
this.server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
|
|
315
|
+
const { name, arguments: args } = request.params;
|
|
316
|
+
switch (name) {
|
|
317
|
+
case "run_benchmark":
|
|
318
|
+
return await this.handleRunBenchmark(args);
|
|
319
|
+
case "compare_defenses":
|
|
320
|
+
return await this.handleCompareDefenses(args);
|
|
321
|
+
case "generate_report":
|
|
322
|
+
return await this.handleGenerateReport(args);
|
|
323
|
+
case "submit_results":
|
|
324
|
+
return await this.handleSubmitResults(args);
|
|
325
|
+
default:
|
|
326
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Handle run_benchmark tool call
|
|
332
|
+
*/
|
|
333
|
+
async handleRunBenchmark(args) {
|
|
334
|
+
const runArgs = args;
|
|
335
|
+
const defenseName = runArgs.defense;
|
|
336
|
+
const corpusVersion = runArgs.corpus || "2026.04";
|
|
337
|
+
const parallel = runArgs.parallel || 10;
|
|
338
|
+
const timeoutMs = runArgs.timeout_ms || 3e4;
|
|
339
|
+
const categories = runArgs.categories;
|
|
340
|
+
logger.info("MCP: Starting benchmark", {
|
|
341
|
+
defense: defenseName,
|
|
342
|
+
corpus: corpusVersion,
|
|
343
|
+
parallel,
|
|
344
|
+
timeoutMs
|
|
345
|
+
});
|
|
346
|
+
try {
|
|
347
|
+
const adapter = await loadAdapter(defenseName);
|
|
348
|
+
if (adapter.initialize) {
|
|
349
|
+
await adapter.initialize?.();
|
|
350
|
+
}
|
|
351
|
+
const engine = (0, import_pi_bench_runner.createBenchmarkEngine)({ maxParallel: parallel, defaultTimeoutMs: timeoutMs });
|
|
352
|
+
engine.setAdapter(adapter);
|
|
353
|
+
const corpus = (0, import_pi_bench_corpus.generateDefaultCorpus)();
|
|
354
|
+
const attackSamples = categories ? corpus.filter((s) => categories.includes(s.category)) : corpus;
|
|
355
|
+
const benignSamples = (0, import_pi_bench_runner3.generateBenignSamples)(100);
|
|
356
|
+
const result = await engine.runBenchmark(
|
|
357
|
+
{
|
|
358
|
+
defense: defenseName,
|
|
359
|
+
corpusVersion,
|
|
360
|
+
categories: categories || (0, import_pi_bench_core.getCategoryIds)(),
|
|
361
|
+
parallel,
|
|
362
|
+
timeoutMs,
|
|
363
|
+
seed: Date.now().toString(36)
|
|
364
|
+
},
|
|
365
|
+
attackSamples,
|
|
366
|
+
benignSamples
|
|
367
|
+
);
|
|
368
|
+
const evaluator = (0, import_pi_bench_runner2.createDefenseEvaluator)();
|
|
369
|
+
const evaluation = evaluator.evaluate(result);
|
|
370
|
+
await adapter.cleanup?.();
|
|
371
|
+
const output = {
|
|
372
|
+
status: "success",
|
|
373
|
+
runId: result.runId,
|
|
374
|
+
defense: defenseName,
|
|
375
|
+
defenseVersion: adapter.version,
|
|
376
|
+
corpusVersion,
|
|
377
|
+
score: evaluation.score,
|
|
378
|
+
overallMetrics: evaluation.overallMetrics,
|
|
379
|
+
categoryBreakdown: evaluation.categoryBreakdown,
|
|
380
|
+
timestamp: result.timestamp
|
|
381
|
+
};
|
|
382
|
+
return {
|
|
383
|
+
content: [
|
|
384
|
+
{
|
|
385
|
+
type: "text",
|
|
386
|
+
text: JSON.stringify(output, null, 2)
|
|
387
|
+
}
|
|
388
|
+
]
|
|
389
|
+
};
|
|
390
|
+
} catch (error) {
|
|
391
|
+
logger.error("MCP: Benchmark failed", {
|
|
392
|
+
error: error instanceof Error ? error.message : String(error)
|
|
393
|
+
});
|
|
394
|
+
return {
|
|
395
|
+
content: [
|
|
396
|
+
{
|
|
397
|
+
type: "text",
|
|
398
|
+
text: JSON.stringify(
|
|
399
|
+
{
|
|
400
|
+
status: "error",
|
|
401
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
402
|
+
},
|
|
403
|
+
null,
|
|
404
|
+
2
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
]
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Handle compare_defenses tool call
|
|
413
|
+
*/
|
|
414
|
+
async handleCompareDefenses(args) {
|
|
415
|
+
const compareArgs = args;
|
|
416
|
+
const resultFiles = compareArgs.results;
|
|
417
|
+
const significanceLevel = compareArgs.significance_level || 0.05;
|
|
418
|
+
logger.info("MCP: Comparing defenses", { files: resultFiles, significance: significanceLevel });
|
|
419
|
+
try {
|
|
420
|
+
if (resultFiles.length < 2) {
|
|
421
|
+
throw new Error("At least 2 result files are required for comparison");
|
|
422
|
+
}
|
|
423
|
+
const results = await Promise.all(
|
|
424
|
+
resultFiles.map(async (file) => {
|
|
425
|
+
validateFilePath(file);
|
|
426
|
+
const content = await (0, import_promises.readFile)(file, "utf-8");
|
|
427
|
+
const parsed = JSON.parse(content);
|
|
428
|
+
const score = parsed.score || parsed;
|
|
429
|
+
const validated = import_pi_bench_core2.DefenseScoreSchema.parse(score);
|
|
430
|
+
return { ...parsed, score: validated };
|
|
431
|
+
})
|
|
432
|
+
);
|
|
433
|
+
const evaluator = (0, import_pi_bench_runner2.createDefenseEvaluator)();
|
|
434
|
+
const comparisons = [];
|
|
435
|
+
for (let i = 0; i < results.length; i++) {
|
|
436
|
+
for (let j = i + 1; j < results.length; j++) {
|
|
437
|
+
const score1 = results[i].score;
|
|
438
|
+
const score2 = results[j].score;
|
|
439
|
+
const defense1Name = results[i].defense || results[i].defenseName || `Defense ${i + 1}`;
|
|
440
|
+
const defense2Name = results[j].defense || results[j].defenseName || `Defense ${j + 1}`;
|
|
441
|
+
const comparison = evaluator.compare(score1, score2);
|
|
442
|
+
comparisons.push({
|
|
443
|
+
defense1: defense1Name,
|
|
444
|
+
defense2: defense2Name,
|
|
445
|
+
winner: comparison.winner,
|
|
446
|
+
scoreDifference: comparison.scoreDifference,
|
|
447
|
+
asrDifference: comparison.asrDifference,
|
|
448
|
+
fprDifference: comparison.fprDifference
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
content: [
|
|
454
|
+
{
|
|
455
|
+
type: "text",
|
|
456
|
+
text: JSON.stringify(
|
|
457
|
+
{
|
|
458
|
+
status: "success",
|
|
459
|
+
comparisons,
|
|
460
|
+
significanceLevel,
|
|
461
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
462
|
+
},
|
|
463
|
+
null,
|
|
464
|
+
2
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
]
|
|
468
|
+
};
|
|
469
|
+
} catch (error) {
|
|
470
|
+
logger.error("MCP: Compare failed", {
|
|
471
|
+
error: error instanceof Error ? error.message : String(error)
|
|
472
|
+
});
|
|
473
|
+
return {
|
|
474
|
+
content: [
|
|
475
|
+
{
|
|
476
|
+
type: "text",
|
|
477
|
+
text: JSON.stringify(
|
|
478
|
+
{
|
|
479
|
+
status: "error",
|
|
480
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
481
|
+
},
|
|
482
|
+
null,
|
|
483
|
+
2
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
]
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Handle generate_report tool call
|
|
492
|
+
*/
|
|
493
|
+
async handleGenerateReport(args) {
|
|
494
|
+
const reportArgs = args;
|
|
495
|
+
const resultsFile = reportArgs.results;
|
|
496
|
+
const format = reportArgs.format || "json";
|
|
497
|
+
const includeCategories = reportArgs.include_categories ?? true;
|
|
498
|
+
logger.info("MCP: Generating report", { results: resultsFile, format });
|
|
499
|
+
try {
|
|
500
|
+
validateFilePath(resultsFile);
|
|
501
|
+
const content = await (0, import_promises.readFile)(resultsFile, "utf-8");
|
|
502
|
+
const results = JSON.parse(content);
|
|
503
|
+
const reportData = normalizeReportData(results);
|
|
504
|
+
let report;
|
|
505
|
+
if (format === "markdown") {
|
|
506
|
+
report = generateMarkdownReport(reportData, includeCategories);
|
|
507
|
+
} else if (format === "html") {
|
|
508
|
+
report = generateHtmlReport(reportData, includeCategories);
|
|
509
|
+
} else {
|
|
510
|
+
report = JSON.stringify(reportData, null, 2);
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
content: [
|
|
514
|
+
{
|
|
515
|
+
type: "text",
|
|
516
|
+
text: report
|
|
517
|
+
}
|
|
518
|
+
]
|
|
519
|
+
};
|
|
520
|
+
} catch (error) {
|
|
521
|
+
logger.error("MCP: Report generation failed", {
|
|
522
|
+
error: error instanceof Error ? error.message : String(error)
|
|
523
|
+
});
|
|
524
|
+
return {
|
|
525
|
+
content: [
|
|
526
|
+
{
|
|
527
|
+
type: "text",
|
|
528
|
+
text: JSON.stringify(
|
|
529
|
+
{
|
|
530
|
+
status: "error",
|
|
531
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
532
|
+
},
|
|
533
|
+
null,
|
|
534
|
+
2
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
]
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Handle submit_results tool call
|
|
543
|
+
*/
|
|
544
|
+
async handleSubmitResults(args) {
|
|
545
|
+
const submitArgs = args;
|
|
546
|
+
const resultsFile = submitArgs.results;
|
|
547
|
+
const defenseName = submitArgs.defense_name;
|
|
548
|
+
const defenseVersion = submitArgs.defense_version || "1.0.0";
|
|
549
|
+
const proof = submitArgs.reproducibility_proof;
|
|
550
|
+
logger.info("MCP: Submitting results", { defense: defenseName, version: defenseVersion });
|
|
551
|
+
try {
|
|
552
|
+
validateFilePath(resultsFile);
|
|
553
|
+
const content = await (0, import_promises.readFile)(resultsFile, "utf-8");
|
|
554
|
+
const parsed = JSON.parse(content);
|
|
555
|
+
if (parsed.score) {
|
|
556
|
+
import_pi_bench_core2.DefenseScoreSchema.parse(parsed.score);
|
|
557
|
+
}
|
|
558
|
+
const results = parsed;
|
|
559
|
+
const proofHash = proof?.seed || `local-${Date.now().toString(36)}`;
|
|
560
|
+
const manager = (0, import_pi_bench_leaderboard.createLeaderboardManager)();
|
|
561
|
+
manager.replaceEntries((0, import_pi_bench_leaderboard2.loadLeaderboardEntries)());
|
|
562
|
+
manager.addEntry({
|
|
563
|
+
defense: defenseName,
|
|
564
|
+
version: defenseVersion,
|
|
565
|
+
overallScore: results.score?.overallScore || results.overallScore || 0,
|
|
566
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
567
|
+
corpusVersion: results.corpusVersion || "2026.04",
|
|
568
|
+
categoryScores: results.score?.categoryScores ? Object.fromEntries(
|
|
569
|
+
Object.entries(results.score.categoryScores).map(([category, value]) => [
|
|
570
|
+
category,
|
|
571
|
+
value.detectionRate || 0
|
|
572
|
+
])
|
|
573
|
+
) : {},
|
|
574
|
+
proofHash,
|
|
575
|
+
submitter: "mcp-user"
|
|
576
|
+
});
|
|
577
|
+
(0, import_pi_bench_leaderboard2.saveLeaderboardEntries)(manager.getAllEntries());
|
|
578
|
+
const entry = {
|
|
579
|
+
status: "success",
|
|
580
|
+
submission: {
|
|
581
|
+
defense: defenseName,
|
|
582
|
+
version: defenseVersion,
|
|
583
|
+
overallScore: results.score?.overallScore || results.overallScore || 0,
|
|
584
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
585
|
+
corpusVersion: results.corpusVersion || "2026.04",
|
|
586
|
+
proofHash,
|
|
587
|
+
storagePath: (0, import_pi_bench_leaderboard2.getDefaultLeaderboardPath)()
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
return {
|
|
591
|
+
content: [
|
|
592
|
+
{
|
|
593
|
+
type: "text",
|
|
594
|
+
text: JSON.stringify(entry, null, 2)
|
|
595
|
+
}
|
|
596
|
+
]
|
|
597
|
+
};
|
|
598
|
+
} catch (error) {
|
|
599
|
+
logger.error("MCP: Submission failed", {
|
|
600
|
+
error: error instanceof Error ? error.message : String(error)
|
|
601
|
+
});
|
|
602
|
+
return {
|
|
603
|
+
content: [
|
|
604
|
+
{
|
|
605
|
+
type: "text",
|
|
606
|
+
text: JSON.stringify(
|
|
607
|
+
{
|
|
608
|
+
status: "error",
|
|
609
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
610
|
+
},
|
|
611
|
+
null,
|
|
612
|
+
2
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
]
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Start the MCP server
|
|
621
|
+
*/
|
|
622
|
+
async start() {
|
|
623
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
624
|
+
await this.server.connect(transport);
|
|
625
|
+
console.log("MCP server started");
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
function generateMarkdownReport(results, includeCategories) {
|
|
629
|
+
let md = "# Prompt Injection Benchmark Report\n\n";
|
|
630
|
+
md += `Generated: ${results.generatedAt || (/* @__PURE__ */ new Date()).toISOString()}
|
|
631
|
+
|
|
632
|
+
`;
|
|
633
|
+
if (results.defense) {
|
|
634
|
+
md += `Defense: ${results.defense}${results.version ? ` (${results.version})` : ""}
|
|
635
|
+
|
|
636
|
+
`;
|
|
637
|
+
}
|
|
638
|
+
md += "## Summary\n\n";
|
|
639
|
+
md += "| Metric | Value |\n|--------|-------|\n";
|
|
640
|
+
md += `| Detection Rate | ${(results.detectionRate * 100).toFixed(1)}% |
|
|
641
|
+
`;
|
|
642
|
+
md += `| False Positive Rate | ${(results.falsePositiveRate * 100).toFixed(1)}% |
|
|
643
|
+
`;
|
|
644
|
+
md += `| Total Attacks | ${results.totalAttacks || 0} |
|
|
645
|
+
`;
|
|
646
|
+
md += `| Avg Latency | ${results.avgLatencyMs.toFixed(1)}ms |
|
|
647
|
+
`;
|
|
648
|
+
if (includeCategories && Object.keys(results.categoryBreakdown).length > 0) {
|
|
649
|
+
md += "\n## Category Breakdown\n\n";
|
|
650
|
+
md += "| Category | Detection Rate | Attacks |\n|----------|----------------|---------|\n";
|
|
651
|
+
for (const [category, data] of Object.entries(results.categoryBreakdown)) {
|
|
652
|
+
md += `| ${category} | ${(data.detectionRate * 100).toFixed(1)}% | ${data.totalAttacks} |
|
|
653
|
+
`;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return md;
|
|
657
|
+
}
|
|
658
|
+
function generateHtmlReport(results, includeCategories) {
|
|
659
|
+
return `<!DOCTYPE html>
|
|
660
|
+
<html>
|
|
661
|
+
<head>
|
|
662
|
+
<title>Prompt Injection Benchmark Report</title>
|
|
663
|
+
<style>
|
|
664
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
|
|
665
|
+
h1 { color: #333; }
|
|
666
|
+
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
|
|
667
|
+
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
|
668
|
+
th { background: #f5f5f5; }
|
|
669
|
+
.metric { display: inline-block; margin: 10px 20px 10px 0; padding: 15px 25px; background: #f8f9fa; border-radius: 8px; }
|
|
670
|
+
.metric-value { font-size: 24px; font-weight: bold; color: #007bff; }
|
|
671
|
+
.metric-label { font-size: 12px; color: #666; text-transform: uppercase; }
|
|
672
|
+
</style>
|
|
673
|
+
</head>
|
|
674
|
+
<body>
|
|
675
|
+
<h1>Prompt Injection Benchmark Report</h1>
|
|
676
|
+
<p>Generated: ${results.generatedAt || (/* @__PURE__ */ new Date()).toISOString()}</p>
|
|
677
|
+
${results.defense ? `<p>Defense: ${results.defense}${results.version ? ` (${results.version})` : ""}</p>` : ""}
|
|
678
|
+
|
|
679
|
+
<h2>Summary</h2>
|
|
680
|
+
<div class="metric">
|
|
681
|
+
<div class="metric-value">${(results.detectionRate * 100).toFixed(1)}%</div>
|
|
682
|
+
<div class="metric-label">Detection Rate</div>
|
|
683
|
+
</div>
|
|
684
|
+
<div class="metric">
|
|
685
|
+
<div class="metric-value">${(results.falsePositiveRate * 100).toFixed(1)}%</div>
|
|
686
|
+
<div class="metric-label">False Positive Rate</div>
|
|
687
|
+
</div>
|
|
688
|
+
<div class="metric">
|
|
689
|
+
<div class="metric-value">${results.totalAttacks || 0}</div>
|
|
690
|
+
<div class="metric-label">Total Attacks</div>
|
|
691
|
+
</div>
|
|
692
|
+
<div class="metric">
|
|
693
|
+
<div class="metric-value">${results.avgLatencyMs.toFixed(1)}ms</div>
|
|
694
|
+
<div class="metric-label">Avg Latency</div>
|
|
695
|
+
</div>
|
|
696
|
+
${includeCategories && Object.keys(results.categoryBreakdown).length > 0 ? `
|
|
697
|
+
<h2>Category Breakdown</h2>
|
|
698
|
+
<table>
|
|
699
|
+
<tr><th>Category</th><th>Detection Rate</th><th>Attacks</th></tr>
|
|
700
|
+
${Object.entries(results.categoryBreakdown).map(
|
|
701
|
+
([category, data]) => `<tr><td>${category}</td><td>${(data.detectionRate * 100).toFixed(1)}%</td><td>${data.totalAttacks}</td></tr>`
|
|
702
|
+
).join("")}
|
|
703
|
+
</table>
|
|
704
|
+
` : ""}
|
|
705
|
+
</body>
|
|
706
|
+
</html>`;
|
|
707
|
+
}
|
|
708
|
+
function createMCPServer(config) {
|
|
709
|
+
return new BenchmarkMCPServer(config);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/seed-manager.ts
|
|
713
|
+
var import_node_crypto = require("crypto");
|
|
714
|
+
var SeedManager = class {
|
|
715
|
+
seed;
|
|
716
|
+
config;
|
|
717
|
+
constructor(config = {}) {
|
|
718
|
+
this.config = {
|
|
719
|
+
baseSeed: Date.now(),
|
|
720
|
+
corpusVersion: "2026.04",
|
|
721
|
+
adapterVersions: {},
|
|
722
|
+
...config
|
|
723
|
+
};
|
|
724
|
+
this.seed = this.config.baseSeed;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Get the current seed
|
|
728
|
+
*/
|
|
729
|
+
getSeed() {
|
|
730
|
+
return this.seed;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Set a new seed
|
|
734
|
+
*/
|
|
735
|
+
setSeed(seed) {
|
|
736
|
+
this.seed = seed;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Generate a deterministic random number
|
|
740
|
+
*/
|
|
741
|
+
next() {
|
|
742
|
+
const a = 1664525;
|
|
743
|
+
const c = 1013904223;
|
|
744
|
+
const m = 2 ** 32;
|
|
745
|
+
this.seed = (a * this.seed + c) % m;
|
|
746
|
+
return this.seed / m;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Generate a deterministic random integer in range
|
|
750
|
+
*/
|
|
751
|
+
nextInt(min, max) {
|
|
752
|
+
return Math.floor(this.next() * (max - min + 1)) + min;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Shuffle an array deterministically
|
|
756
|
+
*/
|
|
757
|
+
shuffle(array) {
|
|
758
|
+
const shuffled = [...array];
|
|
759
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
760
|
+
const j = this.nextInt(0, i);
|
|
761
|
+
const temp = shuffled[i];
|
|
762
|
+
const swapVal = shuffled[j];
|
|
763
|
+
if (temp == null || swapVal == null) continue;
|
|
764
|
+
shuffled[i] = swapVal;
|
|
765
|
+
shuffled[j] = temp;
|
|
766
|
+
}
|
|
767
|
+
return shuffled;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Create a hash from config for reproducibility proof
|
|
771
|
+
*/
|
|
772
|
+
createReproducibilityHash() {
|
|
773
|
+
const data = JSON.stringify({
|
|
774
|
+
seed: this.seed,
|
|
775
|
+
corpusVersion: this.config.corpusVersion,
|
|
776
|
+
adapterVersions: this.config.adapterVersions
|
|
777
|
+
});
|
|
778
|
+
return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Get the full config for persistence
|
|
782
|
+
*/
|
|
783
|
+
getConfig() {
|
|
784
|
+
return { ...this.config, baseSeed: this.seed };
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
function createSeedManager(config) {
|
|
788
|
+
return new SeedManager(config);
|
|
789
|
+
}
|
|
790
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
791
|
+
0 && (module.exports = {
|
|
792
|
+
BenchmarkMCPServer,
|
|
793
|
+
SeedManager,
|
|
794
|
+
createMCPServer,
|
|
795
|
+
createSeedManager,
|
|
796
|
+
normalizeReportData
|
|
797
|
+
});
|