@clampd/mcp-proxy 0.2.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/Dockerfile +32 -0
- package/README.md +103 -0
- package/dist/dashboard.d.ts +64 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +516 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/fleet.d.ts +52 -0
- package/dist/fleet.d.ts.map +1 -0
- package/dist/fleet.js +274 -0
- package/dist/fleet.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +173 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptor.d.ts +92 -0
- package/dist/interceptor.d.ts.map +1 -0
- package/dist/interceptor.js +274 -0
- package/dist/interceptor.js.map +1 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/mock-server.d.ts +14 -0
- package/dist/mock-server.d.ts.map +1 -0
- package/dist/mock-server.js +128 -0
- package/dist/mock-server.js.map +1 -0
- package/dist/proxy.d.ts +59 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +578 -0
- package/dist/proxy.js.map +1 -0
- package/fleet.example.json +38 -0
- package/package.json +44 -0
- package/src/dashboard.ts +602 -0
- package/src/fleet.ts +329 -0
- package/src/index.ts +187 -0
- package/src/interceptor.ts +427 -0
- package/src/logger.ts +17 -0
- package/src/mock-server.ts +240 -0
- package/src/proxy.ts +752 -0
- package/tsconfig.json +20 -0
package/src/fleet.ts
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet orchestrator — runs multiple MCP proxy instances from a single config.
|
|
3
|
+
*
|
|
4
|
+
* Each proxy wraps a different upstream MCP server with a different agent identity,
|
|
5
|
+
* enabling multi-agent demos showing scope isolation, delegation chains, and
|
|
6
|
+
* behavioral correlation.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* clampd-mcp-proxy --fleet-config fleet.json
|
|
10
|
+
*
|
|
11
|
+
* Config:
|
|
12
|
+
* {
|
|
13
|
+
* "gateway": "http://ag-gateway:8080",
|
|
14
|
+
* "apiKey": "ag_test_acme_demo_2026",
|
|
15
|
+
* "secret": "ags_...",
|
|
16
|
+
* "dashboardPort": 3000,
|
|
17
|
+
* "agents": [
|
|
18
|
+
* { "name": "Data Analyst", "agentId": "...", "port": 3003, "upstream": "...", "color": "#3b82f6" },
|
|
19
|
+
* { "name": "DevOps Bot", "agentId": "...", "port": 3004, "upstream": "...", "color": "#22c55e" },
|
|
20
|
+
* { "name": "DB Admin", "agentId": "...", "port": 3005, "upstream": "...", "color": "#f59e0b" }
|
|
21
|
+
* ]
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { startProxy, type ProxyOptions } from "./proxy.js";
|
|
28
|
+
import type { ProxyEvent } from "./dashboard.js";
|
|
29
|
+
import { log, setVerbose } from "./logger.js";
|
|
30
|
+
|
|
31
|
+
// ── Fleet Config ──────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface FleetAgentConfig {
|
|
34
|
+
name: string;
|
|
35
|
+
agentId: string;
|
|
36
|
+
port: number;
|
|
37
|
+
upstream: string;
|
|
38
|
+
color?: string;
|
|
39
|
+
scanInput?: boolean;
|
|
40
|
+
scanOutput?: boolean;
|
|
41
|
+
checkResponse?: boolean;
|
|
42
|
+
demoPanel?: boolean;
|
|
43
|
+
secret?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FleetConfig {
|
|
47
|
+
gateway: string;
|
|
48
|
+
apiKey: string;
|
|
49
|
+
secret?: string;
|
|
50
|
+
dashboardPort: number;
|
|
51
|
+
dryRun?: boolean;
|
|
52
|
+
verbose?: boolean;
|
|
53
|
+
agents: FleetAgentConfig[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Fleet Event (ProxyEvent + agent metadata) ─────────────────────────
|
|
57
|
+
|
|
58
|
+
export interface FleetEvent extends ProxyEvent {
|
|
59
|
+
agentName?: string;
|
|
60
|
+
agentColor?: string;
|
|
61
|
+
agentId?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Fleet State ───────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const fleetEvents: FleetEvent[] = [];
|
|
67
|
+
const MAX_FLEET_EVENTS = 2000;
|
|
68
|
+
const fleetSubscribers = new Set<ServerResponse>();
|
|
69
|
+
|
|
70
|
+
function pushFleetEvent(event: FleetEvent): void {
|
|
71
|
+
fleetEvents.push(event);
|
|
72
|
+
if (fleetEvents.length > MAX_FLEET_EVENTS) {
|
|
73
|
+
fleetEvents.shift();
|
|
74
|
+
}
|
|
75
|
+
const payload = `data: ${JSON.stringify(event)}\n\n`;
|
|
76
|
+
for (const res of fleetSubscribers) {
|
|
77
|
+
try {
|
|
78
|
+
res.write(payload);
|
|
79
|
+
} catch {
|
|
80
|
+
fleetSubscribers.delete(res);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Start Fleet ───────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export async function startFleet(configPath: string): Promise<void> {
|
|
88
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
89
|
+
const config: FleetConfig = JSON.parse(raw);
|
|
90
|
+
|
|
91
|
+
if (config.verbose) setVerbose(true);
|
|
92
|
+
|
|
93
|
+
log("info", "");
|
|
94
|
+
log("info", "=== Clampd MCP Fleet ===");
|
|
95
|
+
log("info", `Agents: ${config.agents.length}`);
|
|
96
|
+
log("info", `Dashboard: http://localhost:${config.dashboardPort}/`);
|
|
97
|
+
log("info", "");
|
|
98
|
+
|
|
99
|
+
// Build color map
|
|
100
|
+
const defaultColors = ["#3b82f6", "#22c55e", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"];
|
|
101
|
+
const colorMap = new Map<string, string>();
|
|
102
|
+
config.agents.forEach((a, i) => {
|
|
103
|
+
colorMap.set(a.agentId, a.color ?? defaultColors[i % defaultColors.length]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Start each proxy instance
|
|
107
|
+
for (const agent of config.agents) {
|
|
108
|
+
const opts: ProxyOptions = {
|
|
109
|
+
upstreamCommand: agent.upstream,
|
|
110
|
+
gatewayUrl: config.gateway,
|
|
111
|
+
apiKey: config.apiKey,
|
|
112
|
+
agentId: agent.agentId,
|
|
113
|
+
port: agent.port,
|
|
114
|
+
secret: agent.secret ?? config.secret,
|
|
115
|
+
dryRun: config.dryRun ?? false,
|
|
116
|
+
verbose: config.verbose ?? false,
|
|
117
|
+
scanInputEnabled: agent.scanInput ?? true,
|
|
118
|
+
scanOutputEnabled: agent.scanOutput ?? true,
|
|
119
|
+
checkResponse: agent.checkResponse ?? false,
|
|
120
|
+
demoPanel: agent.demoPanel ?? false,
|
|
121
|
+
agentName: agent.name,
|
|
122
|
+
onEvent: (event) => {
|
|
123
|
+
pushFleetEvent({
|
|
124
|
+
...event,
|
|
125
|
+
agentName: agent.name,
|
|
126
|
+
agentColor: colorMap.get(agent.agentId),
|
|
127
|
+
agentId: agent.agentId,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await startProxy(opts);
|
|
134
|
+
log("info", `Started agent "${agent.name}" on port ${agent.port} (${agent.agentId.slice(0, 8)}...)`);
|
|
135
|
+
} catch (err: unknown) {
|
|
136
|
+
log("error", `Failed to start agent "${agent.name}": ${err instanceof Error ? err.message : err}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Start fleet dashboard server
|
|
141
|
+
const httpServer = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
142
|
+
const url = new URL(req.url ?? "/", `http://localhost:${config.dashboardPort}`);
|
|
143
|
+
|
|
144
|
+
// GET / — Fleet dashboard
|
|
145
|
+
if (url.pathname === "/" && req.method === "GET") {
|
|
146
|
+
const html = renderFleetDashboard(fleetEvents, config);
|
|
147
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
|
|
148
|
+
res.end(html);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// GET /events — Fleet SSE stream
|
|
153
|
+
if (url.pathname === "/events" && req.method === "GET") {
|
|
154
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
155
|
+
res.write(`data: ${JSON.stringify({ type: "connected", total: fleetEvents.length })}\n\n`);
|
|
156
|
+
fleetSubscribers.add(res);
|
|
157
|
+
req.on("close", () => fleetSubscribers.delete(res));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// GET /health — Fleet health
|
|
162
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
163
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
164
|
+
res.end(JSON.stringify({
|
|
165
|
+
status: "ok",
|
|
166
|
+
mode: "fleet",
|
|
167
|
+
agents: config.agents.map((a) => ({ name: a.name, agentId: a.agentId, port: a.port })),
|
|
168
|
+
total_events: fleetEvents.length,
|
|
169
|
+
}));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
res.writeHead(404);
|
|
174
|
+
res.end("Not found");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
httpServer.listen(config.dashboardPort, () => {
|
|
178
|
+
log("info", `Fleet dashboard: http://localhost:${config.dashboardPort}/`);
|
|
179
|
+
for (const agent of config.agents) {
|
|
180
|
+
log("info", ` ${agent.name}: http://localhost:${agent.port}/sse (MCP) | http://localhost:${agent.port}/ (dashboard)`);
|
|
181
|
+
}
|
|
182
|
+
log("info", "");
|
|
183
|
+
log("info", "Claude Desktop config — add each agent as a separate MCP server:");
|
|
184
|
+
log("info", JSON.stringify({
|
|
185
|
+
mcpServers: Object.fromEntries(
|
|
186
|
+
config.agents.map((a) => [
|
|
187
|
+
a.name.toLowerCase().replace(/\s+/g, "-"),
|
|
188
|
+
{ url: `http://localhost:${a.port}/sse` },
|
|
189
|
+
]),
|
|
190
|
+
),
|
|
191
|
+
}, null, 2));
|
|
192
|
+
log("info", "");
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Fleet Dashboard Renderer ──────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
function renderFleetDashboard(events: FleetEvent[], config: FleetConfig): string {
|
|
199
|
+
const agentStats = new Map<string, { name: string; color: string; total: number; blocked: number; flagged: number; allowed: number; errors: number; port: number }>();
|
|
200
|
+
|
|
201
|
+
for (const a of config.agents) {
|
|
202
|
+
const color = a.color ?? "#888";
|
|
203
|
+
agentStats.set(a.agentId, { name: a.name, color, total: 0, blocked: 0, flagged: 0, allowed: 0, errors: 0, port: a.port });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const e of events) {
|
|
207
|
+
const stats = agentStats.get(e.agentId ?? "");
|
|
208
|
+
if (stats) {
|
|
209
|
+
stats.total++;
|
|
210
|
+
if (e.status === "blocked") stats.blocked++;
|
|
211
|
+
else if (e.status === "flagged") stats.flagged++;
|
|
212
|
+
else if (e.status === "error") stats.errors++;
|
|
213
|
+
else stats.allowed++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Agent cards
|
|
218
|
+
const agentCards = Array.from(agentStats.values())
|
|
219
|
+
.map((s) => `
|
|
220
|
+
<div style="border:1px solid #222;border-left:3px solid ${s.color};border-radius:6px;padding:12px 16px;min-width:200px">
|
|
221
|
+
<div style="font-size:14px;font-weight:600;color:${s.color}">${esc(s.name)}</div>
|
|
222
|
+
<div style="font-size:11px;color:#555;margin-top:2px">Port ${s.port}</div>
|
|
223
|
+
<div style="display:flex;gap:12px;margin-top:8px;font-size:12px">
|
|
224
|
+
<span style="color:#22c55e">${s.allowed} ok</span>
|
|
225
|
+
<span style="color:#ef4444">${s.blocked} blocked</span>
|
|
226
|
+
<span style="color:#f59e0b">${s.flagged} flagged</span>
|
|
227
|
+
<span style="color:#6b7280">${s.errors} err</span>
|
|
228
|
+
</div>
|
|
229
|
+
</div>`)
|
|
230
|
+
.join("");
|
|
231
|
+
|
|
232
|
+
// Event rows (last 100)
|
|
233
|
+
const last100 = events.slice(-100).reverse();
|
|
234
|
+
const rows = last100.map((e, i) => {
|
|
235
|
+
const stats = agentStats.get(e.agentId ?? "");
|
|
236
|
+
const color = stats?.color ?? "#888";
|
|
237
|
+
const agentName = stats?.name ?? e.agentId ?? "?";
|
|
238
|
+
const statusColor = e.status === "blocked" ? "#ef4444" : e.status === "flagged" ? "#f59e0b" : e.status === "error" ? "#6b7280" : "#22c55e";
|
|
239
|
+
const rules = e.matched_rules?.map((r) => `<span style="background:#2d1b69;color:#c4b5fd;padding:1px 5px;border-radius:3px;font-size:10px">${esc(r)}</span>`).join(" ") ?? "";
|
|
240
|
+
const time = e.timestamp.split("T")[1]?.slice(0, 12) ?? "";
|
|
241
|
+
|
|
242
|
+
return `
|
|
243
|
+
<tr style="cursor:pointer;border-bottom:1px solid #151515" onclick="toggleDetail(${i})">
|
|
244
|
+
<td style="padding:6px 10px;font-family:monospace;font-size:11px;color:#666">${esc(time)}</td>
|
|
245
|
+
<td style="padding:6px 10px"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${color};margin-right:6px"></span><span style="font-size:12px;color:${color}">${esc(agentName)}</span></td>
|
|
246
|
+
<td style="padding:6px 10px;font-weight:600;font-size:12px">${esc(e.tool)}</td>
|
|
247
|
+
<td style="padding:6px 10px;font-weight:600;color:${statusColor};font-size:12px">${e.status.toUpperCase()}</td>
|
|
248
|
+
<td style="padding:6px 10px;font-family:monospace;font-size:11px">${e.risk_score.toFixed(2)}</td>
|
|
249
|
+
<td style="padding:6px 10px">${rules}</td>
|
|
250
|
+
<td style="padding:6px 10px;font-family:monospace;font-size:11px">${e.latency_ms}ms</td>
|
|
251
|
+
</tr>
|
|
252
|
+
<tr id="detail-${i}" style="display:none">
|
|
253
|
+
<td colspan="7" style="padding:10px 16px;background:#0d0d14;border-bottom:1px solid #1a1a2e;font-size:12px;color:#888">
|
|
254
|
+
${e.reason ? `<div><strong>Reason:</strong> ${esc(e.reason)}</div>` : ""}
|
|
255
|
+
${e.reasoning ? `<div><strong>Reasoning:</strong> ${esc(e.reasoning)}</div>` : ""}
|
|
256
|
+
${e.session_flags?.length ? `<div><strong>Session Flags:</strong> ${e.session_flags.map((f) => `<span style="background:#422006;color:#fbbf24;padding:1px 5px;border-radius:3px;font-size:10px">${esc(f)}</span>`).join(" ")}</div>` : ""}
|
|
257
|
+
${e.scope_granted ? `<div><strong>Scope:</strong> <span style="background:#042f2e;color:#5eead4;padding:1px 5px;border-radius:3px;font-size:10px">${esc(e.scope_granted)}</span></div>` : ""}
|
|
258
|
+
<div style="margin-top:6px;font-family:monospace;font-size:11px;color:#555;max-height:120px;overflow:auto">${esc(e.params)}</div>
|
|
259
|
+
</td>
|
|
260
|
+
</tr>`;
|
|
261
|
+
}).join("");
|
|
262
|
+
|
|
263
|
+
const total = events.length;
|
|
264
|
+
const blocked = events.filter((e) => e.status === "blocked").length;
|
|
265
|
+
|
|
266
|
+
return `<!DOCTYPE html>
|
|
267
|
+
<html lang="en">
|
|
268
|
+
<head>
|
|
269
|
+
<meta charset="utf-8">
|
|
270
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
271
|
+
<title>Clampd Fleet Dashboard</title>
|
|
272
|
+
<style>
|
|
273
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
274
|
+
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:#0a0a0a; color:#e0e0e0; }
|
|
275
|
+
</style>
|
|
276
|
+
</head>
|
|
277
|
+
<body>
|
|
278
|
+
<div style="padding:20px 32px;border-bottom:1px solid #222;display:flex;align-items:center;gap:16px;flex-wrap:wrap">
|
|
279
|
+
<h1 style="font-size:20px;color:#fff">Clampd Fleet</h1>
|
|
280
|
+
<span style="padding:3px 10px;border-radius:4px;font-size:11px;font-weight:600;background:#1a472a;color:#4ade80">${config.agents.length} AGENTS</span>
|
|
281
|
+
<span style="font-size:13px;color:#888">${total} calls | ${blocked} blocked | ${total > 0 ? ((blocked / total) * 100).toFixed(1) : "0"}% threat rate</span>
|
|
282
|
+
<div style="margin-left:auto">
|
|
283
|
+
<button onclick="location.reload()" style="padding:5px 12px;border:1px solid #333;background:#111;color:#aaa;border-radius:4px;cursor:pointer;font-size:11px">Refresh</button>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<div style="display:flex;gap:12px;padding:16px 32px;border-bottom:1px solid #222;flex-wrap:wrap;overflow-x:auto">
|
|
288
|
+
${agentCards}
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div style="max-height:600px;overflow-y:auto">
|
|
292
|
+
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
|
293
|
+
<thead>
|
|
294
|
+
<tr style="background:#111;position:sticky;top:0;z-index:1">
|
|
295
|
+
<th style="text-align:left;padding:7px 10px;color:#666;font-size:10px;text-transform:uppercase">Time</th>
|
|
296
|
+
<th style="text-align:left;padding:7px 10px;color:#666;font-size:10px;text-transform:uppercase">Agent</th>
|
|
297
|
+
<th style="text-align:left;padding:7px 10px;color:#666;font-size:10px;text-transform:uppercase">Tool</th>
|
|
298
|
+
<th style="text-align:left;padding:7px 10px;color:#666;font-size:10px;text-transform:uppercase">Status</th>
|
|
299
|
+
<th style="text-align:left;padding:7px 10px;color:#666;font-size:10px;text-transform:uppercase">Risk</th>
|
|
300
|
+
<th style="text-align:left;padding:7px 10px;color:#666;font-size:10px;text-transform:uppercase">Rules</th>
|
|
301
|
+
<th style="text-align:left;padding:7px 10px;color:#666;font-size:10px;text-transform:uppercase">Latency</th>
|
|
302
|
+
</tr>
|
|
303
|
+
</thead>
|
|
304
|
+
<tbody>
|
|
305
|
+
${rows || '<tr><td colspan="7" style="padding:48px;text-align:center;color:#333">No events yet. Connect Claude Desktop to the agent SSE endpoints.</td></tr>'}
|
|
306
|
+
</tbody>
|
|
307
|
+
</table>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<script>
|
|
311
|
+
const evtSource = new EventSource('/events');
|
|
312
|
+
let newCount = 0;
|
|
313
|
+
evtSource.onmessage = function() {
|
|
314
|
+
newCount++;
|
|
315
|
+
const btn = document.querySelector('button');
|
|
316
|
+
if (btn) btn.textContent = 'Refresh (' + newCount + ' new)';
|
|
317
|
+
};
|
|
318
|
+
function toggleDetail(idx) {
|
|
319
|
+
const el = document.getElementById('detail-' + idx);
|
|
320
|
+
if (el) el.style.display = el.style.display === 'none' ? 'table-row' : 'none';
|
|
321
|
+
}
|
|
322
|
+
</script>
|
|
323
|
+
</body>
|
|
324
|
+
</html>`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function esc(s: string): string {
|
|
328
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
329
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Clampd MCP Proxy — standalone proxy that wraps any MCP server with
|
|
4
|
+
* Clampd's full security pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Users connect Claude Desktop (or any MCP client) to this proxy via SSE.
|
|
7
|
+
* The proxy intercepts every tool call, classifies it through ag-gateway,
|
|
8
|
+
* and only forwards allowed calls to the upstream MCP server.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* clampd-mcp-proxy \
|
|
12
|
+
* --upstream "npx -y @modelcontextprotocol/server-filesystem /tmp" \
|
|
13
|
+
* --gateway http://localhost:8080 \
|
|
14
|
+
* --api-key ag_test_acme_demo_2026 \
|
|
15
|
+
* --agent-id b0000001-0000-0000-0000-000000000001 \
|
|
16
|
+
* --port 3003
|
|
17
|
+
*
|
|
18
|
+
* Claude Desktop config (mcpServers):
|
|
19
|
+
* {
|
|
20
|
+
* "filesystem": {
|
|
21
|
+
* "url": "http://localhost:3003/sse"
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { parseArgs } from "node:util";
|
|
27
|
+
import { startProxy } from "./proxy.js";
|
|
28
|
+
import { startFleet } from "./fleet.js";
|
|
29
|
+
|
|
30
|
+
// ── CLI argument parsing ──────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function parseCli() {
|
|
33
|
+
const { values } = parseArgs({
|
|
34
|
+
options: {
|
|
35
|
+
upstream: { type: "string", short: "u" },
|
|
36
|
+
gateway: { type: "string", short: "g" },
|
|
37
|
+
"api-key": { type: "string", short: "k" },
|
|
38
|
+
"agent-id": { type: "string", short: "a" },
|
|
39
|
+
port: { type: "string", short: "p" },
|
|
40
|
+
secret: { type: "string", short: "s" },
|
|
41
|
+
"dry-run": { type: "boolean", default: false },
|
|
42
|
+
"scan-input": { type: "boolean", default: false },
|
|
43
|
+
"scan-output": { type: "boolean", default: false },
|
|
44
|
+
"check-response": { type: "boolean", default: false },
|
|
45
|
+
"demo-panel": { type: "boolean", default: false },
|
|
46
|
+
"fleet-config": { type: "string" },
|
|
47
|
+
verbose: { type: "boolean", short: "v", default: false },
|
|
48
|
+
help: { type: "boolean", short: "h", default: false },
|
|
49
|
+
},
|
|
50
|
+
strict: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (values.help) {
|
|
54
|
+
printUsage();
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fleet mode — start multiple proxies from config file
|
|
59
|
+
if (values["fleet-config"]) {
|
|
60
|
+
return { fleetConfig: values["fleet-config"], verbose: values.verbose! };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// CLI args take priority, then env vars, then defaults
|
|
64
|
+
const upstream = values.upstream ?? process.env.UPSTREAM_MCP;
|
|
65
|
+
const gateway = values.gateway ?? process.env.AG_GATEWAY_URL ?? "http://localhost:8080";
|
|
66
|
+
const apiKey = values["api-key"] ?? process.env.AG_API_KEY ?? "clmpd_demo_key";
|
|
67
|
+
const agentId = values["agent-id"] ?? process.env.AG_AGENT_ID;
|
|
68
|
+
const port = values.port ?? process.env.PORT ?? "3003";
|
|
69
|
+
const secret = values.secret ?? process.env.CLAMPD_AGENT_SECRET ?? process.env.JWT_SECRET;
|
|
70
|
+
|
|
71
|
+
// Feature flags: CLI flag OR env var
|
|
72
|
+
const scanInput = values["scan-input"] || process.env.CLAMPD_SCAN_INPUT === "true";
|
|
73
|
+
const scanOutput = values["scan-output"] || process.env.CLAMPD_SCAN_OUTPUT === "true";
|
|
74
|
+
const checkResponse = values["check-response"] || process.env.CLAMPD_CHECK_RESPONSE === "true";
|
|
75
|
+
const demoPanel = values["demo-panel"] || process.env.CLAMPD_DEMO_PANEL === "true";
|
|
76
|
+
|
|
77
|
+
if (!upstream) {
|
|
78
|
+
console.error("Error: --upstream is required (command to spawn the MCP server)");
|
|
79
|
+
console.error(" Set via --upstream flag or UPSTREAM_MCP env var");
|
|
80
|
+
printUsage();
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!agentId) {
|
|
85
|
+
console.error("Error: --agent-id is required");
|
|
86
|
+
console.error(" Set via --agent-id flag or AG_AGENT_ID env var");
|
|
87
|
+
printUsage();
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
upstream,
|
|
93
|
+
gateway,
|
|
94
|
+
apiKey,
|
|
95
|
+
agentId,
|
|
96
|
+
port: parseInt(port, 10),
|
|
97
|
+
secret,
|
|
98
|
+
dryRun: values["dry-run"]!,
|
|
99
|
+
scanInput,
|
|
100
|
+
scanOutput,
|
|
101
|
+
checkResponse,
|
|
102
|
+
demoPanel,
|
|
103
|
+
verbose: values.verbose!,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function printUsage(): void {
|
|
108
|
+
console.log(`
|
|
109
|
+
Clampd MCP Proxy — wraps any MCP server with Clampd security
|
|
110
|
+
|
|
111
|
+
Usage:
|
|
112
|
+
clampd-mcp-proxy --upstream <command> --agent-id <uuid> [options]
|
|
113
|
+
|
|
114
|
+
Required:
|
|
115
|
+
--upstream, -u Command to spawn the upstream MCP server
|
|
116
|
+
Example: "npx -y @modelcontextprotocol/server-filesystem /tmp"
|
|
117
|
+
--agent-id, -a Clampd agent UUID
|
|
118
|
+
|
|
119
|
+
Options:
|
|
120
|
+
--gateway, -g Clampd gateway URL (default: http://localhost:8080)
|
|
121
|
+
--api-key, -k Clampd API key (default: clmpd_demo_key)
|
|
122
|
+
--port, -p Port to listen on (default: 3003)
|
|
123
|
+
--secret, -s Signing secret (default: CLAMPD_AGENT_SECRET or JWT_SECRET env var)
|
|
124
|
+
--dry-run Classify but never forward (uses /v1/verify)
|
|
125
|
+
--scan-input Scan tool arguments for prompt injection / PII / secrets
|
|
126
|
+
--scan-output Scan tool responses for PII / secrets leakage
|
|
127
|
+
--check-response Validate responses against granted scope token
|
|
128
|
+
--demo-panel Show attack demo panel in dashboard
|
|
129
|
+
--fleet-config Path to fleet config JSON (multi-agent mode)
|
|
130
|
+
--verbose, -v Enable debug logging
|
|
131
|
+
--help, -h Show this help
|
|
132
|
+
|
|
133
|
+
Environment Variables:
|
|
134
|
+
UPSTREAM_MCP Upstream MCP server command
|
|
135
|
+
AG_GATEWAY_URL Gateway URL
|
|
136
|
+
AG_API_KEY API key
|
|
137
|
+
AG_AGENT_ID Agent UUID
|
|
138
|
+
CLAMPD_AGENT_SECRET Agent signing secret (ags_...)
|
|
139
|
+
JWT_SECRET Global JWT signing secret (fallback)
|
|
140
|
+
CLAMPD_SCAN_INPUT Enable input scanning (true/false)
|
|
141
|
+
CLAMPD_SCAN_OUTPUT Enable output scanning (true/false)
|
|
142
|
+
CLAMPD_CHECK_RESPONSE Enable response checking (true/false)
|
|
143
|
+
|
|
144
|
+
Claude Desktop config:
|
|
145
|
+
{
|
|
146
|
+
"mcpServers": {
|
|
147
|
+
"filesystem": {
|
|
148
|
+
"url": "http://localhost:3003/sse"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Main ──────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
async function main(): Promise<void> {
|
|
158
|
+
const args = parseCli();
|
|
159
|
+
|
|
160
|
+
// Fleet mode: start multiple proxies from config file
|
|
161
|
+
if ("fleetConfig" in args) {
|
|
162
|
+
const { setVerbose } = await import("./logger.js");
|
|
163
|
+
if (args.verbose) setVerbose(true);
|
|
164
|
+
await startFleet(args.fleetConfig as string);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await startProxy({
|
|
169
|
+
upstreamCommand: args.upstream,
|
|
170
|
+
gatewayUrl: args.gateway,
|
|
171
|
+
apiKey: args.apiKey,
|
|
172
|
+
agentId: args.agentId,
|
|
173
|
+
port: args.port,
|
|
174
|
+
secret: args.secret,
|
|
175
|
+
dryRun: args.dryRun,
|
|
176
|
+
scanInputEnabled: args.scanInput,
|
|
177
|
+
scanOutputEnabled: args.scanOutput,
|
|
178
|
+
checkResponse: args.checkResponse,
|
|
179
|
+
demoPanel: args.demoPanel,
|
|
180
|
+
verbose: args.verbose,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
main().catch((err) => {
|
|
185
|
+
console.error("Fatal error:", err);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
});
|