ccs-mcp-server 1.2.17 → 1.4.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 CHANGED
@@ -1,50 +1,66 @@
1
1
  {
2
2
  "name": "ccs-mcp-server",
3
- "version": "1.2.17",
4
- "description": "MCP security server for AI agents — runtime verification and receipt verification for Claude Code, Cursor, Cline and Windsurf. 7-dimension CCS checks, Ed25519 tamper-evident receipts, offline verification, zero runtime dependencies.",
3
+ "version": "1.4.0",
4
+ "description": "MCP security server for AI agents — runtime verification, receipt verification and field-level tamper localization for Claude Code, Cursor, Cline and Windsurf. 7-dimension CCS checks, Ed25519 tamper-evident receipts, offline verification, zero runtime dependencies.",
5
5
  "type": "commonjs",
6
6
  "bin": {
7
- "ccs-mcp-server": "src/index.js"
7
+ "ccs-mcp-server": "src/index.js",
8
+ "agent-runtime-guard": "src/index.js"
8
9
  },
9
10
  "main": "src/index.js",
11
+ "exports": {
12
+ ".": "./src/index.js",
13
+ "./receipts": "./src/receipts.js",
14
+ "./dsh": "./dsh/index.js",
15
+ "./dsh/cordis.patch.yml": "./dsh/cordis.patch.yml",
16
+ "./package.json": "./package.json"
17
+ },
10
18
  "files": [
11
19
  "src",
20
+ "dsh",
12
21
  "README.md",
13
- "LICENSE"
22
+ "LICENSE",
23
+ "test"
14
24
  ],
15
25
  "keywords": [
16
- "mcp",
17
- "model-context-protocol",
18
- "mcp-server",
19
- "mcp-security",
26
+ "agent-guard",
27
+ "agent-runtime",
28
+ "agent-security",
29
+ "ai-agent-security",
20
30
  "claude",
21
31
  "claude-code",
22
- "cursor",
23
32
  "cline",
24
- "windsurf",
25
- "ai-agent",
26
- "ai-agent-security",
27
- "agent-security",
28
- "runtime-verification",
29
- "receipt-verification",
30
- "agent-receipt",
33
+ "command-injection",
34
+ "correctover",
35
+ "credential-protection",
36
+ "cursor",
37
+ "deepseek-harness",
38
+ "dsh-plugin",
31
39
  "ed25519",
40
+ "fail-closed",
41
+ "forensics",
42
+ "json-pointer",
43
+ "mcp",
44
+ "mcp-security",
45
+ "mcp-server",
46
+ "model-context-protocol",
47
+ "prompt-injection",
48
+ "rfc6901",
49
+ "runtime-interception",
50
+ "runtime-verification",
51
+ "ssrf",
52
+ "supply-chain-security",
32
53
  "tamper-evident",
33
- "offline-verification",
54
+ "tamper-localization",
34
55
  "tool-call-guardrail",
35
- "mcp security audit",
36
- "ccs",
37
- "correctover",
38
- "decision-provenance",
39
- "json-rpc",
40
- "supply-chain-security"
56
+ "windsurf"
41
57
  ],
42
58
  "license": "Elastic-2.0",
43
59
  "repository": {
44
60
  "type": "git",
45
61
  "url": "git+https://github.com/DSHCorrectover/ccs-mcp-server.git"
46
62
  },
47
- "homepage": "https://github.com/DSHCorrectover/ccs-mcp-server#readme",
63
+ "homepage": "https://correctover.com",
48
64
  "bugs": {
49
65
  "url": "https://github.com/DSHCorrectover/ccs-mcp-server/issues"
50
66
  },
@@ -52,13 +68,21 @@
52
68
  "node": ">=18"
53
69
  },
54
70
  "dependencies": {},
71
+ "peerDependencies": {
72
+ "@deepseek-ai/dsh": ">=0.2.1-alpha.1 <0.3.0-0"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "@deepseek-ai/dsh": {
76
+ "optional": true
77
+ }
78
+ },
55
79
  "scripts": {
56
80
  "test": "node test/run-tests.js"
57
81
  },
58
82
  "mcp": {
59
- "name": "ccs-runtime-evidence",
60
- "displayName": "CCS Runtime Evidence",
61
- "description": "Verify AI agent tool calls and issue Ed25519-signed CCS evidence receipts."
83
+ "name": "ccs-mcp-server",
84
+ "displayName": "CCS MCP Server",
85
+ "description": "Full-stack runtime interception for AI agents — blocks destructive commands, credential theft, SSRF and injection, with Ed25519 evidence receipts."
62
86
  },
63
87
  "mcpName": "io.github.DSHCorrectover/ccs-mcp-server"
64
88
  }
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * CCS Runtime Evidence MCP Server v1.0.0
3
+ * CCS Runtime Evidence MCP Server
4
4
  *
5
5
  * A standalone Model Context Protocol (MCP) server that exposes CCS runtime
6
6
  * verification as MCP tools, so any MCP-compatible client (Claude Desktop,
@@ -11,6 +11,9 @@
11
11
  * verify_tool_call Verify a tool call against CCS 7 dimensions + semantic + math
12
12
  * issue_evidence Issue a signed CCS evidence record (allow + deny)
13
13
  * audit_mcp_config Audit an MCP server configuration for security risks
14
+ * verify_intent_binding Verify call arguments match declared intent
15
+ * verify_receipt Offline-verify a CCS Ed25519-signed receipt
16
+ * locate_tampering Field-level localization of which fields differ (RFC 6901)
14
17
  *
15
18
  * Transport: stdio (per MCP spec). To run:
16
19
  * npx @correctover/ccs-mcp-server
@@ -28,6 +31,7 @@ const crypto = require("crypto");
28
31
  const readline = require("readline");
29
32
  const receipts = require("./receipts");
30
33
  const skillpay = require("./skillpay");
34
+ const tamper = require("./locate_tampering");
31
35
 
32
36
  // ---------------------------------------------------------------------------
33
37
  // CCS Verifier Core (pure stdlib Node.js)
@@ -431,7 +435,7 @@ function verifyIntentBinding(intent, toolName, toolArgs, now = Date.now()) {
431
435
  // ---------------------------------------------------------------------------
432
436
 
433
437
  const PROTOCOL_VERSION = "2024-11-05";
434
- const SERVER_INFO = { name: "ccs-runtime-evidence", version: CCS_VERSION, title: "CCS Runtime Evidence" };
438
+ const SERVER_INFO = { name: "agent-runtime-guard", version: CCS_VERSION, title: "Agent Runtime Guard" };
435
439
 
436
440
  // MCP tool annotations (spec 2024-11-05+). Hints describe the tool's real
437
441
  // runtime behaviour so clients/registries can drive consent, UI gating and
@@ -604,6 +608,38 @@ const TOOLS = [
604
608
  required: ["receipt"],
605
609
  },
606
610
  },
611
+ {
612
+ name: "locate_tampering",
613
+ title: "Locate which fields differ between two artifacts",
614
+ description:
615
+ "Field-level tamper localization. Given an original and a suspect artifact, returns an " +
616
+ "RFC 6901 JSON-Pointer list of every changed field with change type, before/after values, " +
617
+ "severity, and remediation. This is what verify_receipt alone cannot tell you: it says WHICH " +
618
+ "field changed, not merely that the signature failed. Severity is assigned by field semantics " +
619
+ "(verdict-bearing fields are critical), not by diff size.",
620
+ annotations: {
621
+ readOnlyHint: true,
622
+ destructiveHint: false,
623
+ idempotentHint: true,
624
+ openWorldHint: false,
625
+ },
626
+ inputSchema: {
627
+ type: "object",
628
+ properties: {
629
+ original: {
630
+ type: "object",
631
+ description: "The trusted/original artifact (receipt, evidence record, or config).",
632
+ additionalProperties: true,
633
+ },
634
+ suspect: {
635
+ type: "object",
636
+ description: "The artifact to compare against the original.",
637
+ additionalProperties: true,
638
+ },
639
+ },
640
+ required: ["original", "suspect"],
641
+ },
642
+ },
607
643
  ];
608
644
 
609
645
  function handleRequest(req) {
@@ -649,6 +685,18 @@ function handleRequest(req) {
649
685
  content: [{ type: "text", text: JSON.stringify(verdict, null, 2) }],
650
686
  isError: verdict.allowed === false,
651
687
  };
688
+ } else if (name === "locate_tampering") {
689
+ if (typeof args.original !== "object" || args.original === null ||
690
+ typeof args.suspect !== "object" || args.suspect === null) {
691
+ result = {
692
+ content: [{ type: "text",
693
+ text: "Error: both 'original' and 'suspect' must be JSON objects." }],
694
+ isError: true,
695
+ };
696
+ } else {
697
+ const report = tamper.buildReport(args.original, args.suspect);
698
+ result = { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
699
+ }
652
700
  } else if (name === "verify_receipt") {
653
701
  const v = args.expected_signer
654
702
  ? receipts.verifyReceiptWithKey(args.receipt, args.expected_signer)
@@ -670,7 +718,67 @@ function handleRequest(req) {
670
718
  return null;
671
719
  }
672
720
 
721
+ function cliInfo() {
722
+ const S = " ";
723
+ return [
724
+ `ccs-mcp-server v${CCS_VERSION} — Agent 运行时验证 + CCS 收据验签`,
725
+ ``,
726
+ `用法:`,
727
+ `${S}npx -y ccs-mcp-server # 作为 MCP server 启动(stdio)`,
728
+ `${S}npx -y ccs-mcp-server --help # 显示本说明`,
729
+ `${S}npx -y ccs-mcp-server --version # 显示版本`,
730
+ `${S}npx -y ccs-mcp-server --tools # 列出可用工具`,
731
+ ``,
732
+ `配置到 MCP 客户端(Claude Code / Cursor / Cline / Windsurf):`,
733
+ `${S}{ "mcpServers": { "ccs-mcp-server": { "command": "npx", "args": ["-y", "ccs-mcp-server"] } } }`,
734
+ ``,
735
+ `免费可用(本地离线,零依赖,无需 API key):`,
736
+ `${S}verify_tool_call 7 维运行时验证,危险调用默认拦截(fail-closed)`,
737
+ `${S}issue_evidence Ed25519 证据收据(放行与拦截都产生,第三方可离线验证)`,
738
+ `${S}audit_mcp_config MCP 配置静态安全审计`,
739
+ `${S}verify_intent_binding 校验实际参数与声明意图是否一致(抓跨模型参数漂移)`,
740
+ `${S}verify_receipt 离线验签 CCS 收据、检测篡改`,
741
+ `${S}locate_tampering 逐字段定位收据被改动的位置(RFC 6901 路径)`,
742
+ ``,
743
+ `需要订阅(¥49.90/月,支付宝 SkillPay):`,
744
+ `${S}· CCS收据验签 深度篡改取证`,
745
+ `${S}· CCS收据批量审计 目录级批量验签与合规留痕`,
746
+ `${S}· MCP配置安全体检 完整 7 维分级报告`,
747
+ `${S}货架: https://skillpay.alipay.com/public/correctover`,
748
+ ``,
749
+ `文档: https://github.com/DSHCorrectover/agent-runtime-guard`,
750
+ ].join("\n");
751
+ }
752
+
753
+ function cliTools() {
754
+ return [
755
+ "verify_tool_call Runtime verification, 7 dimensions, fail-closed by default",
756
+ "issue_evidence Issue an Ed25519-signed evidence receipt",
757
+ "audit_mcp_config Static security audit of MCP server configuration",
758
+ "verify_intent_binding Detect drift between declared intent and actual arguments",
759
+ "verify_receipt Offline receipt verification and tamper detection",
760
+ "locate_tampering Field-level localization of tampered fields (RFC 6901)",
761
+ ].join("\n");
762
+ }
763
+
673
764
  function main() {
765
+ // CLI 参数层:只处理显式的信息类参数,未命中则行为与以往完全一致。
766
+ // 为什么需要这层:MCP 客户端调用不带参数,但人类用户会敲 --help / --version 探路。
767
+ // 之前无论传什么参数都直接启动 stdio 服务并打广告,用户得不到任何信息、命令还会挂住。
768
+ const argv = process.argv.slice(2);
769
+ if (argv.includes("--help") || argv.includes("-h") || argv.includes("help")) {
770
+ process.stdout.write(cliInfo() + "\n");
771
+ process.exit(0);
772
+ }
773
+ if (argv.includes("--version") || argv.includes("-v")) {
774
+ process.stdout.write(CCS_VERSION + "\n");
775
+ process.exit(0);
776
+ }
777
+ if (argv.includes("--tools")) {
778
+ process.stdout.write(cliTools() + "\n");
779
+ process.exit(0);
780
+ }
781
+
674
782
  const kp = receipts.getKeypair();
675
783
  const fp = receipts.publicKeyFingerprint();
676
784
  const banner =
@@ -692,4 +800,4 @@ function main() {
692
800
  }
693
801
 
694
802
  if (require.main === module) main();
695
- module.exports = { verifyCall, auditMcpConfig, verifyIntentBinding, receipts };
803
+ module.exports = { verifyCall, auditMcpConfig, verifyIntentBinding, receipts, tamper };
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ /**
3
+ * CCS 字段级篡改定位 —— locate_tampering
4
+ *
5
+ * 为什么需要这个(它不是 verify_receipt 的替代品):
6
+ * verify_receipt 回一个布尔:签名对不对。审计人员拿到 false 之后
7
+ * 仍然不知道**哪里被改了**。要把话说清楚、要能举证,需要一份差异清单。
8
+ *
9
+ * 本模块产出 RFC 6901 JSON-Pointer 形式的字段变更清单,
10
+ * 每条带变化类型、原值、新值、严重度与整改建议。
11
+ *
12
+ * 与 Python 侧 correctover-ccs-mcp 的 ccs_locate_tampering 输出结构对齐,
13
+ * 使两个渠道(npm 运行时 / PyPI 取证)给出同一份证据格式。
14
+ *
15
+ * 零依赖:仅 crypto 用于可选的内容摘要。
16
+ *
17
+ * License: Elastic-2.0 (c) Correctover
18
+ */
19
+
20
+ const crypto = require("crypto");
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // RFC 6901 JSON-Pointer 转义
24
+ // ---------------------------------------------------------------------------
25
+
26
+ function pointerEscape(token) {
27
+ // 顺序不可颠倒:必须先转义 ~,否则会把 / 转出来的 ~1 再转一次
28
+ return String(token).replace(/~/g, "~0").replace(/\//g, "~1");
29
+ }
30
+
31
+ function joinPointer(base, token) {
32
+ return base + "/" + pointerEscape(token);
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // 严重度:按字段语义分级,不是按"改了多少字节"
37
+ //
38
+ // 设计取舍:改一个字符的 verdict 比改一百个字符的 latency 严重得多。
39
+ // 分级的依据是**这个字段决定谁的结论**,不是差异大小。
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const CRITICAL_EXACT = [
43
+ "/verdict", "/signature", "/signer_public_key", "/signature_alg",
44
+ "/evidence_hash", "/content_hash", "/receipt_id", "/instruction_digest",
45
+ ];
46
+ const CRITICAL_PREFIX = ["/checks/", "/policy/", "/decision"];
47
+ const HIGH_EXACT = [
48
+ "/reason", "/allowed", "/action", "/tool", "/principal", "/subject",
49
+ "/policy_mode", "/delegation",
50
+ ];
51
+ const MEDIUM_PREFIX = ["/latency", "/cost", "/model", "/provider", "/intent"];
52
+
53
+ function classifySeverity(pointer) {
54
+ if (CRITICAL_EXACT.includes(pointer)) return "critical";
55
+ if (CRITICAL_PREFIX.some((p) => pointer.startsWith(p))) return "critical";
56
+ if (HIGH_EXACT.includes(pointer)) return "high";
57
+ if (MEDIUM_PREFIX.some((p) => pointer.startsWith(p))) return "medium";
58
+ return "low";
59
+ }
60
+
61
+ const REMEDIATION = {
62
+ critical:
63
+ "A verdict-bearing field changed. Treat the artifact as untrusted: do not act on it, " +
64
+ "re-derive the conclusion from the original source, and re-issue the receipt.",
65
+ high:
66
+ "A context field changed. Confirm with the counterparty which version is authoritative " +
67
+ "before relying on either.",
68
+ medium:
69
+ "An operational field changed. Usually affects interpretation rather than the verdict; " +
70
+ "verify if it feeds a downstream decision.",
71
+ low:
72
+ "A non-decisional field changed. Low impact; record for completeness.",
73
+ };
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // 深度差异
77
+ // ---------------------------------------------------------------------------
78
+
79
+ function isPlainObject(v) {
80
+ return v !== null && typeof v === "object" && !Array.isArray(v);
81
+ }
82
+
83
+ /**
84
+ * 返回字段级变更清单。
85
+ * 数组按**下标**逐项比较,不用集合语义——收据里的数组是有序的,
86
+ * 把 [a,b] 与 [b,a] 判为"相同"会漏掉真实的顺序篡改。
87
+ */
88
+ function deepDiff(original, suspect, base = "") {
89
+ const changes = [];
90
+
91
+ const bothObjects = isPlainObject(original) && isPlainObject(suspect);
92
+ const bothArrays = Array.isArray(original) && Array.isArray(suspect);
93
+
94
+ if (bothObjects) {
95
+ const keys = new Set([...Object.keys(original), ...Object.keys(suspect)]);
96
+ for (const k of Array.from(keys).sort()) {
97
+ const ptr = joinPointer(base, k);
98
+ const hasO = Object.prototype.hasOwnProperty.call(original, k);
99
+ const hasS = Object.prototype.hasOwnProperty.call(suspect, k);
100
+ if (hasO && !hasS) {
101
+ changes.push(mk(ptr, "removed", original[k], undefined));
102
+ } else if (!hasO && hasS) {
103
+ changes.push(mk(ptr, "added", undefined, suspect[k]));
104
+ } else {
105
+ changes.push(...deepDiff(original[k], suspect[k], ptr));
106
+ }
107
+ }
108
+ return changes;
109
+ }
110
+
111
+ if (bothArrays) {
112
+ const n = Math.max(original.length, suspect.length);
113
+ for (let i = 0; i < n; i++) {
114
+ const ptr = joinPointer(base, i);
115
+ if (i >= original.length) {
116
+ changes.push(mk(ptr, "added", undefined, suspect[i]));
117
+ } else if (i >= suspect.length) {
118
+ changes.push(mk(ptr, "removed", original[i], undefined));
119
+ } else {
120
+ changes.push(...deepDiff(original[i], suspect[i], ptr));
121
+ }
122
+ }
123
+ return changes;
124
+ }
125
+
126
+ if (!sameScalar(original, suspect)) {
127
+ changes.push(mk(base || "/", "modified", original, suspect));
128
+ }
129
+ return changes;
130
+ }
131
+
132
+ function sameScalar(a, b) {
133
+ if (a === b) return true;
134
+ if (typeof a === "number" && typeof b === "number" && Number.isNaN(a) && Number.isNaN(b)) {
135
+ return true;
136
+ }
137
+ if (isPlainObject(a) || isPlainObject(b) || Array.isArray(a) || Array.isArray(b)) {
138
+ // 类型不一致(对象 vs 标量):交给 JSON 比较判等
139
+ return JSON.stringify(a) === JSON.stringify(b);
140
+ }
141
+ return false;
142
+ }
143
+
144
+ function preview(v) {
145
+ if (v === undefined) return null;
146
+ if (typeof v === "string") return v.length > 200 ? v.slice(0, 200) + "…" : v;
147
+ try {
148
+ const s = JSON.stringify(v);
149
+ return s && s.length > 200 ? s.slice(0, 200) + "…" : v;
150
+ } catch {
151
+ return String(v);
152
+ }
153
+ }
154
+
155
+ function mk(pointer, change, original, suspect) {
156
+ const severity = classifySeverity(pointer);
157
+ return {
158
+ path: pointer,
159
+ path_format: "rfc6901",
160
+ change,
161
+ original: preview(original),
162
+ suspect: preview(suspect),
163
+ severity,
164
+ remediation: REMEDIATION[severity],
165
+ // 兼容既有消费方(Python 侧曾输出点号路径)
166
+ path_dotted: pointer.replace(/^\//, "").replace(/\//g, "."),
167
+ };
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // 报告
172
+ // ---------------------------------------------------------------------------
173
+
174
+ function digestOf(obj) {
175
+ try {
176
+ return "sha256:" + crypto
177
+ .createHash("sha256")
178
+ .update(JSON.stringify(sortDeep(obj)))
179
+ .digest("hex");
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+
185
+ function sortDeep(v) {
186
+ if (Array.isArray(v)) return v.map(sortDeep);
187
+ if (isPlainObject(v)) {
188
+ const out = {};
189
+ for (const k of Object.keys(v).sort()) out[k] = sortDeep(v[k]);
190
+ return out;
191
+ }
192
+ return v;
193
+ }
194
+
195
+ function buildReport(original, suspect) {
196
+ const changes = deepDiff(original, suspect);
197
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
198
+ for (const c of changes) counts[c.severity]++;
199
+
200
+ const originalDigest = digestOf(original);
201
+ const suspectDigest = digestOf(suspect);
202
+
203
+ let conclusion;
204
+ if (changes.length === 0) {
205
+ conclusion =
206
+ "No field-level differences found between the two artifacts. " +
207
+ "Note: this compares content only; it does not by itself prove either artifact is authentic.";
208
+ } else if (counts.critical > 0) {
209
+ conclusion =
210
+ `${changes.length} field(s) differ, including ${counts.critical} verdict-bearing field(s). ` +
211
+ "At least one change affects the conclusion itself, so the two artifacts are not interchangeable.";
212
+ } else {
213
+ conclusion =
214
+ `${changes.length} field(s) differ. None are verdict-bearing; differences are likely ` +
215
+ "operational or contextual rather than decisional.";
216
+ }
217
+
218
+ return {
219
+ schema: "correctover.tamper-localization.v1",
220
+ tampered: changes.length > 0,
221
+ counts,
222
+ changes,
223
+ digests: { original: originalDigest, suspect: suspectDigest },
224
+ conclusion,
225
+ limitations: [
226
+ "This compares two artifacts you supplied. It does not prove which one is authentic.",
227
+ "It reports content differences only; it does not verify any cryptographic signature.",
228
+ "Pair it with verify_receipt to establish authenticity, then use this to localize the change.",
229
+ ],
230
+ };
231
+ }
232
+
233
+ module.exports = {
234
+ deepDiff,
235
+ buildReport,
236
+ classifySeverity,
237
+ pointerEscape,
238
+ joinPointer,
239
+ digestOf,
240
+ };
package/src/skillpay.js CHANGED
File without changes