ccs-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -0
- package/package.json +42 -0
- package/src/index.js +426 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# CCS Runtime Evidence MCP Server
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that brings **CCS runtime verification** to any MCP-compatible client — Claude Desktop, Cursor, Windsurf, ChatGPT apps, and more.
|
|
4
|
+
|
|
5
|
+
It verifies AI agent tool calls at runtime, blocks unsafe ones by default, and issues **tamper-evident evidence records** for every call (allowed and denied).
|
|
6
|
+
|
|
7
|
+
> **This is a runtime enforcement layer, not a static scanner.** It runs on every live tool call — not once on source code.
|
|
8
|
+
|
|
9
|
+
## Tools
|
|
10
|
+
|
|
11
|
+
| Tool | What it does |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `verify_tool_call` | Verify a tool call across 7 CCS dimensions + semantic attack-chain analysis + math overflow detection. Returns `verdict: allowed/denied`. **Blocks by default.** |
|
|
14
|
+
| `issue_evidence` | Issue a cryptographically bound CCS evidence record (`content_hash` + `evidence_hash`) for a call. Independently verifiable. |
|
|
15
|
+
| `audit_mcp_config` | Audit an MCP configuration JSON for security risks: plain HTTP, weak secrets, disabled TLS, missing commands. |
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### Install and run with npx
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @correctover/ccs-mcp-server
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Claude Desktop / Cursor configuration
|
|
26
|
+
|
|
27
|
+
Add to your MCP client settings:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"ccs-runtime-evidence": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "@correctover/ccs-mcp-server"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## What it detects
|
|
41
|
+
|
|
42
|
+
- **Command injection**: shell metacharacters, `$()`, backticks, dangerous commands
|
|
43
|
+
- **Path traversal**: `../`, `/etc/passwd`, `/proc/self/`, Windows system paths
|
|
44
|
+
- **SSRF**: localhost, link-local (169.254.x.x), private IP ranges
|
|
45
|
+
- **Prompt injection**: "ignore previous instructions", system prompt extraction, special tokens
|
|
46
|
+
- **SQL injection**: UNION SELECT, `OR 1=1`, `DROP TABLE`
|
|
47
|
+
- **Attack chains**: multi-step combinations (e.g. path traversal + SSRF = exfiltration chain)
|
|
48
|
+
- **Math safety**: integer overflow (>2^53-1), NaN/Infinity, DoS strings
|
|
49
|
+
- **Caller identity**: agent ID verification against allowlist
|
|
50
|
+
- **Schema violations**: type, enum, range, required field checks
|
|
51
|
+
- **MCP config risks**: plain HTTP, weak secrets, `--insecure`, disabled TLS
|
|
52
|
+
|
|
53
|
+
## Evidence format
|
|
54
|
+
|
|
55
|
+
Every call produces evidence like:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"evidence_type": "ccs.tool_call.verification",
|
|
60
|
+
"evidence_id": "uuid-v4",
|
|
61
|
+
"tool": "shell.exec",
|
|
62
|
+
"caller": "agent-001",
|
|
63
|
+
"verdict": "denied",
|
|
64
|
+
"mode": "block",
|
|
65
|
+
"params_hash": "sha256:...",
|
|
66
|
+
"policy_hash": "sha256:...",
|
|
67
|
+
"content_hash": "sha256:...",
|
|
68
|
+
"evidence_hash": "sha256:...",
|
|
69
|
+
"dimensions": {
|
|
70
|
+
"Structure": {"status": "pass"},
|
|
71
|
+
"Schema": {"status": "pass"},
|
|
72
|
+
"Security": {"status": "fail", "reason": "cmd: command_injection; cmd: path_traversal; cmd: ssrf"},
|
|
73
|
+
"Identity": {"status": "pass"}
|
|
74
|
+
},
|
|
75
|
+
"semantic_analysis": {
|
|
76
|
+
"attack_chains": [{"chain": "exfil_chain", "severity": "critical"}],
|
|
77
|
+
"semantic_severity": "critical"
|
|
78
|
+
},
|
|
79
|
+
"issued_at": "2026-08-22T12:00:00.000Z"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The dual-hash design (`content_hash` + `evidence_hash`) means any tampering is detectable by any third party.
|
|
84
|
+
|
|
85
|
+
## About CCS
|
|
86
|
+
|
|
87
|
+
CCS (Correctover Conformance Shape) is a 7-dimension runtime verification standard for AI agent tool calls:
|
|
88
|
+
|
|
89
|
+
1. **Structure** — valid tool name, argument format, nesting depth, payload size
|
|
90
|
+
2. **Schema** — type, required fields, enums, ranges, string lengths
|
|
91
|
+
3. **Security** — injection, traversal, SSRF, prompt injection, SQL injection
|
|
92
|
+
4. **Identity** — caller agent ID verification
|
|
93
|
+
5. **Integrity** — request hash validation
|
|
94
|
+
6. **Latency** — execution time budget (warn)
|
|
95
|
+
7. **Cost** — cost budget (warn)
|
|
96
|
+
|
|
97
|
+
Plus: semantic intent analysis (attack chains, obfuscation signals, privilege escalation) and mathematical verification (overflow, NaN, DoS).
|
|
98
|
+
|
|
99
|
+
## Links
|
|
100
|
+
|
|
101
|
+
- npm: https://www.npmjs.com/package/@correctover/ccs-mcp-server
|
|
102
|
+
- Website: https://correctover.com
|
|
103
|
+
- IETF Draft: https://www.ietf.org/archive/id/draft-correctover-ccs-05.txt
|
|
104
|
+
- SkillHub: https://skillhub.cn
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
Proprietary Commercial License. Reference implementation for CCS standard evaluation.
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ccs-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
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.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ccs-mcp-server": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/index.js",
|
|
10
|
+
"files": ["src", "README.md", "LICENSE"],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"mcp",
|
|
13
|
+
"model-context-protocol",
|
|
14
|
+
"mcp-server",
|
|
15
|
+
"mcp-security",
|
|
16
|
+
"ai-agent-security",
|
|
17
|
+
"runtime-verification",
|
|
18
|
+
"ccs",
|
|
19
|
+
"correctover",
|
|
20
|
+
"audit",
|
|
21
|
+
"evidence",
|
|
22
|
+
"supply-chain-security",
|
|
23
|
+
"agent-security",
|
|
24
|
+
"security",
|
|
25
|
+
"developer-tools"
|
|
26
|
+
],
|
|
27
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/Correctover/ccs-mcp-server.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://correctover.com",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {},
|
|
37
|
+
"mcp": {
|
|
38
|
+
"name": "ccs-runtime-evidence",
|
|
39
|
+
"displayName": "CCS Runtime Evidence",
|
|
40
|
+
"description": "Verify AI agent tool calls and issue tamper-evident CCS evidence records. Blocks command injection, path traversal, SSRF, SQL injection; validates caller identity, schema, cost/latency budgets; produces cryptographically bound evidence."
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CCS Runtime Evidence MCP Server v1.0.0
|
|
4
|
+
*
|
|
5
|
+
* A standalone Model Context Protocol (MCP) server that exposes CCS runtime
|
|
6
|
+
* verification as MCP tools, so any MCP-compatible client (Claude Desktop,
|
|
7
|
+
* Cursor, Windsurf, ChatGPT apps, etc.) can verify AI agent tool calls and
|
|
8
|
+
* obtain tamper-evident evidence records.
|
|
9
|
+
*
|
|
10
|
+
* Tools exposed:
|
|
11
|
+
* verify_tool_call Verify a tool call against CCS 7 dimensions + semantic + math
|
|
12
|
+
* issue_evidence Issue a signed CCS evidence record (allow + deny)
|
|
13
|
+
* audit_mcp_config Audit an MCP server configuration for security risks
|
|
14
|
+
*
|
|
15
|
+
* Transport: stdio (per MCP spec). To run:
|
|
16
|
+
* npx @correctover/ccs-mcp-server
|
|
17
|
+
*
|
|
18
|
+
* License: Proprietary Commercial License (reference implementation).
|
|
19
|
+
*
|
|
20
|
+
* This server is a self-contained JavaScript implementation of the CCS
|
|
21
|
+
* runtime verification reference (mirroring the Python SkillHub distribution
|
|
22
|
+
* ccs-runtime-verifier). Zero runtime dependencies.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
"use strict";
|
|
26
|
+
|
|
27
|
+
const crypto = require("crypto");
|
|
28
|
+
const readline = require("readline");
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// CCS Verifier Core (pure stdlib Node.js)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const CCS_VERSION = "1.0.0";
|
|
35
|
+
|
|
36
|
+
const DEFAULT_POLICY = {
|
|
37
|
+
mode: "block",
|
|
38
|
+
max_nesting_depth: 8,
|
|
39
|
+
max_argument_bytes: 65536,
|
|
40
|
+
max_string_length: 16384,
|
|
41
|
+
latency_budget_us: 5000000,
|
|
42
|
+
cost_budget: 0.05,
|
|
43
|
+
allowed_tools: [],
|
|
44
|
+
denied_tools: [],
|
|
45
|
+
tool_schemas: {},
|
|
46
|
+
require_caller_identity: true,
|
|
47
|
+
require_request_hash: false,
|
|
48
|
+
allowed_callers: [],
|
|
49
|
+
enable_semantic_analysis: true,
|
|
50
|
+
enable_math_verification: true,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const INJECTION_PATTERNS = {
|
|
54
|
+
command_injection: [
|
|
55
|
+
/[;&|`$]\s*(rm|curl|wget|bash|sh|nc|cat|chmod|eval|exec)\b/i,
|
|
56
|
+
/\$\([^)]+\)/,
|
|
57
|
+
/`[^`]+`/,
|
|
58
|
+
/\|\|\s*\w+/,
|
|
59
|
+
],
|
|
60
|
+
path_traversal: [
|
|
61
|
+
/\.\.[\/\\]/,
|
|
62
|
+
/\/etc\/(passwd|shadow|hosts)/,
|
|
63
|
+
/\/proc\/self\//,
|
|
64
|
+
/\\windows\\system32/i,
|
|
65
|
+
],
|
|
66
|
+
ssrf: [
|
|
67
|
+
/https?:\/\/(127\.0\.0\.1|localhost|0\.0\.0\.0|169\.254\.169\.254)/i,
|
|
68
|
+
/https?:\/\/10\.\d+\.\d+\.\d+/,
|
|
69
|
+
/https?:\/\/192\.168\.\d+\.\d+/,
|
|
70
|
+
/https?:\/\/172\.(1[6-9]|2\d|3[01])\.\d+\.\d+/,
|
|
71
|
+
],
|
|
72
|
+
prompt_injection: [
|
|
73
|
+
/ignore\s+(all\s+)?(previous|above|prior)\s+(instructions?|prompts?)/i,
|
|
74
|
+
/disregard\s+(all\s+)?(previous|above)/i,
|
|
75
|
+
/you\s+are\s+now\s+(?:a|an|the)\s+/i,
|
|
76
|
+
/system\s*prompt/i,
|
|
77
|
+
/<\|im_start\|>|<\|im_end\|>/,
|
|
78
|
+
],
|
|
79
|
+
sql_injection: [
|
|
80
|
+
/(\bunion\b.*\bselect\b)/i,
|
|
81
|
+
/(\bor\b\s+1\s*=\s*1)/i,
|
|
82
|
+
/(;\s*drop\s+table)/i,
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const TOOL_RISK_PROFILES = {
|
|
87
|
+
shell: 0.9, exec: 0.9, system: 0.95,
|
|
88
|
+
filesystem: 0.6, file: 0.6,
|
|
89
|
+
network: 0.7, http: 0.7,
|
|
90
|
+
database: 0.75, sql: 0.8,
|
|
91
|
+
read: 0.2, math: 0.1,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const DANGEROUS_COMBOS = [
|
|
95
|
+
{ name: "exfil_chain", indicators: ["path_traversal", "ssrf"], severity: "critical" },
|
|
96
|
+
{ name: "rce_chain", indicators: ["command_injection", "path_traversal"], severity: "critical" },
|
|
97
|
+
{ name: "sql_exfil", indicators: ["sql_injection", "ssrf"], severity: "critical" },
|
|
98
|
+
{ name: "prompt_hijack", indicators: ["prompt_injection", "command_injection"], severity: "critical" },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
function deepMerge(base, override) {
|
|
102
|
+
const result = { ...base };
|
|
103
|
+
for (const [k, v] of Object.entries(override || {})) {
|
|
104
|
+
if (v && typeof v === "object" && !Array.isArray(v) && result[k] && typeof result[k] === "object") {
|
|
105
|
+
result[k] = deepMerge(result[k], v);
|
|
106
|
+
} else {
|
|
107
|
+
result[k] = v;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function canonical(obj) {
|
|
114
|
+
return JSON.stringify(obj, Object.keys(obj).sort());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sha256(s) {
|
|
118
|
+
return "sha256:" + crypto.createHash("sha256").update(s).digest("hex");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function scanValue(val, findings, path = "<root>") {
|
|
122
|
+
if (typeof val === "string") {
|
|
123
|
+
for (const [cat, pats] of Object.entries(INJECTION_PATTERNS)) {
|
|
124
|
+
for (const pat of pats) {
|
|
125
|
+
if (pat.test(val)) {
|
|
126
|
+
findings.push(`${path}: ${cat}`);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (val.length > 1e7) findings.push(`${path}: dos_string`);
|
|
132
|
+
} else if (typeof val === "number") {
|
|
133
|
+
if (Number.isNaN(val) || !Number.isFinite(val)) findings.push(`${path}: nan_or_infinity`);
|
|
134
|
+
else if (Math.abs(val) > Number.MAX_SAFE_INTEGER) findings.push(`${path}: integer_overflow`);
|
|
135
|
+
else if (Math.abs(val) > 1e9) findings.push(`${path}: unreasonable_magnitude`);
|
|
136
|
+
else if (val < 0 && !/offset|delta|diff|change/i.test(path)) findings.push(`${path}: negative_value`);
|
|
137
|
+
} else if (val && typeof val === "object") {
|
|
138
|
+
if (Array.isArray(val)) {
|
|
139
|
+
val.forEach((v, i) => scanValue(v, findings, `${path}[${i}]`));
|
|
140
|
+
} else {
|
|
141
|
+
for (const [k, v] of Object.entries(val)) scanValue(v, findings, path === "<root>" ? k : `${path}.${k}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function structureCheck(call, policy) {
|
|
147
|
+
const tool = call.tool;
|
|
148
|
+
if (!tool || typeof tool !== "string") return { ok: false, reason: "missing or invalid 'tool' field" };
|
|
149
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/.test(tool))
|
|
150
|
+
return { ok: false, reason: `tool name '${tool}' has invalid format` };
|
|
151
|
+
if (call.arguments !== undefined && typeof call.arguments !== "object")
|
|
152
|
+
return { ok: false, reason: "'arguments' must be an object or array" };
|
|
153
|
+
if (policy.denied_tools?.includes(tool)) return { ok: false, reason: `tool '${tool}' is denied` };
|
|
154
|
+
if (policy.allowed_tools?.length && !policy.allowed_tools.includes(tool))
|
|
155
|
+
return { ok: false, reason: `tool '${tool}' not in allowed list` };
|
|
156
|
+
const argBytes = Buffer.byteLength(JSON.stringify(call.arguments || {}));
|
|
157
|
+
if (argBytes > policy.max_argument_bytes)
|
|
158
|
+
return { ok: false, reason: `argument size ${argBytes} exceeds limit ${policy.max_argument_bytes}` };
|
|
159
|
+
return { ok: true };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function securityCheck(call) {
|
|
163
|
+
const findings = [];
|
|
164
|
+
scanValue(call.arguments || {}, findings);
|
|
165
|
+
if (findings.length) return { ok: false, reason: findings.slice(0, 5).join("; "), findings };
|
|
166
|
+
return { ok: true, findings: [] };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function identityCheck(call, policy) {
|
|
170
|
+
if (!policy.require_caller_identity) return { ok: true };
|
|
171
|
+
const caller = call.caller;
|
|
172
|
+
if (!caller || typeof caller !== "object") return { ok: false, reason: "missing 'caller' object" };
|
|
173
|
+
if (!caller.agent_id) return { ok: false, reason: "caller missing 'agent_id'" };
|
|
174
|
+
if (policy.allowed_callers?.length && !policy.allowed_callers.includes(caller.agent_id))
|
|
175
|
+
return { ok: false, reason: `caller '${caller.agent_id}' not in allowed_callers` };
|
|
176
|
+
return { ok: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function schemaCheck(call, policy) {
|
|
180
|
+
const schema = policy.tool_schemas?.[call.tool];
|
|
181
|
+
if (!schema || !call.arguments || typeof call.arguments !== "object") return { ok: true };
|
|
182
|
+
for (const [field, def] of Object.entries(schema.properties || {})) {
|
|
183
|
+
if (!(field in call.arguments)) {
|
|
184
|
+
if ((schema.required || []).includes(field))
|
|
185
|
+
return { ok: false, reason: `missing required field '${field}'` };
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const val = call.arguments[field];
|
|
189
|
+
if (def.type === "string" && typeof val !== "string") return { ok: false, reason: `field '${field}' must be string` };
|
|
190
|
+
if (def.type === "number" && typeof val !== "number") return { ok: false, reason: `field '${field}' must be number` };
|
|
191
|
+
if (def.type === "integer" && !Number.isInteger(val)) return { ok: false, reason: `field '${field}' must be integer` };
|
|
192
|
+
if (def.type === "boolean" && typeof val !== "boolean") return { ok: false, reason: `field '${field}' must be boolean` };
|
|
193
|
+
if (def.type === "array" && !Array.isArray(val)) return { ok: false, reason: `field '${field}' must be array` };
|
|
194
|
+
if (typeof val === "string" && val.length > policy.max_string_length)
|
|
195
|
+
return { ok: false, reason: `field '${field}' exceeds max length` };
|
|
196
|
+
if (def.enum && !def.enum.includes(val)) return { ok: false, reason: `field '${field}' not in enum` };
|
|
197
|
+
}
|
|
198
|
+
return { ok: true };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function semanticAnalysis(tool, secFindings) {
|
|
202
|
+
const ns = (tool || "").split(".")[0].toLowerCase();
|
|
203
|
+
const baseRisk = TOOL_RISK_PROFILES[ns] ?? 0.4;
|
|
204
|
+
const cats = new Set();
|
|
205
|
+
for (const f of secFindings) {
|
|
206
|
+
const m = f.split(": ").pop();
|
|
207
|
+
if (INJECTION_PATTERNS[m]) cats.add(m);
|
|
208
|
+
}
|
|
209
|
+
const chains = DANGEROUS_COMBOS
|
|
210
|
+
.filter((c) => c.indicators.every((i) => cats.has(i)))
|
|
211
|
+
.map((c) => ({ chain: c.name, severity: c.severity }));
|
|
212
|
+
let score = baseRisk + cats.size * 0.15;
|
|
213
|
+
let severity = "info";
|
|
214
|
+
if (chains.some((c) => c.severity === "critical")) severity = "critical";
|
|
215
|
+
else if (score >= 0.9) severity = "critical";
|
|
216
|
+
else if (score >= 0.7) severity = "high";
|
|
217
|
+
else if (score >= 0.5) severity = "medium";
|
|
218
|
+
else if (score >= 0.3) severity = "low";
|
|
219
|
+
return { tool_namespace: ns, base_risk: baseRisk, attack_chains: chains,
|
|
220
|
+
finding_categories: [...cats], semantic_severity: severity };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function verifyCall(call, policyOverride) {
|
|
224
|
+
const policy = deepMerge(DEFAULT_POLICY, policyOverride || {});
|
|
225
|
+
const dimensions = {};
|
|
226
|
+
|
|
227
|
+
const checks = [
|
|
228
|
+
["Structure", () => structureCheck(call, policy)],
|
|
229
|
+
["Schema", () => schemaCheck(call, policy)],
|
|
230
|
+
["Security", () => securityCheck(call)],
|
|
231
|
+
["Identity", () => identityCheck(call, policy)],
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
let secFindings = [];
|
|
235
|
+
for (const [name, fn] of checks) {
|
|
236
|
+
const r = fn();
|
|
237
|
+
dimensions[name] = { status: r.ok ? "pass" : "fail" };
|
|
238
|
+
if (!r.ok) dimensions[name].reason = r.reason;
|
|
239
|
+
if (name === "Security") secFindings = r.findings || [];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Latency/Cost (warn-only)
|
|
243
|
+
const meta = call.metadata || {};
|
|
244
|
+
dimensions.Latency = { status: "pass" };
|
|
245
|
+
if (meta.execution_time_us != null && policy.latency_budget_us && meta.execution_time_us > policy.latency_budget_us)
|
|
246
|
+
dimensions.Latency = { status: "warn", reason: `latency ${meta.execution_time_us}us exceeds budget` };
|
|
247
|
+
dimensions.Cost = { status: "pass" };
|
|
248
|
+
if (meta.cost != null && policy.cost_budget && meta.cost > policy.cost_budget)
|
|
249
|
+
dimensions.Cost = { status: "warn", reason: `cost ${meta.cost} exceeds budget` };
|
|
250
|
+
dimensions.Integrity = { status: "pass" };
|
|
251
|
+
|
|
252
|
+
const enforced = ["Structure", "Schema", "Security", "Identity"];
|
|
253
|
+
const verdict = (policy.mode === "audit" || enforced.every((d) => dimensions[d].status === "pass")) ? "allowed" : "denied";
|
|
254
|
+
|
|
255
|
+
const paramsHash = sha256(canonical(call.arguments || {}));
|
|
256
|
+
const policyHash = sha256(canonical(policy));
|
|
257
|
+
|
|
258
|
+
const semantic = policy.enable_semantic_analysis ? semanticAnalysis(call.tool, secFindings) : undefined;
|
|
259
|
+
|
|
260
|
+
const content = {
|
|
261
|
+
tool: call.tool, caller: call.caller?.agent_id || "unknown", verdict, mode: policy.mode,
|
|
262
|
+
params_hash: paramsHash, policy_hash: policyHash, dimensions,
|
|
263
|
+
};
|
|
264
|
+
const contentHash = sha256(canonical(content));
|
|
265
|
+
|
|
266
|
+
const evidence = {
|
|
267
|
+
evidence_type: "ccs.tool_call.verification",
|
|
268
|
+
evidence_version: CCS_VERSION,
|
|
269
|
+
evidence_id: crypto.randomUUID(),
|
|
270
|
+
tool: call.tool,
|
|
271
|
+
caller: call.caller?.agent_id || "unknown",
|
|
272
|
+
verdict,
|
|
273
|
+
mode: policy.mode,
|
|
274
|
+
params_hash: paramsHash,
|
|
275
|
+
policy_hash: policyHash,
|
|
276
|
+
content_hash: contentHash,
|
|
277
|
+
dimensions,
|
|
278
|
+
issued_at: new Date().toISOString(),
|
|
279
|
+
};
|
|
280
|
+
if (semantic) evidence.semantic_analysis = semantic;
|
|
281
|
+
if (call.parent_evidence_hash) evidence.parent_evidence_hash = call.parent_evidence_hash;
|
|
282
|
+
|
|
283
|
+
// Finalize evidence_hash
|
|
284
|
+
const hashBody = { ...evidence };
|
|
285
|
+
evidence.evidence_hash = sha256(canonical(hashBody));
|
|
286
|
+
return evidence;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function auditMcpConfig(config) {
|
|
290
|
+
const issues = [];
|
|
291
|
+
const servers = config.mcpServers || config.servers || {};
|
|
292
|
+
for (const [name, srv] of Object.entries(servers)) {
|
|
293
|
+
if (!srv.command && !srv.url) issues.push({ server: name, severity: "high", issue: "no command or url" });
|
|
294
|
+
if (srv.url && /^http:\/\//i.test(srv.url)) issues.push({ server: name, severity: "high", issue: "HTTP url (not HTTPS)" });
|
|
295
|
+
if (srv.env) {
|
|
296
|
+
for (const k of Object.keys(srv.env)) {
|
|
297
|
+
if (/KEY|TOKEN|SECRET|PASSWORD/i.test(k) && typeof srv.env[k] === "string" && srv.env[k].length < 8)
|
|
298
|
+
issues.push({ server: name, severity: "medium", issue: `weak ${k}` });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (srv.args?.some((a) => /--insecure|--no-verify|-k$/i.test(a)))
|
|
302
|
+
issues.push({ server: name, severity: "critical", issue: "TLS verification disabled" });
|
|
303
|
+
}
|
|
304
|
+
return { total_servers: Object.keys(servers).length, issues,
|
|
305
|
+
risk: issues.some((i) => i.severity === "critical") ? "critical" :
|
|
306
|
+
issues.some((i) => i.severity === "high") ? "high" :
|
|
307
|
+
issues.length ? "medium" : "low" };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// MCP Protocol (stdio JSON-RPC 2.0)
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
315
|
+
const SERVER_INFO = { name: "ccs-runtime-evidence", version: CCS_VERSION, title: "CCS Runtime Evidence" };
|
|
316
|
+
|
|
317
|
+
const TOOLS = [
|
|
318
|
+
{
|
|
319
|
+
name: "verify_tool_call",
|
|
320
|
+
description: "Verify an AI agent tool call against CCS 7 dimensions (Structure, Schema, Security, Identity, Integrity, Latency, Cost) plus semantic attack-chain analysis and math overflow detection. Returns verdict (allowed/denied) with detailed findings. DEFAULT MODE BLOCKS UNSAFE CALLS.",
|
|
321
|
+
inputSchema: {
|
|
322
|
+
type: "object",
|
|
323
|
+
properties: {
|
|
324
|
+
tool: { type: "string", description: "Tool name as namespace.action, e.g. 'shell.exec' or 'read.file'" },
|
|
325
|
+
arguments: { type: "object", description: "Tool call arguments to verify", additionalProperties: true },
|
|
326
|
+
caller: { type: "object", description: "Caller identity, e.g. {agent_id: 'agent-001'}",
|
|
327
|
+
properties: { agent_id: { type: "string" } }, required: ["agent_id"] },
|
|
328
|
+
metadata: { type: "object", description: "Optional metadata (execution_time_us, cost, request_id, etc.)", additionalProperties: true },
|
|
329
|
+
policy: { type: "object", description: "Optional CCS policy override (allowed_tools, denied_tools, mode, budgets, etc.)", additionalProperties: true },
|
|
330
|
+
},
|
|
331
|
+
required: ["tool"],
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "issue_evidence",
|
|
336
|
+
description: "Issue a CCS evidence record for a tool call. Evidence is cryptographically bound (content_hash + evidence_hash), tamper-evident, independently verifiable. Issued for allowed AND denied calls.",
|
|
337
|
+
inputSchema: {
|
|
338
|
+
type: "object",
|
|
339
|
+
properties: {
|
|
340
|
+
tool: { type: "string" },
|
|
341
|
+
arguments: { type: "object", additionalProperties: true },
|
|
342
|
+
caller: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] },
|
|
343
|
+
metadata: { type: "object", additionalProperties: true },
|
|
344
|
+
policy: { type: "object", additionalProperties: true },
|
|
345
|
+
},
|
|
346
|
+
required: ["tool", "caller"],
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "audit_mcp_config",
|
|
351
|
+
description: "Audit an MCP client/server configuration JSON for security risks: plain HTTP, weak secrets, disabled TLS, missing commands. Returns structured issues with severity.",
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: {
|
|
355
|
+
config: { type: "object", description: "MCP configuration JSON (e.g. Claude Desktop config)", additionalProperties: true },
|
|
356
|
+
},
|
|
357
|
+
required: ["config"],
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
function handleRequest(req) {
|
|
363
|
+
const { method, params, id } = req;
|
|
364
|
+
if (method === "initialize") {
|
|
365
|
+
return {
|
|
366
|
+
jsonrpc: "2.0", id,
|
|
367
|
+
result: {
|
|
368
|
+
protocolVersion: params?.protocolVersion || PROTOCOL_VERSION,
|
|
369
|
+
capabilities: { tools: {} },
|
|
370
|
+
serverInfo: SERVER_INFO,
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (method === "notifications/initialized" || method === "initialized") return null;
|
|
375
|
+
if (method === "ping") return { jsonrpc: "2.0", id, result: {} };
|
|
376
|
+
if (method === "tools/list") return { jsonrpc: "2.0", id, result: { tools: TOOLS } };
|
|
377
|
+
if (method === "tools/call") {
|
|
378
|
+
const name = params?.name;
|
|
379
|
+
const args = params?.arguments || {};
|
|
380
|
+
try {
|
|
381
|
+
let result;
|
|
382
|
+
if (name === "verify_tool_call") {
|
|
383
|
+
const ev = verifyCall(args, args.policy);
|
|
384
|
+
result = {
|
|
385
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
386
|
+
verdict: ev.verdict, blocked: ev.verdict === "denied",
|
|
387
|
+
tool: ev.tool, caller: ev.caller,
|
|
388
|
+
dimensions: ev.dimensions, semantic_analysis: ev.semantic_analysis,
|
|
389
|
+
evidence_id: ev.evidence_id, evidence_hash: ev.evidence_hash,
|
|
390
|
+
}, null, 2) }],
|
|
391
|
+
isError: ev.verdict === "denied",
|
|
392
|
+
};
|
|
393
|
+
} else if (name === "issue_evidence") {
|
|
394
|
+
const ev = verifyCall(args, args.policy);
|
|
395
|
+
result = { content: [{ type: "text", text: JSON.stringify(ev, null, 2) }] };
|
|
396
|
+
} else if (name === "audit_mcp_config") {
|
|
397
|
+
const report = auditMcpConfig(args.config || {});
|
|
398
|
+
result = { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
|
|
399
|
+
} else {
|
|
400
|
+
result = { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
401
|
+
}
|
|
402
|
+
return { jsonrpc: "2.0", id, result };
|
|
403
|
+
} catch (e) {
|
|
404
|
+
return { jsonrpc: "2.0", id,
|
|
405
|
+
result: { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true } };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (id !== undefined) return { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } };
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function main() {
|
|
413
|
+
process.stderr.write(`[ccs-mcp-server] v${CCS_VERSION} starting on stdio\n`);
|
|
414
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
415
|
+
rl.on("line", (line) => {
|
|
416
|
+
if (!line.trim()) return;
|
|
417
|
+
let req;
|
|
418
|
+
try { req = JSON.parse(line); } catch { return; }
|
|
419
|
+
const res = handleRequest(req);
|
|
420
|
+
if (res) process.stdout.write(JSON.stringify(res) + "\n");
|
|
421
|
+
});
|
|
422
|
+
rl.on("close", () => process.exit(0));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (require.main === module) main();
|
|
426
|
+
module.exports = { verifyCall, auditMcpConfig };
|