ccs-mcp-server 1.0.0 → 1.1.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/package.json +7 -3
- package/src/index.js +131 -1
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccs-mcp-server",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CCS Runtime Evidence MCP Server — verify AI agent tool calls, issue tamper-evident evidence, audit MCP configs. For Claude Desktop, Cursor, Windsurf, and any MCP client.",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "CCS Runtime Evidence MCP Server — verify AI agent tool calls, issue tamper-evident evidence, audit MCP configs, and bind execution to declared intent (cross-model amount drift protection). For Claude Desktop, Cursor, Windsurf, and any MCP client.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ccs-mcp-server": "src/index.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "src/index.js",
|
|
10
|
-
"files": [
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
11
15
|
"keywords": [
|
|
12
16
|
"mcp",
|
|
13
17
|
"model-context-protocol",
|
package/src/index.js
CHANGED
|
@@ -31,7 +31,7 @@ const readline = require("readline");
|
|
|
31
31
|
// CCS Verifier Core (pure stdlib Node.js)
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
33
|
|
|
34
|
-
const CCS_VERSION = "1.
|
|
34
|
+
const CCS_VERSION = "1.1.0";
|
|
35
35
|
|
|
36
36
|
const DEFAULT_POLICY = {
|
|
37
37
|
mode: "block",
|
|
@@ -307,6 +307,82 @@ function auditMcpConfig(config) {
|
|
|
307
307
|
issues.length ? "medium" : "low" };
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Intent Binding (L4) — zero-LLM, deterministic, microsecond
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// Agent framework declares structured intent before tool execution.
|
|
314
|
+
// CCS verifies actual args match declared intent with zero tolerance.
|
|
315
|
+
// This catches cross-model amount drift (planner says 100, executor writes 10000)
|
|
316
|
+
// without any NLP or LLM call.
|
|
317
|
+
|
|
318
|
+
function deepEqual(a, b) {
|
|
319
|
+
if (a === b) return true;
|
|
320
|
+
if (typeof a !== typeof b) {
|
|
321
|
+
// numeric normalization: 100 == 100.0 == 1e2
|
|
322
|
+
if (typeof a === "number" && typeof b === "string") {
|
|
323
|
+
const n = Number(b);
|
|
324
|
+
return Number.isFinite(n) && a === n;
|
|
325
|
+
}
|
|
326
|
+
if (typeof b === "number" && typeof a === "string") {
|
|
327
|
+
const n = Number(a);
|
|
328
|
+
return Number.isFinite(n) && b === n;
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (typeof a !== "object" || a === null || b === null) return false;
|
|
333
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
334
|
+
const ka = Object.keys(a), kb = Object.keys(b);
|
|
335
|
+
if (ka.length !== kb.length) return false;
|
|
336
|
+
return ka.every((k) => deepEqual(a[k], b[k]));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function verifyIntentBinding(intent, toolName, toolArgs, now = Date.now()) {
|
|
340
|
+
// 1. TTL check
|
|
341
|
+
if (intent.ttl_ms != null && now - intent.issued_at > intent.ttl_ms) {
|
|
342
|
+
return {
|
|
343
|
+
allowed: false,
|
|
344
|
+
reason: { type: "intent_expired", expected: intent.issued_at + intent.ttl_ms, actual: now },
|
|
345
|
+
intent_hash: crypto.createHash("sha256").update(canonical(intent)).digest("hex"),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 2. Field-by-field comparison
|
|
350
|
+
const fields = intent.fields || {};
|
|
351
|
+
for (const [field, spec] of Object.entries(fields)) {
|
|
352
|
+
const actual = toolArgs[field];
|
|
353
|
+
const expected = spec.value;
|
|
354
|
+
const mode = spec.binding_mode || "exact";
|
|
355
|
+
let match = false;
|
|
356
|
+
if (mode === "exact") {
|
|
357
|
+
match = deepEqual(expected, actual);
|
|
358
|
+
} else if (mode === "numeric_tolerance") {
|
|
359
|
+
if (typeof expected === "number" && typeof actual === "number") {
|
|
360
|
+
match = Math.abs(expected - actual) <= (spec.tolerance || 0);
|
|
361
|
+
}
|
|
362
|
+
} else if (mode === "pattern") {
|
|
363
|
+
if (typeof actual === "string") {
|
|
364
|
+
try { match = new RegExp(spec.pattern).test(actual); } catch { match = false; }
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (!match) {
|
|
368
|
+
return {
|
|
369
|
+
allowed: false,
|
|
370
|
+
reason: {
|
|
371
|
+
type: "intent_arg_mismatch",
|
|
372
|
+
field, binding_mode: mode,
|
|
373
|
+
expected, actual,
|
|
374
|
+
},
|
|
375
|
+
intent_hash: crypto.createHash("sha256").update(canonical(intent)).digest("hex"),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
allowed: true,
|
|
382
|
+
intent_hash: crypto.createHash("sha256").update(canonical(intent)).digest("hex"),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
310
386
|
// ---------------------------------------------------------------------------
|
|
311
387
|
// MCP Protocol (stdio JSON-RPC 2.0)
|
|
312
388
|
// ---------------------------------------------------------------------------
|
|
@@ -357,6 +433,44 @@ const TOOLS = [
|
|
|
357
433
|
required: ["config"],
|
|
358
434
|
},
|
|
359
435
|
},
|
|
436
|
+
{
|
|
437
|
+
name: "verify_intent_binding",
|
|
438
|
+
description: "Verify that actual tool call arguments match a declared intent with zero tolerance. Catches cross-model parameter drift (planner says amount=100, executor writes amount=10000). No LLM needed — pure deterministic comparison. Supports exact match, numeric tolerance, and regex pattern binding modes.",
|
|
439
|
+
inputSchema: {
|
|
440
|
+
type: "object",
|
|
441
|
+
properties: {
|
|
442
|
+
intent: {
|
|
443
|
+
type: "object",
|
|
444
|
+
description: "Structured intent declaration from the agent framework",
|
|
445
|
+
properties: {
|
|
446
|
+
intent_id: { type: "string", description: "Unique intent identifier (UUID v7 recommended)" },
|
|
447
|
+
intent_type: { type: "string", description: "e.g. payment, file_write, api_call" },
|
|
448
|
+
fields: {
|
|
449
|
+
type: "object",
|
|
450
|
+
description: "Map of field name to binding specification",
|
|
451
|
+
additionalProperties: {
|
|
452
|
+
type: "object",
|
|
453
|
+
properties: {
|
|
454
|
+
value: { description: "Expected value" },
|
|
455
|
+
binding_mode: { type: "string", enum: ["exact", "numeric_tolerance", "pattern"] },
|
|
456
|
+
tolerance: { type: "number", description: "Tolerance for numeric_tolerance mode" },
|
|
457
|
+
pattern: { type: "string", description: "Regex for pattern mode" },
|
|
458
|
+
},
|
|
459
|
+
required: ["value"],
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
issued_at: { type: "integer", description: "Unix ms when intent was issued" },
|
|
463
|
+
ttl_ms: { type: "integer", description: "Time-to-live in ms (default 30000)" },
|
|
464
|
+
evidence_ref: { type: "string", description: "Optional hash of external evidence (invoice, authorization VC)" },
|
|
465
|
+
},
|
|
466
|
+
required: ["intent_id", "intent_type", "fields", "issued_at"],
|
|
467
|
+
},
|
|
468
|
+
tool: { type: "string", description: "Tool being called, e.g. payments.send" },
|
|
469
|
+
arguments: { type: "object", description: "Actual tool call arguments", additionalProperties: true },
|
|
470
|
+
},
|
|
471
|
+
required: ["intent", "tool", "arguments"],
|
|
472
|
+
},
|
|
473
|
+
},
|
|
360
474
|
];
|
|
361
475
|
|
|
362
476
|
function handleRequest(req) {
|
|
@@ -396,6 +510,22 @@ function handleRequest(req) {
|
|
|
396
510
|
} else if (name === "audit_mcp_config") {
|
|
397
511
|
const report = auditMcpConfig(args.config || {});
|
|
398
512
|
result = { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
|
|
513
|
+
} else if (name === "verify_intent_binding") {
|
|
514
|
+
const verdict = verifyIntentBinding(args.intent, args.tool, args.arguments || {});
|
|
515
|
+
result = {
|
|
516
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
517
|
+
allowed: verdict.allowed,
|
|
518
|
+
intent_id: args.intent.intent_id,
|
|
519
|
+
intent_hash: verdict.intent_hash,
|
|
520
|
+
tool: args.tool,
|
|
521
|
+
...(verdict.reason ? { reason: verdict.reason } : {}),
|
|
522
|
+
blocked: !verdict.allowed,
|
|
523
|
+
message: verdict.allowed
|
|
524
|
+
? "Intent binding verified — actual arguments match declared intent"
|
|
525
|
+
: `Intent binding DENIED: ${verdict.reason?.type}`,
|
|
526
|
+
}, null, 2) }],
|
|
527
|
+
isError: !verdict.allowed,
|
|
528
|
+
};
|
|
399
529
|
} else {
|
|
400
530
|
result = { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
401
531
|
}
|