@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,108 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
4
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
+ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
+ import { Gateway } from "./proxy/gateway.js";
7
+ import { HmacSigner } from "./attestation/signer.js";
8
+ import { verifyAuditLog } from "./attestation/verify.js";
9
+ import { unlink } from "node:fs/promises";
10
+ import type { GatewayConfig } from "./types.js";
11
+
12
+ const AUDIT_PATH = "/tmp/integration-test-audit.jsonl";
13
+
14
+ const config: GatewayConfig = {
15
+ name: "test-integration",
16
+ version: "0.1.0",
17
+ listen: { transport: "streamable-http", port: 3199, host: "127.0.0.1" },
18
+ upstreams: [],
19
+ policy: {
20
+ defaultEffect: "allow",
21
+ rules: [
22
+ { effect: "deny", principals: ["agent:blocked"], tools: ["*/secret_*"] },
23
+ ],
24
+ },
25
+ attestation: { enabled: true, algorithm: "hmac-sha256", secret: "c".repeat(64), includeParams: false, includeResult: false },
26
+ telemetry: { enabled: false, serviceName: "test", sampleRate: 0 },
27
+ auditLog: { enabled: true, path: AUDIT_PATH, rotateAfterMb: 10 },
28
+ };
29
+
30
+ describe("Integration: Gateway with in-memory MCP server", () => {
31
+ afterEach(async () => {
32
+ try { await unlink(AUDIT_PATH); } catch {}
33
+ });
34
+
35
+ it("routes a tool call through the gateway and produces a verifiable audit record", async () => {
36
+ const server = new Server(
37
+ { name: "echo-server", version: "1.0.0" },
38
+ { capabilities: { tools: {} } },
39
+ );
40
+
41
+ server.setRequestHandler(
42
+ ListToolsRequestSchema,
43
+ async () => ({
44
+ tools: [
45
+ { name: "echo", description: "Echoes input", inputSchema: { type: "object" as const, properties: { msg: { type: "string" } } } },
46
+ { name: "secret_tool", description: "Secret", inputSchema: { type: "object" as const } },
47
+ ],
48
+ }),
49
+ );
50
+
51
+ server.setRequestHandler(
52
+ CallToolRequestSchema,
53
+ async (request) => ({
54
+ content: [{ type: "text", text: `echo: ${request.params.arguments?.msg}` }],
55
+ }),
56
+ );
57
+
58
+ const gateway = new Gateway(config);
59
+ await gateway.init();
60
+
61
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
62
+ const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
63
+
64
+ await server.connect(serverTransport);
65
+ await client.connect(clientTransport);
66
+
67
+ const toolsResult = await client.listTools();
68
+ gateway.registerUpstreamTools("echo-server", "echo", toolsResult.tools.map((t) => ({
69
+ name: t.name,
70
+ description: t.description,
71
+ inputSchema: t.inputSchema,
72
+ })));
73
+
74
+ const listed = await gateway.handleToolsList();
75
+ expect(listed.tools).toHaveLength(2);
76
+ const names = listed.tools.map((t: any) => t.name);
77
+ expect(names).toContain("echo/echo");
78
+ expect(names).toContain("echo/secret_tool");
79
+
80
+ const filteredList = await gateway.handleToolsList("agent:blocked");
81
+ const filteredNames = filteredList.tools.map((t: any) => t.name);
82
+ expect(filteredNames).toContain("echo/echo");
83
+ expect(filteredNames).not.toContain("echo/secret_tool");
84
+
85
+ // Make a denied call to generate an audit record
86
+ try {
87
+ await gateway.handleToolsCall("echo/secret_tool", {}, "agent:blocked");
88
+ } catch {
89
+ // expected denial
90
+ }
91
+
92
+ // Make a call to unknown tool to generate another audit record
93
+ try {
94
+ await gateway.handleToolsCall("echo/nonexistent", {});
95
+ } catch {
96
+ // expected
97
+ }
98
+
99
+ const signer = new HmacSigner("c".repeat(64));
100
+ const verifyResult = await verifyAuditLog(AUDIT_PATH, signer);
101
+ expect(verifyResult.total).toBe(2);
102
+ expect(verifyResult.valid).toBe(2);
103
+ expect(verifyResult.invalid).toBe(0);
104
+
105
+ await client.close();
106
+ await server.close();
107
+ });
108
+ });
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { PolicyEngine } from "./engine.js";
3
+ import type { ToolEntry } from "../types.js";
4
+
5
+ const tool = (name: string, ns: string): ToolEntry => ({
6
+ name: `${ns}/${name}`,
7
+ originalName: name,
8
+ namespace: ns,
9
+ upstream: `${ns}-server`,
10
+ });
11
+
12
+ describe("PolicyEngine", () => {
13
+ describe("default-allow", () => {
14
+ const engine = new PolicyEngine("allow", [
15
+ { effect: "deny", principals: ["agent:readonly-*"], tools: ["*/write_*", "*/delete_*"] },
16
+ { effect: "deny", tools: ["admin/shutdown"] },
17
+ ]);
18
+
19
+ it("allows unmatched tools", () => {
20
+ const decision = engine.evaluate("agent:dev", tool("read_data", "github"));
21
+ expect(decision.allowed).toBe(true);
22
+ });
23
+
24
+ it("denies matching principal+tool pattern", () => {
25
+ const decision = engine.evaluate("agent:readonly-bot", tool("write_file", "fs"));
26
+ expect(decision.allowed).toBe(false);
27
+ });
28
+
29
+ it("allows write for non-readonly principal", () => {
30
+ const decision = engine.evaluate("agent:admin", tool("write_file", "fs"));
31
+ expect(decision.allowed).toBe(true);
32
+ });
33
+
34
+ it("denies tool pattern regardless of principal", () => {
35
+ const decision = engine.evaluate("agent:admin", tool("shutdown", "admin"));
36
+ expect(decision.allowed).toBe(false);
37
+ });
38
+ });
39
+
40
+ describe("default-deny", () => {
41
+ const engine = new PolicyEngine("deny", [
42
+ { effect: "allow", principals: ["agent:trusted-*"], namespaces: ["github"] },
43
+ ]);
44
+
45
+ it("denies by default", () => {
46
+ const decision = engine.evaluate("agent:unknown", tool("create_pr", "github"));
47
+ expect(decision.allowed).toBe(false);
48
+ });
49
+
50
+ it("allows matching principal+namespace", () => {
51
+ const decision = engine.evaluate("agent:trusted-ci", tool("create_pr", "github"));
52
+ expect(decision.allowed).toBe(true);
53
+ });
54
+
55
+ it("denies trusted principal on wrong namespace", () => {
56
+ const decision = engine.evaluate("agent:trusted-ci", tool("drop_table", "database"));
57
+ expect(decision.allowed).toBe(false);
58
+ });
59
+ });
60
+
61
+ describe("rate limiting", () => {
62
+ it("allows calls within limit", () => {
63
+ const engine = new PolicyEngine("allow", [
64
+ { effect: "allow", principals: ["agent:bot"], rateLimit: { maxPerMinute: 3 } },
65
+ ]);
66
+
67
+ const t = tool("read", "api");
68
+ engine.evaluate("agent:bot", t);
69
+ engine.recordInvocation("agent:bot", t);
70
+ engine.recordInvocation("agent:bot", t);
71
+ engine.recordInvocation("agent:bot", t);
72
+
73
+ const decision = engine.evaluate("agent:bot", t);
74
+ expect(decision.allowed).toBe(false);
75
+ expect(decision.reason).toContain("rate limit");
76
+ });
77
+ });
78
+
79
+ describe("filterTools", () => {
80
+ const engine = new PolicyEngine("allow", [
81
+ { effect: "deny", principals: ["agent:limited"], tools: ["*/dangerous_*"] },
82
+ ]);
83
+
84
+ it("removes denied tools from list", () => {
85
+ const tools = [
86
+ tool("safe_read", "fs"),
87
+ tool("dangerous_delete", "fs"),
88
+ tool("safe_write", "fs"),
89
+ ];
90
+ const filtered = engine.filterTools("agent:limited", tools);
91
+ expect(filtered).toHaveLength(2);
92
+ expect(filtered.map((t) => t.originalName)).not.toContain("dangerous_delete");
93
+ });
94
+ });
95
+ });
@@ -0,0 +1,162 @@
1
+ import type { PolicyRule, ToolEntry } from "../types.js";
2
+
3
+ export interface PolicyDecision {
4
+ allowed: boolean;
5
+ reason?: string;
6
+ rateLimit?: { maxPerMinute?: number; maxPerHour?: number };
7
+ }
8
+
9
+ interface RateLimitState {
10
+ minuteCounts: Map<string, { count: number; resetAt: number }>;
11
+ hourCounts: Map<string, { count: number; resetAt: number }>;
12
+ }
13
+
14
+ export class PolicyEngine {
15
+ private rateLimitState: RateLimitState = {
16
+ minuteCounts: new Map(),
17
+ hourCounts: new Map(),
18
+ };
19
+
20
+ constructor(
21
+ private defaultEffect: "allow" | "deny",
22
+ private rules: PolicyRule[],
23
+ ) {}
24
+
25
+ evaluate(principal: string | undefined, tool: ToolEntry): PolicyDecision {
26
+ let matchedRule: PolicyRule | null = null;
27
+
28
+ for (const rule of this.rules) {
29
+ if (this.ruleMatches(rule, principal, tool)) {
30
+ matchedRule = rule;
31
+ break;
32
+ }
33
+ }
34
+
35
+ if (!matchedRule) {
36
+ return { allowed: this.defaultEffect === "allow" };
37
+ }
38
+
39
+ if (matchedRule.effect === "deny") {
40
+ return { allowed: false, reason: "denied by policy rule" };
41
+ }
42
+
43
+ if (matchedRule.rateLimit) {
44
+ const key = `${principal ?? "anonymous"}:${tool.name}`;
45
+ const rateLimited = this.checkRateLimit(key, matchedRule.rateLimit);
46
+ if (rateLimited) {
47
+ return { allowed: false, reason: "rate limit exceeded", rateLimit: matchedRule.rateLimit };
48
+ }
49
+ }
50
+
51
+ return { allowed: true, rateLimit: matchedRule.rateLimit };
52
+ }
53
+
54
+ filterTools(principal: string | undefined, tools: ToolEntry[]): ToolEntry[] {
55
+ return tools.filter((tool) => this.evaluate(principal, tool).allowed);
56
+ }
57
+
58
+ recordInvocation(principal: string | undefined, tool: ToolEntry): void {
59
+ const key = `${principal ?? "anonymous"}:${tool.name}`;
60
+ this.incrementCounter(key);
61
+ }
62
+
63
+ private ruleMatches(
64
+ rule: PolicyRule,
65
+ principal: string | undefined,
66
+ tool: ToolEntry,
67
+ ): boolean {
68
+ if (rule.principals && rule.principals.length > 0) {
69
+ if (!principal || !this.matchesPattern(principal, rule.principals)) {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ if (rule.namespaces && rule.namespaces.length > 0) {
75
+ if (!rule.namespaces.includes(tool.namespace)) {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ if (rule.tools && rule.tools.length > 0) {
81
+ if (!this.matchesPattern(tool.name, rule.tools) &&
82
+ !this.matchesPattern(tool.originalName, rule.tools)) {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ return true;
88
+ }
89
+
90
+ private matchesPattern(value: string, patterns: string[]): boolean {
91
+ return patterns.some((pattern) => this.globMatch(pattern, value));
92
+ }
93
+
94
+ private globMatch(pattern: string, value: string): boolean {
95
+ if (!pattern.includes("*")) return pattern === value;
96
+
97
+ const parts = pattern.split("*");
98
+ let pos = 0;
99
+
100
+ for (let i = 0; i < parts.length; i++) {
101
+ const part = parts[i];
102
+ if (part === "") continue;
103
+
104
+ if (i === 0) {
105
+ if (!value.startsWith(part)) return false;
106
+ pos = part.length;
107
+ } else if (i === parts.length - 1) {
108
+ if (value.length - pos < part.length) return false;
109
+ if (!value.endsWith(part)) return false;
110
+ const suffixStart = value.length - part.length;
111
+ if (suffixStart < pos) return false;
112
+ } else {
113
+ const idx = value.indexOf(part, pos);
114
+ if (idx === -1) return false;
115
+ pos = idx + part.length;
116
+ }
117
+ }
118
+
119
+ return true;
120
+ }
121
+
122
+ private checkRateLimit(
123
+ key: string,
124
+ limit: { maxPerMinute?: number; maxPerHour?: number },
125
+ ): boolean {
126
+ const now = Date.now();
127
+
128
+ if (limit.maxPerMinute != null) {
129
+ const entry = this.rateLimitState.minuteCounts.get(key);
130
+ if (entry && entry.resetAt > now && entry.count >= limit.maxPerMinute) {
131
+ return true;
132
+ }
133
+ }
134
+
135
+ if (limit.maxPerHour != null) {
136
+ const entry = this.rateLimitState.hourCounts.get(key);
137
+ if (entry && entry.resetAt > now && entry.count >= limit.maxPerHour) {
138
+ return true;
139
+ }
140
+ }
141
+
142
+ return false;
143
+ }
144
+
145
+ private incrementCounter(key: string): void {
146
+ const now = Date.now();
147
+
148
+ const minute = this.rateLimitState.minuteCounts.get(key);
149
+ if (!minute || minute.resetAt <= now) {
150
+ this.rateLimitState.minuteCounts.set(key, { count: 1, resetAt: now + 60_000 });
151
+ } else {
152
+ minute.count++;
153
+ }
154
+
155
+ const hour = this.rateLimitState.hourCounts.get(key);
156
+ if (!hour || hour.resetAt <= now) {
157
+ this.rateLimitState.hourCounts.set(key, { count: 1, resetAt: now + 3_600_000 });
158
+ } else {
159
+ hour.count++;
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { Gateway } from "./gateway.js";
3
+ import type { GatewayConfig } from "../types.js";
4
+
5
+ const testConfig: GatewayConfig = {
6
+ name: "test-gateway",
7
+ version: "0.1.0",
8
+ listen: { transport: "streamable-http", port: 3100, host: "127.0.0.1" },
9
+ upstreams: [
10
+ {
11
+ name: "test-server",
12
+ namespace: "test",
13
+ transport: { type: "stdio", command: "echo", args: [] },
14
+ },
15
+ ],
16
+ policy: {
17
+ defaultEffect: "allow",
18
+ rules: [
19
+ {
20
+ effect: "deny",
21
+ principals: ["agent:blocked"],
22
+ tools: ["test/dangerous_tool"],
23
+ },
24
+ ],
25
+ },
26
+ attestation: { enabled: true, algorithm: "hmac-sha256", secret: "a".repeat(64), includeParams: false, includeResult: false },
27
+ telemetry: { enabled: false, serviceName: "test", sampleRate: 0 },
28
+ auditLog: { enabled: true, path: "/tmp/test-audit.jsonl", rotateAfterMb: 10 },
29
+ };
30
+
31
+ describe("Gateway", () => {
32
+ let gateway: Gateway;
33
+
34
+ beforeEach(async () => {
35
+ gateway = new Gateway(testConfig);
36
+ await gateway.init();
37
+
38
+ gateway.registerUpstreamTools("test-server", "test", [
39
+ { name: "safe_tool", description: "A safe tool" },
40
+ { name: "dangerous_tool", description: "A dangerous tool" },
41
+ { name: "read_data", description: "Read some data" },
42
+ ]);
43
+ });
44
+
45
+ describe("server/discover", () => {
46
+ it("advertises gateway capabilities", () => {
47
+ const discover = gateway.getServerDiscover();
48
+ expect(discover.capabilities).toHaveProperty("gateway");
49
+ const gw = (discover.capabilities as any).gateway;
50
+ expect(gw.namespacing).toBe(true);
51
+ expect(gw.filtering).toBe(true);
52
+ expect(gw.routing).toBe(true);
53
+ expect(gw.attestation).toBe(true);
54
+ expect(gw.upstreamCount).toBe(1);
55
+ });
56
+ });
57
+
58
+ describe("tools/list", () => {
59
+ it("returns namespaced tools", async () => {
60
+ const result = await gateway.handleToolsList();
61
+ expect(result.tools).toHaveLength(3);
62
+ const names = result.tools.map((t: any) => t.name);
63
+ expect(names).toContain("test/safe_tool");
64
+ expect(names).toContain("test/dangerous_tool");
65
+ expect(names).toContain("test/read_data");
66
+ });
67
+
68
+ it("filters tools based on policy", async () => {
69
+ const result = await gateway.handleToolsList("agent:blocked");
70
+ const names = result.tools.map((t: any) => t.name);
71
+ expect(names).toContain("test/safe_tool");
72
+ expect(names).not.toContain("test/dangerous_tool");
73
+ expect(names).toContain("test/read_data");
74
+ });
75
+
76
+ it("includes filtering metadata", async () => {
77
+ const result = await gateway.handleToolsList("agent:blocked");
78
+ const meta = result._meta as any;
79
+ expect(meta["x-gateway-routing/v1"].filteringApplied).toBe(true);
80
+ expect(meta["x-gateway-routing/v1"].totalToolsPreFilter).toBe(3);
81
+ expect(meta["x-gateway-routing/v1"].totalToolsPostFilter).toBe(2);
82
+ });
83
+ });
84
+
85
+ describe("tools/call", () => {
86
+ it("rejects unknown tools", async () => {
87
+ await expect(
88
+ gateway.handleToolsCall("nonexistent/tool", {}),
89
+ ).rejects.toThrow("Unknown tool");
90
+ });
91
+
92
+ it("rejects unauthorized tool calls", async () => {
93
+ await expect(
94
+ gateway.handleToolsCall("test/dangerous_tool", {}, "agent:blocked"),
95
+ ).rejects.toThrow();
96
+ });
97
+ });
98
+
99
+ describe("status", () => {
100
+ it("reports gateway and upstream health", () => {
101
+ const status = gateway.getStatus();
102
+ expect(status.gateway.status).toBe("healthy");
103
+ expect(status.upstreams).toHaveLength(1);
104
+ expect(status.upstreams[0].name).toBe("test-server");
105
+ expect(status.aggregate.totalTools).toBe(3);
106
+ });
107
+ });
108
+ });