@mcp-audit-gateway/core 0.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.
Files changed (145) hide show
  1. package/.github/workflows/npm-publish.yml +21 -0
  2. package/CONTRIBUTING.md +41 -0
  3. package/LICENSE +21 -0
  4. package/README.md +156 -0
  5. package/dist/attestation/audit-log.d.ts +25 -0
  6. package/dist/attestation/audit-log.d.ts.map +1 -0
  7. package/dist/attestation/audit-log.js +95 -0
  8. package/dist/attestation/audit-log.js.map +1 -0
  9. package/dist/attestation/bugfix.test.d.ts +2 -0
  10. package/dist/attestation/bugfix.test.d.ts.map +1 -0
  11. package/dist/attestation/bugfix.test.js +103 -0
  12. package/dist/attestation/bugfix.test.js.map +1 -0
  13. package/dist/attestation/chain.test.d.ts +2 -0
  14. package/dist/attestation/chain.test.d.ts.map +1 -0
  15. package/dist/attestation/chain.test.js +153 -0
  16. package/dist/attestation/chain.test.js.map +1 -0
  17. package/dist/attestation/keygen.d.ts +5 -0
  18. package/dist/attestation/keygen.d.ts.map +1 -0
  19. package/dist/attestation/keygen.js +12 -0
  20. package/dist/attestation/keygen.js.map +1 -0
  21. package/dist/attestation/signer.d.ts +25 -0
  22. package/dist/attestation/signer.d.ts.map +1 -0
  23. package/dist/attestation/signer.js +93 -0
  24. package/dist/attestation/signer.js.map +1 -0
  25. package/dist/attestation/signer.test.d.ts +2 -0
  26. package/dist/attestation/signer.test.d.ts.map +1 -0
  27. package/dist/attestation/signer.test.js +62 -0
  28. package/dist/attestation/signer.test.js.map +1 -0
  29. package/dist/attestation/verify.d.ts +26 -0
  30. package/dist/attestation/verify.d.ts.map +1 -0
  31. package/dist/attestation/verify.js +104 -0
  32. package/dist/attestation/verify.js.map +1 -0
  33. package/dist/attestation/verify.test.d.ts +2 -0
  34. package/dist/attestation/verify.test.d.ts.map +1 -0
  35. package/dist/attestation/verify.test.js +79 -0
  36. package/dist/attestation/verify.test.js.map +1 -0
  37. package/dist/cli.d.ts +3 -0
  38. package/dist/cli.d.ts.map +1 -0
  39. package/dist/cli.js +364 -0
  40. package/dist/cli.js.map +1 -0
  41. package/dist/index.d.ts +12 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +12 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/integration.test.d.ts +2 -0
  46. package/dist/integration.test.d.ts.map +1 -0
  47. package/dist/integration.test.js +88 -0
  48. package/dist/integration.test.js.map +1 -0
  49. package/dist/policy/engine.d.ts +24 -0
  50. package/dist/policy/engine.d.ts.map +1 -0
  51. package/dist/policy/engine.js +130 -0
  52. package/dist/policy/engine.js.map +1 -0
  53. package/dist/policy/engine.test.d.ts +2 -0
  54. package/dist/policy/engine.test.d.ts.map +1 -0
  55. package/dist/policy/engine.test.js +80 -0
  56. package/dist/policy/engine.test.js.map +1 -0
  57. package/dist/proxy/gateway.d.ts +54 -0
  58. package/dist/proxy/gateway.d.ts.map +1 -0
  59. package/dist/proxy/gateway.js +245 -0
  60. package/dist/proxy/gateway.js.map +1 -0
  61. package/dist/proxy/gateway.test.d.ts +2 -0
  62. package/dist/proxy/gateway.test.d.ts.map +1 -0
  63. package/dist/proxy/gateway.test.js +93 -0
  64. package/dist/proxy/gateway.test.js.map +1 -0
  65. package/dist/proxy/mcp-server-adapter.d.ts +21 -0
  66. package/dist/proxy/mcp-server-adapter.d.ts.map +1 -0
  67. package/dist/proxy/mcp-server-adapter.js +98 -0
  68. package/dist/proxy/mcp-server-adapter.js.map +1 -0
  69. package/dist/proxy/mcp-server-adapter.test.d.ts +2 -0
  70. package/dist/proxy/mcp-server-adapter.test.d.ts.map +1 -0
  71. package/dist/proxy/mcp-server-adapter.test.js +160 -0
  72. package/dist/proxy/mcp-server-adapter.test.js.map +1 -0
  73. package/dist/proxy/upstream-manager.d.ts +49 -0
  74. package/dist/proxy/upstream-manager.d.ts.map +1 -0
  75. package/dist/proxy/upstream-manager.js +246 -0
  76. package/dist/proxy/upstream-manager.js.map +1 -0
  77. package/dist/proxy/upstream-manager.test.d.ts +2 -0
  78. package/dist/proxy/upstream-manager.test.d.ts.map +1 -0
  79. package/dist/proxy/upstream-manager.test.js +413 -0
  80. package/dist/proxy/upstream-manager.test.js.map +1 -0
  81. package/dist/telemetry/logger.d.ts +26 -0
  82. package/dist/telemetry/logger.d.ts.map +1 -0
  83. package/dist/telemetry/logger.js +48 -0
  84. package/dist/telemetry/logger.js.map +1 -0
  85. package/dist/telemetry/logger.test.d.ts +2 -0
  86. package/dist/telemetry/logger.test.d.ts.map +1 -0
  87. package/dist/telemetry/logger.test.js +124 -0
  88. package/dist/telemetry/logger.test.js.map +1 -0
  89. package/dist/telemetry/metrics.d.ts +41 -0
  90. package/dist/telemetry/metrics.d.ts.map +1 -0
  91. package/dist/telemetry/metrics.js +64 -0
  92. package/dist/telemetry/metrics.js.map +1 -0
  93. package/dist/telemetry/metrics.test.d.ts +2 -0
  94. package/dist/telemetry/metrics.test.d.ts.map +1 -0
  95. package/dist/telemetry/metrics.test.js +93 -0
  96. package/dist/telemetry/metrics.test.js.map +1 -0
  97. package/dist/telemetry/tracer.d.ts +16 -0
  98. package/dist/telemetry/tracer.d.ts.map +1 -0
  99. package/dist/telemetry/tracer.js +71 -0
  100. package/dist/telemetry/tracer.js.map +1 -0
  101. package/dist/types.d.ts +477 -0
  102. package/dist/types.d.ts.map +1 -0
  103. package/dist/types.js +68 -0
  104. package/dist/types.js.map +1 -0
  105. package/dist/wrap/proxy.d.ts +2 -0
  106. package/dist/wrap/proxy.d.ts.map +1 -0
  107. package/dist/wrap/proxy.js +152 -0
  108. package/dist/wrap/proxy.js.map +1 -0
  109. package/dist/wrap/proxy.test.d.ts +2 -0
  110. package/dist/wrap/proxy.test.d.ts.map +1 -0
  111. package/dist/wrap/proxy.test.js +155 -0
  112. package/dist/wrap/proxy.test.js.map +1 -0
  113. package/examples/README.md +33 -0
  114. package/examples/claude-desktop.json +50 -0
  115. package/examples/multi-agent-production.json +72 -0
  116. package/gateway.config.example.json +61 -0
  117. package/package.json +49 -0
  118. package/src/attestation/audit-log.ts +126 -0
  119. package/src/attestation/bugfix.test.ts +113 -0
  120. package/src/attestation/chain.test.ts +188 -0
  121. package/src/attestation/keygen.ts +18 -0
  122. package/src/attestation/signer.test.ts +75 -0
  123. package/src/attestation/signer.ts +105 -0
  124. package/src/attestation/verify.test.ts +86 -0
  125. package/src/attestation/verify.ts +131 -0
  126. package/src/cli.ts +407 -0
  127. package/src/index.ts +21 -0
  128. package/src/integration.test.ts +108 -0
  129. package/src/policy/engine.test.ts +95 -0
  130. package/src/policy/engine.ts +162 -0
  131. package/src/proxy/gateway.test.ts +108 -0
  132. package/src/proxy/gateway.ts +328 -0
  133. package/src/proxy/mcp-server-adapter.test.ts +197 -0
  134. package/src/proxy/mcp-server-adapter.ts +140 -0
  135. package/src/proxy/upstream-manager.test.ts +534 -0
  136. package/src/proxy/upstream-manager.ts +326 -0
  137. package/src/telemetry/logger.test.ts +139 -0
  138. package/src/telemetry/logger.ts +72 -0
  139. package/src/telemetry/metrics.test.ts +125 -0
  140. package/src/telemetry/metrics.ts +83 -0
  141. package/src/telemetry/tracer.ts +90 -0
  142. package/src/types.ts +113 -0
  143. package/src/wrap/proxy.test.ts +175 -0
  144. package/src/wrap/proxy.ts +192 -0
  145. package/tsconfig.json +18 -0
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { writeFile, unlink } from "node:fs/promises";
3
+ import { HmacSigner } from "./signer.js";
4
+ import { AuditLog } from "./audit-log.js";
5
+ import { verifyAuditLog } from "./verify.js";
6
+
7
+ const TEST_LOG = "/tmp/test-verify-audit.jsonl";
8
+ const SECRET = "b".repeat(64);
9
+
10
+ describe("verifyAuditLog", () => {
11
+ const signer = new HmacSigner(SECRET);
12
+
13
+ beforeEach(async () => {
14
+ try { await unlink(TEST_LOG); } catch {}
15
+ });
16
+
17
+ afterEach(async () => {
18
+ try { await unlink(TEST_LOG); } catch {}
19
+ });
20
+
21
+ it("verifies a valid audit log", async () => {
22
+ const log = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
23
+ await log.init();
24
+
25
+ await log.record("tools/call", {
26
+ toolName: "test/tool_a",
27
+ namespace: "test",
28
+ upstream: "test-server",
29
+ principal: "agent:bot",
30
+ durationMs: 100,
31
+ success: true,
32
+ });
33
+
34
+ await log.record("tools/call", {
35
+ toolName: "test/tool_b",
36
+ namespace: "test",
37
+ upstream: "test-server",
38
+ principal: "agent:bot",
39
+ durationMs: 50,
40
+ success: true,
41
+ });
42
+
43
+ const result = await verifyAuditLog(TEST_LOG, signer);
44
+ expect(result.total).toBe(2);
45
+ expect(result.valid).toBe(2);
46
+ expect(result.invalid).toBe(0);
47
+ expect(result.errors).toHaveLength(0);
48
+ });
49
+
50
+ it("detects tampered records", async () => {
51
+ const log = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
52
+ await log.init();
53
+
54
+ await log.record("tools/call", {
55
+ toolName: "test/tool_a",
56
+ durationMs: 100,
57
+ success: true,
58
+ });
59
+
60
+ const { readFile: rf } = await import("node:fs/promises");
61
+ const content = await rf(TEST_LOG, "utf-8");
62
+ const tampered = content.replace('"success":true', '"success":false');
63
+ await writeFile(TEST_LOG, tampered);
64
+
65
+ const result = await verifyAuditLog(TEST_LOG, signer);
66
+ expect(result.total).toBe(1);
67
+ expect(result.invalid).toBe(1);
68
+ expect(result.errors[0].reason).toBe("signature mismatch");
69
+ });
70
+
71
+ it("detects missing attestation", async () => {
72
+ const record = JSON.stringify({
73
+ id: "no-sig",
74
+ timestamp: "2026-08-16T10:00:00Z",
75
+ method: "tools/call",
76
+ durationMs: 50,
77
+ success: true,
78
+ });
79
+ await writeFile(TEST_LOG, record + "\n");
80
+
81
+ const result = await verifyAuditLog(TEST_LOG, signer);
82
+ expect(result.total).toBe(1);
83
+ expect(result.invalid).toBe(1);
84
+ expect(result.errors[0].reason).toBe("missing attestation");
85
+ });
86
+ });
@@ -0,0 +1,131 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { createInterface } from "node:readline";
3
+ import type { AuditRecord } from "../types.js";
4
+ import { HmacSigner, Ed25519Signer, type Signer } from "./signer.js";
5
+ import { hashRecord } from "./audit-log.js";
6
+
7
+ export interface VerifyResult {
8
+ total: number;
9
+ valid: number;
10
+ invalid: number;
11
+ errors: Array<{ line: number; id: string; reason: string }>;
12
+ }
13
+
14
+ export interface ChainVerifyResult {
15
+ total: number;
16
+ valid: boolean;
17
+ errors: Array<{ line: number; id: string; reason: string }>;
18
+ }
19
+
20
+ export async function verifyAuditLog(
21
+ path: string,
22
+ signer: Signer,
23
+ options?: { verifyChain?: boolean },
24
+ ): Promise<VerifyResult> {
25
+ const result: VerifyResult = { total: 0, valid: 0, invalid: 0, errors: [] };
26
+
27
+ const rl = createInterface({
28
+ input: createReadStream(path),
29
+ crlfDelay: Infinity,
30
+ });
31
+
32
+ let lineNum = 0;
33
+ let previousHash: string | null = null;
34
+
35
+ for await (const line of rl) {
36
+ lineNum++;
37
+ if (!line.trim()) continue;
38
+
39
+ let record: AuditRecord;
40
+ try {
41
+ record = JSON.parse(line);
42
+ } catch {
43
+ result.invalid++;
44
+ result.errors.push({ line: lineNum, id: "?", reason: "invalid JSON" });
45
+ result.total++;
46
+ continue;
47
+ }
48
+
49
+ result.total++;
50
+ const signature = record.attestation;
51
+ if (!signature) {
52
+ result.invalid++;
53
+ result.errors.push({ line: lineNum, id: record.id, reason: "missing attestation" });
54
+ continue;
55
+ }
56
+
57
+ const recordWithoutSig = { ...record };
58
+ delete recordWithoutSig.attestation;
59
+
60
+ const valid = await signer.verify(recordWithoutSig as AuditRecord, signature);
61
+ if (valid) {
62
+ result.valid++;
63
+ } else {
64
+ result.invalid++;
65
+ result.errors.push({ line: lineNum, id: record.id, reason: "signature mismatch" });
66
+ }
67
+
68
+ if (options?.verifyChain) {
69
+ if (record.previousHash === undefined) {
70
+ if (valid) {
71
+ result.invalid++;
72
+ result.valid--;
73
+ }
74
+ result.errors.push({
75
+ line: lineNum,
76
+ id: record.id,
77
+ reason: "missing previousHash in chain mode",
78
+ });
79
+ } else {
80
+ const expectedPrevHash = previousHash === null ? "genesis" : previousHash;
81
+ if (record.previousHash !== expectedPrevHash) {
82
+ if (valid) {
83
+ result.invalid++;
84
+ result.valid--;
85
+ }
86
+ result.errors.push({
87
+ line: lineNum,
88
+ id: record.id,
89
+ reason: "chain hash mismatch",
90
+ });
91
+ }
92
+ }
93
+ previousHash = hashRecord(record);
94
+ }
95
+ }
96
+
97
+ return result;
98
+ }
99
+
100
+ export async function verifyChain(records: AuditRecord[]): Promise<ChainVerifyResult> {
101
+ const result: ChainVerifyResult = { total: records.length, valid: true, errors: [] };
102
+
103
+ for (let i = 0; i < records.length; i++) {
104
+ const record = records[i];
105
+ const lineNum = i + 1;
106
+
107
+ if (i === 0) {
108
+ if (record.previousHash !== "genesis") {
109
+ result.valid = false;
110
+ result.errors.push({
111
+ line: lineNum,
112
+ id: record.id,
113
+ reason: "first record previousHash must be \"genesis\"",
114
+ });
115
+ }
116
+ } else {
117
+ const prevRecord = records[i - 1];
118
+ const expectedHash = hashRecord(prevRecord);
119
+ if (record.previousHash !== expectedHash) {
120
+ result.valid = false;
121
+ result.errors.push({
122
+ line: lineNum,
123
+ id: record.id,
124
+ reason: "previousHash does not match hash of prior record",
125
+ });
126
+ }
127
+ }
128
+ }
129
+
130
+ return result;
131
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { Gateway, ToolCallError } from "./proxy/gateway.js";
5
+ import { GatewayConfigSchema } from "./types.js";
6
+
7
+ const USAGE = `mcp-audit — cryptographic proof of what your AI agents did
8
+
9
+ Commands:
10
+ wrap -- <cmd> Wrap any MCP server with transparent audit logging
11
+ tail Live stream of audit records
12
+ verify <log> Verify integrity of an audit log file
13
+ serve [config] Start the full gateway (multi-upstream, policy, OTel)
14
+ keygen [dir] Generate Ed25519 key pair
15
+ help Show this help
16
+
17
+ Examples:
18
+ mcp-audit wrap -- npx @modelcontextprotocol/server-github
19
+ mcp-audit tail
20
+ mcp-audit verify ~/.mcp-audit/audit.jsonl
21
+ mcp-audit serve ./gateway.config.json
22
+ `;
23
+
24
+ async function main() {
25
+ const [command = "serve", ...args] = process.argv.slice(2);
26
+
27
+ switch (command) {
28
+ case "wrap":
29
+ return wrap(args);
30
+ case "tail":
31
+ return tail();
32
+ case "serve":
33
+ return serve(args[0]);
34
+ case "verify":
35
+ return verify(args[0]);
36
+ case "keygen":
37
+ return keygen(args[0]);
38
+ case "help":
39
+ case "--help":
40
+ case "-h":
41
+ console.log(USAGE);
42
+ return;
43
+ default:
44
+ if (command.endsWith(".json")) {
45
+ return serve(command);
46
+ }
47
+ console.error(`Unknown command: ${command}`);
48
+ console.log(USAGE);
49
+ process.exit(1);
50
+ }
51
+ }
52
+
53
+ async function wrap(args: string[]) {
54
+ const dashDash = args.indexOf("--");
55
+ const cmdArgs = dashDash >= 0 ? args.slice(dashDash + 1) : args;
56
+
57
+ if (cmdArgs.length === 0) {
58
+ console.error("Usage: mcp-audit wrap -- <command> [args...]");
59
+ console.error("Example: mcp-audit wrap -- npx @modelcontextprotocol/server-github");
60
+ process.exit(1);
61
+ }
62
+
63
+ const [command, ...rest] = cmdArgs;
64
+ const { runWrapProxy } = await import("./wrap/proxy.js");
65
+ await runWrapProxy(command, rest);
66
+ }
67
+
68
+ async function tail() {
69
+ const { homedir } = await import("node:os");
70
+ const { join } = await import("node:path");
71
+ const { createReadStream, watch } = await import("node:fs");
72
+ const { stat } = await import("node:fs/promises");
73
+
74
+ const logPath = join(homedir(), ".mcp-audit", "audit.jsonl");
75
+
76
+ try {
77
+ await stat(logPath);
78
+ } catch {
79
+ console.error(`No audit log found at ${logPath}`);
80
+ console.error("Start wrapping an MCP server first: mcp-audit wrap -- <command>");
81
+ process.exit(1);
82
+ }
83
+
84
+ console.log(`Tailing ${logPath}...\n`);
85
+
86
+ let position = 0;
87
+ const s = await stat(logPath);
88
+ position = s.size;
89
+
90
+ const printLine = (line: string) => {
91
+ try {
92
+ const entry = JSON.parse(line);
93
+ const status = entry.success ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
94
+ const dur = `${entry.durationMs}ms`.padStart(7);
95
+ const tool = (entry.toolName ?? "unknown").padEnd(30);
96
+ const time = entry.timestamp?.substring(11, 19) ?? "";
97
+ console.log(`${status} ${time} ${tool} ${dur} ${entry.id.substring(0, 8)}`);
98
+ } catch {}
99
+ };
100
+
101
+ const readNewLines = () => {
102
+ const stream = createReadStream(logPath, { start: position, encoding: "utf-8" });
103
+ let buffer = "";
104
+ stream.on("data", (chunk) => {
105
+ buffer += chunk;
106
+ const lines = buffer.split("\n");
107
+ buffer = lines.pop() ?? "";
108
+ for (const line of lines) {
109
+ if (line.trim()) printLine(line);
110
+ }
111
+ position += Buffer.byteLength(chunk as string);
112
+ });
113
+ };
114
+
115
+ readNewLines();
116
+ watch(logPath, () => readNewLines());
117
+ }
118
+
119
+ async function serve(configPath?: string) {
120
+ const resolved = resolve(configPath ?? "gateway.config.json");
121
+
122
+ let rawConfig: unknown;
123
+ try {
124
+ const content = await readFile(resolved, "utf-8");
125
+ rawConfig = JSON.parse(content);
126
+ } catch (err) {
127
+ console.error(`Failed to read config from ${resolved}:`, err);
128
+ process.exit(1);
129
+ }
130
+
131
+ const parsed = GatewayConfigSchema.safeParse(rawConfig);
132
+ if (!parsed.success) {
133
+ console.error("Invalid configuration:");
134
+ for (const issue of parsed.error.issues) {
135
+ console.error(` ${issue.path.join(".")}: ${issue.message}`);
136
+ }
137
+ process.exit(1);
138
+ }
139
+
140
+ const config = parsed.data;
141
+ const gateway = new Gateway(config);
142
+ await gateway.init();
143
+
144
+ console.log(`${config.name} v${config.version} started`);
145
+ console.log(` Transport: ${config.listen.transport}`);
146
+ console.log(` Upstreams: ${config.upstreams.length}`);
147
+ console.log(` Attestation: ${config.attestation.enabled ? config.attestation.algorithm : "disabled"}`);
148
+ console.log(` Telemetry: ${config.telemetry.enabled ? "enabled" : "disabled"}`);
149
+ console.log(` Audit log: ${config.auditLog.enabled ? config.auditLog.path : "disabled"}`);
150
+ console.log(` Policy: default=${config.policy.defaultEffect}, ${config.policy.rules.length} rules`);
151
+ console.log();
152
+
153
+ if (config.listen.transport === "streamable-http") {
154
+ const { createServer } = await import("node:http");
155
+ const server = createServer(async (req, res) => {
156
+ if (req.url === "/health") {
157
+ const status = gateway.getStatus();
158
+ res.writeHead(200, { "Content-Type": "application/json" });
159
+ res.end(JSON.stringify(status));
160
+ return;
161
+ }
162
+
163
+ if (req.url === "/discover") {
164
+ res.writeHead(200, { "Content-Type": "application/json" });
165
+ res.end(JSON.stringify(gateway.getServerDiscover()));
166
+ return;
167
+ }
168
+
169
+ if (req.method === "POST" && req.url === "/mcp") {
170
+ const MAX_BODY_SIZE = 10 * 1024 * 1024;
171
+ let body = "";
172
+ let size = 0;
173
+ let aborted = false;
174
+ const principalHeader = config.listen.principalHeader;
175
+ const transportPrincipal = principalHeader
176
+ ? (req.headers[principalHeader.toLowerCase()] as string | undefined)
177
+ : undefined;
178
+ req.on("data", (chunk) => {
179
+ size += chunk.length;
180
+ if (size > MAX_BODY_SIZE) {
181
+ aborted = true;
182
+ res.writeHead(413, { "Content-Type": "application/json" });
183
+ res.end(JSON.stringify({
184
+ jsonrpc: "2.0",
185
+ id: null,
186
+ error: { code: -32600, message: "Request body too large" },
187
+ }));
188
+ req.destroy();
189
+ return;
190
+ }
191
+ body += chunk;
192
+ });
193
+ req.on("end", async () => {
194
+ if (aborted) return;
195
+ let requestId: unknown = null;
196
+ try {
197
+ const request = JSON.parse(body);
198
+ requestId = request.id ?? null;
199
+ const result = await handleJsonRpc(gateway, request, transportPrincipal);
200
+ res.writeHead(200, { "Content-Type": "application/json" });
201
+ res.end(JSON.stringify(result));
202
+ } catch (err) {
203
+ res.writeHead(500, { "Content-Type": "application/json" });
204
+ res.end(JSON.stringify({
205
+ jsonrpc: "2.0",
206
+ id: requestId,
207
+ error: { code: -32603, message: String(err) },
208
+ }));
209
+ }
210
+ });
211
+ return;
212
+ }
213
+
214
+ res.writeHead(404);
215
+ res.end("Not found");
216
+ });
217
+
218
+ server.listen(config.listen.port, config.listen.host, () => {
219
+ console.log(`Listening on http://${config.listen.host}:${config.listen.port}`);
220
+ });
221
+
222
+ process.on("SIGINT", async () => {
223
+ await gateway.shutdown();
224
+ server.close();
225
+ process.exit(0);
226
+ });
227
+ } else {
228
+ console.log("stdio transport: reading JSON-RPC from stdin");
229
+ const { createInterface } = await import("node:readline");
230
+ const rl = createInterface({ input: process.stdin });
231
+ for await (const line of rl) {
232
+ if (!line.trim()) continue;
233
+ let requestId: unknown = null;
234
+ try {
235
+ const request = JSON.parse(line);
236
+ requestId = request.id ?? null;
237
+ const result = await handleJsonRpc(gateway, request);
238
+ process.stdout.write(JSON.stringify(result) + "\n");
239
+ } catch (err) {
240
+ process.stdout.write(
241
+ JSON.stringify({
242
+ jsonrpc: "2.0",
243
+ id: requestId,
244
+ error: { code: -32603, message: String(err) },
245
+ }) + "\n",
246
+ );
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ async function verify(logPath?: string) {
253
+ if (!logPath) {
254
+ console.error("Usage: mcp-audit-gateway verify <audit-log-path>");
255
+ process.exit(1);
256
+ }
257
+
258
+ const resolved = resolve(logPath);
259
+ const { verifyAuditLog } = await import("./attestation/verify.js");
260
+ const { HmacSigner, Ed25519Signer } = await import("./attestation/signer.js");
261
+
262
+ const { homedir } = await import("node:os");
263
+ const { join } = await import("node:path");
264
+
265
+ const configPath = process.env.GATEWAY_CONFIG ?? "gateway.config.json";
266
+ let signer: import("./attestation/signer.js").Signer;
267
+
268
+ try {
269
+ const content = await readFile(resolve(configPath), "utf-8");
270
+ const config = GatewayConfigSchema.parse(JSON.parse(content));
271
+
272
+ if (config.attestation.algorithm === "hmac-sha256" && config.attestation.secret) {
273
+ signer = new HmacSigner(config.attestation.secret);
274
+ } else {
275
+ const edSigner = new Ed25519Signer(config.attestation.keyPath);
276
+ await edSigner.init();
277
+ signer = edSigner;
278
+ }
279
+ } catch {
280
+ const wrapKeyPath = join(homedir(), ".mcp-audit", "key.hex");
281
+ try {
282
+ const keyHex = await readFile(wrapKeyPath, "utf-8");
283
+ signer = new HmacSigner(keyHex.trim());
284
+ } catch {
285
+ console.error("Cannot find signing key. Checked gateway.config.json and ~/.mcp-audit/key.hex");
286
+ process.exit(1);
287
+ }
288
+ }
289
+
290
+ console.log(`Verifying ${resolved}...`);
291
+ const result = await verifyAuditLog(resolved, signer, { verifyChain: true });
292
+
293
+ console.log(`\nResults:`);
294
+ console.log(` Total records: ${result.total}`);
295
+ console.log(` Valid: ${result.valid}`);
296
+ console.log(` Invalid: ${result.invalid}`);
297
+
298
+ if (result.errors.length > 0) {
299
+ console.log(`\nErrors:`);
300
+ for (const err of result.errors.slice(0, 20)) {
301
+ console.log(` Line ${err.line} [${err.id}]: ${err.reason}`);
302
+ }
303
+ if (result.errors.length > 20) {
304
+ console.log(` ... and ${result.errors.length - 20} more`);
305
+ }
306
+ process.exit(1);
307
+ }
308
+
309
+ console.log("\nAll records verified successfully.");
310
+ }
311
+
312
+ async function keygen(dir?: string) {
313
+ const outputDir = resolve(dir ?? ".");
314
+ const { generateKeyPair } = await import("./attestation/keygen.js");
315
+
316
+ const { publicKeyPath, privateKeyPath } = await generateKeyPair(outputDir);
317
+ console.log(`Key pair generated:`);
318
+ console.log(` Private key: ${privateKeyPath}`);
319
+ console.log(` Public key: ${publicKeyPath}`);
320
+ console.log(`\nAdd to gateway.config.json:`);
321
+ console.log(` "attestation": { "enabled": true, "algorithm": "ed25519", "keyPath": "${privateKeyPath}" }`);
322
+ }
323
+
324
+ async function handleJsonRpc(
325
+ gateway: Gateway,
326
+ request: { id?: unknown; method?: string; params?: Record<string, unknown> },
327
+ transportPrincipal?: string,
328
+ ) {
329
+ const { id, method, params } = request;
330
+ const principal = transportPrincipal ?? undefined;
331
+ const traceContext = (params?._meta as Record<string, unknown>)?.traceContext as
332
+ | { traceparent?: string; tracestate?: string }
333
+ | undefined;
334
+
335
+ switch (method) {
336
+ case "server/discover":
337
+ return { jsonrpc: "2.0", id, result: gateway.getServerDiscover() };
338
+
339
+ case "tools/list": {
340
+ const listResult = await gateway.handleToolsList(principal);
341
+ return { jsonrpc: "2.0", id, result: listResult };
342
+ }
343
+
344
+ case "tools/call": {
345
+ const toolName = params?.name as string;
346
+ const args = (params?.arguments ?? {}) as Record<string, unknown>;
347
+ try {
348
+ const { result, auditRecord } = await gateway.handleToolsCall(
349
+ toolName,
350
+ args,
351
+ principal,
352
+ traceContext,
353
+ );
354
+ return {
355
+ jsonrpc: "2.0",
356
+ id,
357
+ result: {
358
+ ...(result as object),
359
+ _meta: {
360
+ "x-gateway-attestation/v1": {
361
+ auditId: auditRecord.id,
362
+ attestation: auditRecord.attestation,
363
+ timestamp: auditRecord.timestamp,
364
+ },
365
+ },
366
+ },
367
+ };
368
+ } catch (err) {
369
+ if (err instanceof ToolCallError) {
370
+ return {
371
+ jsonrpc: "2.0",
372
+ id,
373
+ error: {
374
+ code: err.code,
375
+ message: err.message,
376
+ data: {
377
+ _meta: {
378
+ "x-gateway-attestation/v1": {
379
+ auditId: err.auditRecord.id,
380
+ attestation: err.auditRecord.attestation,
381
+ timestamp: err.auditRecord.timestamp,
382
+ },
383
+ },
384
+ },
385
+ },
386
+ };
387
+ }
388
+ throw err;
389
+ }
390
+ }
391
+
392
+ case "x-gateway-routing/v1/status":
393
+ return { jsonrpc: "2.0", id, result: gateway.getStatus() };
394
+
395
+ default:
396
+ return {
397
+ jsonrpc: "2.0",
398
+ id,
399
+ error: { code: -32601, message: `Method not found: ${method}` },
400
+ };
401
+ }
402
+ }
403
+
404
+ main().catch((err) => {
405
+ console.error("Fatal:", err);
406
+ process.exit(1);
407
+ });
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ export { Gateway, ToolCallError } from "./proxy/gateway.js";
2
+ export { runWrapProxy } from "./wrap/proxy.js";
3
+ export { McpServerAdapter } from "./proxy/mcp-server-adapter.js";
4
+ export { UpstreamManager } from "./proxy/upstream-manager.js";
5
+ export { PolicyEngine } from "./policy/engine.js";
6
+ export { AuditLog } from "./attestation/audit-log.js";
7
+ export { createSigner, HmacSigner, Ed25519Signer } from "./attestation/signer.js";
8
+ export { verifyAuditLog } from "./attestation/verify.js";
9
+ export { generateKeyPair } from "./attestation/keygen.js";
10
+ export { GatewayTracer } from "./telemetry/tracer.js";
11
+ export {
12
+ GatewayConfigSchema,
13
+ type GatewayConfig,
14
+ type UpstreamConfig,
15
+ type PolicyRule,
16
+ type AttestationConfig,
17
+ type TelemetryConfig,
18
+ type AuditRecord,
19
+ type ToolEntry,
20
+ type UpstreamStatus,
21
+ } from "./types.js";