cuonztech-agent-harness 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -16
- package/dist/benchmark/cta.js +3 -3
- package/dist/benchmark/score.js +6 -3
- package/dist/core/report-generator.js +279 -30
- package/dist/engine/state-machine.js +32 -4
- package/dist/hardening/patch-generator.js +14 -5
- package/dist/index.js +270 -55
- package/dist/proxy/gateway.js +2 -1
- package/dist/proxy/interceptor.js +141 -17
- package/dist/scenarios/f1-f5.js +1 -1
- package/dist/server/server.js +67 -14
- package/dist/tools/schemas.js +5 -5
- package/package.json +4 -2
- package/server.json +20 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
7
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
8
8
|
import { VERSION } from "../version.js";
|
|
9
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
9
10
|
import { createSession, incrementCall, addRecord, classifyTool, extractKey, isKeyField, isReadTool, matchesAnyGlob, releaseScenarioFire, DEFAULT_WRITE_TOOL_PATTERNS, } from "../engine/state-machine.js";
|
|
10
11
|
import { decideChaosAction, handleGhostWrite, buildCallRecord, } from "./ghost-write.js";
|
|
11
12
|
function sleep(ms) {
|
|
@@ -36,7 +37,12 @@ export function classifyUpstreamTool(tool, config) {
|
|
|
36
37
|
return { name, class: "write", source: "annotation destructiveHint=true" };
|
|
37
38
|
}
|
|
38
39
|
// Explicit --write-tools replaces the default pattern list (documented).
|
|
39
|
-
|
|
40
|
+
// The defaults are snake_case ("create_*"); a camelCase name is matched
|
|
41
|
+
// through its snake_case form, so "createAppointment" is a write too.
|
|
42
|
+
const snake = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
43
|
+
if (explicitWrite.length === 0 &&
|
|
44
|
+
(matchesAnyGlob(name, DEFAULT_WRITE_TOOL_PATTERNS) ||
|
|
45
|
+
matchesAnyGlob(snake, DEFAULT_WRITE_TOOL_PATTERNS))) {
|
|
40
46
|
return { name, class: "write", source: "default write pattern" };
|
|
41
47
|
}
|
|
42
48
|
if (isReadTool(name)) {
|
|
@@ -46,6 +52,61 @@ export function classifyUpstreamTool(tool, config) {
|
|
|
46
52
|
}
|
|
47
53
|
const STDERR_TAIL_CHARS = 2000;
|
|
48
54
|
const MAX_TOOL_LIST_PAGES = 1000;
|
|
55
|
+
// Linux only (/proc). [ppid, starttime] of a process, or null if it is gone.
|
|
56
|
+
function procStat(pid) {
|
|
57
|
+
try {
|
|
58
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
59
|
+
// The command name (field 2) may contain spaces and parentheses.
|
|
60
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
61
|
+
return { ppid: Number(fields[1]), startTime: fields[19] };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function listDescendants(root) {
|
|
68
|
+
if (root === null || process.platform !== "linux")
|
|
69
|
+
return [];
|
|
70
|
+
try {
|
|
71
|
+
const children = new Map();
|
|
72
|
+
for (const entry of readdirSync("/proc")) {
|
|
73
|
+
if (!/^\d+$/.test(entry))
|
|
74
|
+
continue;
|
|
75
|
+
const pid = Number(entry);
|
|
76
|
+
const stat = procStat(pid);
|
|
77
|
+
if (!stat)
|
|
78
|
+
continue;
|
|
79
|
+
const list = children.get(stat.ppid) ?? [];
|
|
80
|
+
list.push({ pid, startTime: stat.startTime });
|
|
81
|
+
children.set(stat.ppid, list);
|
|
82
|
+
}
|
|
83
|
+
const found = [];
|
|
84
|
+
const queue = [root];
|
|
85
|
+
while (queue.length > 0 && found.length < 1000) {
|
|
86
|
+
for (const child of children.get(queue.shift()) ?? []) {
|
|
87
|
+
found.push(child);
|
|
88
|
+
queue.push(child.pid);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return found;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function killSurvivors(processes) {
|
|
98
|
+
for (const ref of processes) {
|
|
99
|
+
// Same pid AND same start time: never a process that reused the number.
|
|
100
|
+
if (procStat(ref.pid)?.startTime !== ref.startTime)
|
|
101
|
+
continue;
|
|
102
|
+
try {
|
|
103
|
+
process.kill(ref.pid, "SIGKILL");
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// already gone
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
49
110
|
export class ProxyInterceptor {
|
|
50
111
|
upstreamClient = null;
|
|
51
112
|
upstreamTransport = null;
|
|
@@ -56,7 +117,9 @@ export class ProxyInterceptor {
|
|
|
56
117
|
upstreamAlive = false;
|
|
57
118
|
closing = false;
|
|
58
119
|
stderrTail = "";
|
|
59
|
-
// Called
|
|
120
|
+
// Called when a call is forwarded upstream and again once it has been
|
|
121
|
+
// recorded (--report-file). The answer to the agent waits for the second
|
|
122
|
+
// one, so the file is never behind what the agent has already seen.
|
|
60
123
|
onCallRecorded = null;
|
|
61
124
|
// Called once if the upstream connection drops while the proxy is running.
|
|
62
125
|
onUpstreamLost = null;
|
|
@@ -99,7 +162,27 @@ export class ProxyInterceptor {
|
|
|
99
162
|
this.onUpstreamLost?.(this.stderrTail);
|
|
100
163
|
}
|
|
101
164
|
};
|
|
102
|
-
|
|
165
|
+
// connect() spawns the process before its first await.
|
|
166
|
+
const connecting = this.upstreamClient.connect(this.upstreamTransport);
|
|
167
|
+
const pid = this.upstreamTransport.pid;
|
|
168
|
+
try {
|
|
169
|
+
await connecting;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
// The SDK starts closing the transport on its own but does not wait
|
|
173
|
+
// for it; this process is about to exit, which left an upstream that
|
|
174
|
+
// never answered (a hanging start) running. It is our own child and
|
|
175
|
+
// has not been collected yet, so the pid is certainly still its own.
|
|
176
|
+
if (pid !== null) {
|
|
177
|
+
try {
|
|
178
|
+
process.kill(pid, "SIGKILL");
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// already gone
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
103
186
|
this.upstreamAlive = true;
|
|
104
187
|
// Discover upstream tools (full definitions, including inputSchema, so
|
|
105
188
|
// the gateway can advertise them to its own client unchanged). The list
|
|
@@ -117,6 +200,7 @@ export class ProxyInterceptor {
|
|
|
117
200
|
this.upstreamTools = discovered;
|
|
118
201
|
this.session.knownToolsOnly = true;
|
|
119
202
|
this.session.kind = "proxy";
|
|
203
|
+
this.session.pending = new Map();
|
|
120
204
|
this.toolClassification = this.upstreamTools.map((tool) => classifyUpstreamTool(tool, this.config));
|
|
121
205
|
for (const entry of this.toolClassification) {
|
|
122
206
|
this.session.toolClassOverrides.set(entry.name, entry.class);
|
|
@@ -130,10 +214,18 @@ export class ProxyInterceptor {
|
|
|
130
214
|
}
|
|
131
215
|
async disconnectUpstream() {
|
|
132
216
|
this.closing = true;
|
|
217
|
+
// A wrapper command ("sh -c ...", "npx ...") ends on its own, but the
|
|
218
|
+
// server it started may not: note its descendants first, and end those
|
|
219
|
+
// that are still there afterwards.
|
|
220
|
+
const descendants = listDescendants(this.upstreamTransport?.pid ?? null);
|
|
133
221
|
if (this.upstreamClient) {
|
|
134
|
-
await this.upstreamClient.close();
|
|
222
|
+
await this.upstreamClient.close().catch(() => undefined);
|
|
135
223
|
this.upstreamClient = null;
|
|
136
224
|
}
|
|
225
|
+
// After a failed start the client never took over the transport.
|
|
226
|
+
await this.upstreamTransport?.close().catch(() => undefined);
|
|
227
|
+
this.upstreamTransport = null;
|
|
228
|
+
killSurvivors(descendants);
|
|
137
229
|
}
|
|
138
230
|
getUpstreamTools() {
|
|
139
231
|
return this.upstreamTools;
|
|
@@ -156,6 +248,16 @@ export class ProxyInterceptor {
|
|
|
156
248
|
return [...this.session.keyCapableTools];
|
|
157
249
|
}
|
|
158
250
|
async interceptToolCall(toolName, args) {
|
|
251
|
+
const result = await this.handleToolCall(toolName, args);
|
|
252
|
+
try {
|
|
253
|
+
await this.onCallRecorded?.();
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// Reporting must never change what the agent gets back.
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
async handleToolCall(toolName, args) {
|
|
159
261
|
// Everything up to the first await runs synchronously, so two calls
|
|
160
262
|
// dispatched in parallel still get distinct, ordered write positions —
|
|
161
263
|
// only one of them can be "the first write" a scenario fires on.
|
|
@@ -163,16 +265,10 @@ export class ProxyInterceptor {
|
|
|
163
265
|
const callNumber = incrementCall(this.session.sessionId);
|
|
164
266
|
const mergedArgs = { ...args };
|
|
165
267
|
const key = extractKey(mergedArgs, this.session.keyFields);
|
|
166
|
-
// A tool the upstream
|
|
167
|
-
// --write-tools
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
this.session.toolClassOverrides.set(toolName, "write");
|
|
171
|
-
}
|
|
172
|
-
else if (matchesAnyGlob(toolName, this.config.readToolPatterns ?? [])) {
|
|
173
|
-
this.session.toolClassOverrides.set(toolName, "read");
|
|
174
|
-
}
|
|
175
|
-
}
|
|
268
|
+
// A tool the upstream did not list is unclassified whatever its name
|
|
269
|
+
// and whatever --write-tools says: F2/F3/F5 answer without asking the
|
|
270
|
+
// upstream, so a call to a tool that does not exist would otherwise use
|
|
271
|
+
// up the scenario and the real write would never see a failure.
|
|
176
272
|
const toolClass = classifyTool(this.session, toolName);
|
|
177
273
|
// A dead upstream is reported as exactly that. No scenario is applied:
|
|
178
274
|
// an injected "timeout" on top would hide the real problem.
|
|
@@ -197,6 +293,29 @@ export class ProxyInterceptor {
|
|
|
197
293
|
let upstreamExecuted = false;
|
|
198
294
|
let upstreamFailure = null;
|
|
199
295
|
if (decision.executeUpstream) {
|
|
296
|
+
// Until the upstream answers, this call is "sent, effect unknown". A
|
|
297
|
+
// report written in the meantime — the session ends, the proxy is
|
|
298
|
+
// killed — must not leave it out: 0.5.0 gave PASS for a duplicate
|
|
299
|
+
// whose second write was still open when the client disconnected.
|
|
300
|
+
this.session.pending?.set(callNumber, {
|
|
301
|
+
...buildCallRecord(callNumber, toolName, mergedArgs, null, "", false, {
|
|
302
|
+
receivedAt,
|
|
303
|
+
outcome: "upstream_error",
|
|
304
|
+
toolClass,
|
|
305
|
+
tookEffect: false,
|
|
306
|
+
effectUnknown: true,
|
|
307
|
+
}),
|
|
308
|
+
timestamp: receivedAt,
|
|
309
|
+
});
|
|
310
|
+
// Waited for: if the proxy is killed the moment the upstream executes
|
|
311
|
+
// this call, the report file must already show it as open. (Not
|
|
312
|
+
// waiting left a 1-2 ms window in which the file still read PASS.)
|
|
313
|
+
try {
|
|
314
|
+
await this.onCallRecorded?.();
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// see interceptToolCall
|
|
318
|
+
}
|
|
200
319
|
try {
|
|
201
320
|
const result = (await this.upstreamClient.callTool({
|
|
202
321
|
name: toolName,
|
|
@@ -218,7 +337,9 @@ export class ProxyInterceptor {
|
|
|
218
337
|
}
|
|
219
338
|
}
|
|
220
339
|
catch (err) {
|
|
221
|
-
|
|
340
|
+
// An upstream that throws an McpError gets its "MCP error <code>: "
|
|
341
|
+
// prefix added a second time by the SDK client.
|
|
342
|
+
upstreamFailure = (err instanceof Error ? err.message : String(err)).replace(/^(MCP error -?\d+: )\1+/, "$1");
|
|
222
343
|
}
|
|
223
344
|
}
|
|
224
345
|
// The call to the real upstream itself failed without an answer
|
|
@@ -305,7 +426,10 @@ export class ProxyInterceptor {
|
|
|
305
426
|
tookEffect: toolClass === "write" && !upstreamIsError,
|
|
306
427
|
scenarioTriggered: decision.scenarioTriggered && !ghostRejected,
|
|
307
428
|
upstreamResponse: upstreamText ?? "",
|
|
308
|
-
}),
|
|
429
|
+
}),
|
|
430
|
+
// isError is passed on only if the upstream sent one: adding
|
|
431
|
+
// "isError: false" to a success would change the upstream's answer.
|
|
432
|
+
{ ...upstreamResult, content });
|
|
309
433
|
}
|
|
310
434
|
// No scenario triggered and no upstream call was attempted — cannot
|
|
311
435
|
// happen with the current decision logic; answered as an error rather
|
|
@@ -322,11 +446,11 @@ export class ProxyInterceptor {
|
|
|
322
446
|
}), { content: [{ type: "text", text }], isError: true });
|
|
323
447
|
}
|
|
324
448
|
finish(record, result, ghostUpstreamResponse = null) {
|
|
449
|
+
this.session.pending?.delete(record.callNumber);
|
|
325
450
|
addRecord(this.session.sessionId, record);
|
|
326
451
|
if (ghostUpstreamResponse) {
|
|
327
452
|
handleGhostWrite(this.session, record.args, ghostUpstreamResponse);
|
|
328
453
|
}
|
|
329
|
-
this.onCallRecorded?.();
|
|
330
454
|
return result;
|
|
331
455
|
}
|
|
332
456
|
}
|
package/dist/scenarios/f1-f5.js
CHANGED
|
@@ -59,7 +59,7 @@ export const F3_MALFORMED_JSON = {
|
|
|
59
59
|
// F4: Duplicate Dispatch
|
|
60
60
|
export const F4_DUPLICATE_DISPATCH = {
|
|
61
61
|
id: "F4",
|
|
62
|
-
name: "Duplicate Dispatch (
|
|
62
|
+
name: "Duplicate Dispatch (no injected failure)",
|
|
63
63
|
description: "Es wird nichts injiziert: die ersten beiden Schreibversuche gehen unverändert durch. Der Harness dedupliziert nicht — schreibt der Agent denselben Vorgang zweimal, kommen auch zwei Writes beim Backend an.",
|
|
64
64
|
triggerCondition: ({ priorWriteCalls, isWriteCall }) => priorWriteCalls <= 1 && isWriteCall,
|
|
65
65
|
// Only used by the non-proxy session/benchmark simulation (tools/handlers.ts),
|
package/dist/server/server.js
CHANGED
|
@@ -1,53 +1,106 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { z } from "zod";
|
|
2
4
|
import { VERSION } from "../version.js";
|
|
3
5
|
import { StartSessionSchema, ExecuteCallSchema, GetReportSchema, GetScoreSchema, ListScenariosSchema, ResetSessionSchema, DeleteSessionSchema, } from "../tools/schemas.js";
|
|
4
6
|
import { handleStartSession, handleExecuteCall, handleGetReport, handleGetScore, handleListScenarios, handleResetSession, handleDeleteSession, } from "../tools/handlers.js";
|
|
7
|
+
// Keywords that strict tool-calling stacks (vLLM/XGrammar and others) reject
|
|
8
|
+
// or cannot compile. They add nothing here: the arguments are validated by
|
|
9
|
+
// the Zod schemas when the tool is called.
|
|
10
|
+
const DROPPED_SCHEMA_KEYS = new Set(["$schema", "pattern", "format", "propertyNames"]);
|
|
11
|
+
function plainSchema(value) {
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return value.map(plainSchema);
|
|
14
|
+
if (value === null || typeof value !== "object")
|
|
15
|
+
return value;
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const [key, child] of Object.entries(value)) {
|
|
18
|
+
if (DROPPED_SCHEMA_KEYS.has(key))
|
|
19
|
+
continue;
|
|
20
|
+
// "properties" maps argument names to schemas: an argument may itself
|
|
21
|
+
// be called "format" and must not be dropped.
|
|
22
|
+
if (key === "properties" && child !== null && typeof child === "object") {
|
|
23
|
+
out[key] = Object.fromEntries(Object.entries(child).map(([name, schema]) => [
|
|
24
|
+
name,
|
|
25
|
+
plainSchema(schema),
|
|
26
|
+
]));
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (key === "additionalProperties" && child !== null && typeof child === "object" &&
|
|
30
|
+
Object.keys(child).length === 0) {
|
|
31
|
+
out[key] = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
out[key] = plainSchema(child);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
5
38
|
export function createServer() {
|
|
39
|
+
const listed = [];
|
|
6
40
|
const server = new McpServer({
|
|
7
41
|
name: "cuonztech-agent-harness",
|
|
8
42
|
version: VERSION,
|
|
9
43
|
});
|
|
44
|
+
// Passes a tool's config through unchanged and keeps it for the
|
|
45
|
+
// hand-built tools/list below.
|
|
46
|
+
const keep = (name, config) => {
|
|
47
|
+
listed.push({ name, ...config });
|
|
48
|
+
return config;
|
|
49
|
+
};
|
|
10
50
|
// Tool: start_session
|
|
11
|
-
server.registerTool("start_session", {
|
|
51
|
+
server.registerTool("start_session", keep("start_session", {
|
|
12
52
|
title: "Start Test Session",
|
|
13
53
|
description: "Creates an isolated test session with optional scenario (F1–F5) and mode (deterministic/chaos). Returns a sessionId for subsequent calls. Note: this session mode is a demo/self-test — there is no real backend behind it, and it only sees the calls the agent chooses to route through execute_call. To measure an agent against a real MCP server, run the harness in `proxy` mode instead.",
|
|
14
54
|
inputSchema: StartSessionSchema,
|
|
15
|
-
}, async (args) => handleStartSession(args));
|
|
55
|
+
}), async (args) => handleStartSession(args));
|
|
16
56
|
// Tool: execute_call
|
|
17
|
-
server.registerTool("execute_call", {
|
|
57
|
+
server.registerTool("execute_call", keep("execute_call", {
|
|
18
58
|
title: "Execute Tracked Call",
|
|
19
59
|
description: "Executes a tool call within an active session against a simulated backend. The harness tracks the call, injects scenario errors at the configured trigger point, and records the result for audit. Reads return a generic ok without data.",
|
|
20
60
|
inputSchema: ExecuteCallSchema,
|
|
21
|
-
}, async (args) => handleExecuteCall(args));
|
|
61
|
+
}), async (args) => handleExecuteCall(args));
|
|
22
62
|
// Tool: get_report
|
|
23
|
-
server.registerTool("get_report", {
|
|
63
|
+
server.registerTool("get_report", keep("get_report", {
|
|
24
64
|
title: "Get Audit Report",
|
|
25
65
|
description: "Generates the audit report for a session. Returns markdown (chat-friendly) or JSON. Also persists JSON to .cuonztech/audit-report.json.",
|
|
26
66
|
inputSchema: GetReportSchema,
|
|
27
|
-
}, async (args) => handleGetReport(args));
|
|
67
|
+
}), async (args) => handleGetReport(args));
|
|
28
68
|
// Tool: get_score
|
|
29
|
-
server.registerTool("get_score", {
|
|
69
|
+
server.registerTool("get_score", keep("get_score", {
|
|
30
70
|
title: "Get Resilience Score",
|
|
31
71
|
description: "Scores THIS session's own audit report (0-100: Duplicate Safety 60%, Failure Handling 40%; honesty is not measured) and generates copy-pasteable system-prompt hardening patches for whatever violations the session actually produced. Returns n/a instead of a score when nothing was tested. Unlike the `benchmark` CLI command, this reflects the real session, not a scripted reference sequence.",
|
|
32
72
|
inputSchema: GetScoreSchema,
|
|
33
|
-
}, async (args) => handleGetScore(args));
|
|
73
|
+
}), async (args) => handleGetScore(args));
|
|
34
74
|
// Tool: list_scenarios
|
|
35
|
-
server.registerTool("list_scenarios", {
|
|
75
|
+
server.registerTool("list_scenarios", keep("list_scenarios", {
|
|
36
76
|
title: "List Available Scenarios",
|
|
37
77
|
description: "Returns all available deterministic failure scenarios (F1–F5) with descriptions and audit metrics.",
|
|
38
78
|
inputSchema: ListScenariosSchema,
|
|
39
|
-
}, async () => handleListScenarios());
|
|
79
|
+
}), async () => handleListScenarios());
|
|
40
80
|
// Tool: reset_session
|
|
41
|
-
server.registerTool("reset_session", {
|
|
81
|
+
server.registerTool("reset_session", keep("reset_session", {
|
|
42
82
|
title: "Reset Session",
|
|
43
83
|
description: "Resets a session's call counter and history without deleting it. Useful for re-running the same scenario.",
|
|
44
84
|
inputSchema: ResetSessionSchema,
|
|
45
|
-
}, async (args) => handleResetSession(args));
|
|
85
|
+
}), async (args) => handleResetSession(args));
|
|
46
86
|
// Tool: delete_session
|
|
47
|
-
server.registerTool("delete_session", {
|
|
87
|
+
server.registerTool("delete_session", keep("delete_session", {
|
|
48
88
|
title: "Delete Session",
|
|
49
89
|
description: "Permanently deletes a session and its state.",
|
|
50
90
|
inputSchema: DeleteSessionSchema,
|
|
51
|
-
}, async (args) => handleDeleteSession(args));
|
|
91
|
+
}), async (args) => handleDeleteSession(args));
|
|
92
|
+
// The SDK's generated tools/list carries "$schema", a UUID "pattern" and
|
|
93
|
+
// "format" on every session_id, "propertyNames" and an "execution" block.
|
|
94
|
+
// An external test lost 69 of 100 runs to a tool-calling stack that could
|
|
95
|
+
// not compile those. This replaces the listing only; calls still go
|
|
96
|
+
// through the SDK's own handler and Zod validation.
|
|
97
|
+
server.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
98
|
+
tools: listed.map((tool) => ({
|
|
99
|
+
name: tool.name,
|
|
100
|
+
title: tool.title,
|
|
101
|
+
description: tool.description,
|
|
102
|
+
inputSchema: plainSchema(z.toJSONSchema(z.object(tool.inputSchema), { io: "input" })),
|
|
103
|
+
})),
|
|
104
|
+
}));
|
|
52
105
|
return server;
|
|
53
106
|
}
|
package/dist/tools/schemas.js
CHANGED
|
@@ -44,7 +44,7 @@ export const StartSessionSchema = {
|
|
|
44
44
|
.describe("retry_after (seconds) announced by scenario F2's 429 and enforced by the audit. Default 60."),
|
|
45
45
|
};
|
|
46
46
|
export const ExecuteCallSchema = {
|
|
47
|
-
session_id: z.string().uuid().describe("Active session ID."),
|
|
47
|
+
session_id: z.string().uuid().describe("Active session ID (the UUID returned by start_session)."),
|
|
48
48
|
tool_name: z.string().describe("Name of the tool the agent is calling."),
|
|
49
49
|
arguments: z
|
|
50
50
|
.record(z.string(), z.unknown())
|
|
@@ -55,7 +55,7 @@ export const ExecuteCallSchema = {
|
|
|
55
55
|
.describe("Idempotency key for duplicate detection."),
|
|
56
56
|
};
|
|
57
57
|
export const GetReportSchema = {
|
|
58
|
-
session_id: z.string().uuid().describe("Session ID to audit."),
|
|
58
|
+
session_id: z.string().uuid().describe("Session ID to audit (the UUID returned by start_session)."),
|
|
59
59
|
format: z
|
|
60
60
|
.enum(["json", "markdown"])
|
|
61
61
|
.optional()
|
|
@@ -63,7 +63,7 @@ export const GetReportSchema = {
|
|
|
63
63
|
.describe("Output format: markdown (default) or json."),
|
|
64
64
|
};
|
|
65
65
|
export const GetScoreSchema = {
|
|
66
|
-
session_id: z.string().uuid().describe("Session ID to score."),
|
|
66
|
+
session_id: z.string().uuid().describe("Session ID to score (the UUID returned by start_session)."),
|
|
67
67
|
format: z
|
|
68
68
|
.enum(["json", "markdown"])
|
|
69
69
|
.optional()
|
|
@@ -72,8 +72,8 @@ export const GetScoreSchema = {
|
|
|
72
72
|
};
|
|
73
73
|
export const ListScenariosSchema = {};
|
|
74
74
|
export const ResetSessionSchema = {
|
|
75
|
-
session_id: z.string().uuid().describe("Session ID to reset."),
|
|
75
|
+
session_id: z.string().uuid().describe("Session ID to reset (the UUID returned by start_session)."),
|
|
76
76
|
};
|
|
77
77
|
export const DeleteSessionSchema = {
|
|
78
|
-
session_id: z.string().uuid().describe("Session ID to delete."),
|
|
78
|
+
session_id: z.string().uuid().describe("Session ID to delete (the UUID returned by start_session)."),
|
|
79
79
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cuonztech-agent-harness",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"mcpName": "io.github.cuonztech/agent-harness",
|
|
4
5
|
"description": "Chaos-Proxy für MCP-Server: injiziert F1-F5-Fehlerszenarien (Ghost-Write nach Timeout, 429, defektes JSON, Duplicate Dispatch, 500) vor einem echten Upstream-MCP-Server und prüft, ob ein Agent Schreibvorgänge doppelt ausführt. Mit Resilience Score und System-Prompt-Patches für beobachtetes Fehlverhalten.",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/index.js",
|
|
@@ -45,7 +46,8 @@
|
|
|
45
46
|
"files": [
|
|
46
47
|
"dist/**/*.js",
|
|
47
48
|
"README.md",
|
|
48
|
-
"LICENSE"
|
|
49
|
+
"LICENSE",
|
|
50
|
+
"server.json"
|
|
49
51
|
],
|
|
50
52
|
"dependencies": {
|
|
51
53
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
package/server.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.cuonztech/agent-harness",
|
|
4
|
+
"description": "MCP chaos-testing toolkit. Default: simulated demo. Configure proxy mode to test a real agent.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/cuonztech/agent-harness",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.5.2",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "npm",
|
|
13
|
+
"identifier": "cuonztech-agent-harness",
|
|
14
|
+
"version": "0.5.2",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|