@solongate/proxy 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/dist/index.js +366 -7
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
**MCP Security Proxy** — Protect any MCP server with security policies, input validation, rate limiting, and audit logging. Zero code changes required.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
MCP Client ──(stdio)──> SolonGate Proxy ──(stdio)──> MCP Server
|
|
7
|
+
│
|
|
8
|
+
[rate limit]
|
|
9
|
+
[input guard]
|
|
10
|
+
[policy eval]
|
|
11
|
+
[audit log]
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
**Works with every MCP client:** Claude Code, Claude Desktop, Cursor, Windsurf, Cline, Zed, and any application that supports the Model Context Protocol over stdio.
|
|
15
|
+
|
|
14
16
|
## Quick Start
|
|
15
17
|
|
|
16
18
|
### Automatic Setup
|
|
@@ -21,7 +23,7 @@ Run this in your project directory (where your `.mcp.json` lives):
|
|
|
21
23
|
npx @solongate/proxy init --all
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
Restart
|
|
26
|
+
Restart your MCP client. Done.
|
|
25
27
|
|
|
26
28
|
### Manual Setup
|
|
27
29
|
|
|
@@ -115,6 +117,8 @@ Options:
|
|
|
115
117
|
--rate-limit <n> Per-tool rate limit (calls/min)
|
|
116
118
|
--global-rate-limit <n> Global rate limit (calls/min)
|
|
117
119
|
--config <file> Load full config from JSON file
|
|
120
|
+
--api-key <key> SolonGate Cloud API key (cloud policy + audit)
|
|
121
|
+
--api-url <url> Custom API URL (default: api.solongate.com)
|
|
118
122
|
```
|
|
119
123
|
|
|
120
124
|
## Restore Original Config
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,325 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/init.ts
|
|
8
|
+
var init_exports = {};
|
|
9
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, copyFileSync } from "fs";
|
|
10
|
+
import { resolve as resolve2, join, dirname } from "path";
|
|
11
|
+
import { createInterface } from "readline";
|
|
12
|
+
function findProxyPath() {
|
|
13
|
+
const candidates = [
|
|
14
|
+
resolve2(dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "index.js"),
|
|
15
|
+
resolve2("node_modules", "@solongate", "proxy", "dist", "index.js"),
|
|
16
|
+
resolve2("packages", "proxy", "dist", "index.js")
|
|
17
|
+
];
|
|
18
|
+
for (const p of candidates) {
|
|
19
|
+
if (existsSync2(p)) return p.replace(/\\/g, "/");
|
|
20
|
+
}
|
|
21
|
+
return "solongate-proxy";
|
|
22
|
+
}
|
|
23
|
+
function findConfigFile(explicitPath) {
|
|
24
|
+
if (explicitPath) {
|
|
25
|
+
if (existsSync2(explicitPath)) {
|
|
26
|
+
return { path: resolve2(explicitPath), type: "mcp" };
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
for (const searchPath of SEARCH_PATHS) {
|
|
31
|
+
const full = resolve2(searchPath);
|
|
32
|
+
if (existsSync2(full)) return { path: full, type: "mcp" };
|
|
33
|
+
}
|
|
34
|
+
for (const desktopPath of CLAUDE_DESKTOP_PATHS) {
|
|
35
|
+
if (existsSync2(desktopPath)) return { path: desktopPath, type: "claude-desktop" };
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function readConfig(filePath) {
|
|
40
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
41
|
+
const parsed = JSON.parse(content);
|
|
42
|
+
if (parsed.mcpServers) return parsed;
|
|
43
|
+
throw new Error(`Unrecognized config format in ${filePath}`);
|
|
44
|
+
}
|
|
45
|
+
function isAlreadyProtected(server) {
|
|
46
|
+
const cmdStr = [server.command, ...server.args ?? []].join(" ");
|
|
47
|
+
return cmdStr.includes("solongate") || cmdStr.includes("solongate-proxy");
|
|
48
|
+
}
|
|
49
|
+
function wrapServer(server, policy, proxyPath) {
|
|
50
|
+
return {
|
|
51
|
+
command: "node",
|
|
52
|
+
args: [
|
|
53
|
+
proxyPath,
|
|
54
|
+
"--policy",
|
|
55
|
+
policy,
|
|
56
|
+
"--verbose",
|
|
57
|
+
"--",
|
|
58
|
+
server.command,
|
|
59
|
+
...server.args ?? []
|
|
60
|
+
],
|
|
61
|
+
env: server.env
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function prompt(question) {
|
|
65
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
66
|
+
return new Promise((res) => {
|
|
67
|
+
rl.question(question, (answer) => {
|
|
68
|
+
rl.close();
|
|
69
|
+
res(answer.trim());
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function parseInitArgs(argv) {
|
|
74
|
+
const args = argv.slice(2);
|
|
75
|
+
const options = {
|
|
76
|
+
policy: "restricted",
|
|
77
|
+
all: false,
|
|
78
|
+
dryRun: false,
|
|
79
|
+
restore: false
|
|
80
|
+
};
|
|
81
|
+
for (let i = 0; i < args.length; i++) {
|
|
82
|
+
switch (args[i]) {
|
|
83
|
+
case "--config":
|
|
84
|
+
options.configPath = args[++i];
|
|
85
|
+
break;
|
|
86
|
+
case "--policy":
|
|
87
|
+
options.policy = args[++i];
|
|
88
|
+
break;
|
|
89
|
+
case "--all":
|
|
90
|
+
options.all = true;
|
|
91
|
+
break;
|
|
92
|
+
case "--dry-run":
|
|
93
|
+
options.dryRun = true;
|
|
94
|
+
break;
|
|
95
|
+
case "--restore":
|
|
96
|
+
options.restore = true;
|
|
97
|
+
break;
|
|
98
|
+
case "--help":
|
|
99
|
+
case "-h":
|
|
100
|
+
printHelp();
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!POLICY_PRESETS.includes(options.policy)) {
|
|
105
|
+
if (!existsSync2(resolve2(options.policy))) {
|
|
106
|
+
console.error(`Unknown policy: ${options.policy}`);
|
|
107
|
+
console.error(`Available presets: ${POLICY_PRESETS.join(", ")}`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return options;
|
|
112
|
+
}
|
|
113
|
+
function printHelp() {
|
|
114
|
+
const help = `
|
|
115
|
+
SolonGate Init \u2014 Protect your MCP servers in seconds
|
|
116
|
+
|
|
117
|
+
USAGE
|
|
118
|
+
solongate-init [options]
|
|
119
|
+
|
|
120
|
+
OPTIONS
|
|
121
|
+
--config <path> Path to MCP config file (default: auto-detect)
|
|
122
|
+
--policy <preset> Policy preset or JSON file (default: restricted)
|
|
123
|
+
Presets: ${POLICY_PRESETS.join(", ")}
|
|
124
|
+
--all Protect all servers without prompting
|
|
125
|
+
--dry-run Preview changes without writing
|
|
126
|
+
--restore Restore original config from backup
|
|
127
|
+
-h, --help Show this help message
|
|
128
|
+
|
|
129
|
+
EXAMPLES
|
|
130
|
+
solongate-init # Interactive setup
|
|
131
|
+
solongate-init --all # Protect everything
|
|
132
|
+
solongate-init --policy read-only # Use read-only policy
|
|
133
|
+
solongate-init --dry-run # Preview changes
|
|
134
|
+
solongate-init --restore # Undo protection
|
|
135
|
+
|
|
136
|
+
POLICY PRESETS
|
|
137
|
+
restricted Block shell/exec/eval, allow reads and writes (recommended)
|
|
138
|
+
read-only Only allow read/list/get/search/query operations
|
|
139
|
+
permissive Allow everything (monitoring + audit only)
|
|
140
|
+
deny-all Block all tool calls
|
|
141
|
+
`;
|
|
142
|
+
console.error(help);
|
|
143
|
+
}
|
|
144
|
+
async function main() {
|
|
145
|
+
const options = parseInitArgs(process.argv);
|
|
146
|
+
console.error("");
|
|
147
|
+
console.error(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
148
|
+
console.error(" \u2551 SolonGate \u2014 Init Setup \u2551");
|
|
149
|
+
console.error(" \u2551 Secure your MCP servers in seconds \u2551");
|
|
150
|
+
console.error(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
151
|
+
console.error("");
|
|
152
|
+
const configInfo = findConfigFile(options.configPath);
|
|
153
|
+
if (!configInfo) {
|
|
154
|
+
console.error(" No MCP config file found.");
|
|
155
|
+
console.error(" Searched: .mcp.json, mcp.json, Claude Desktop config");
|
|
156
|
+
console.error("");
|
|
157
|
+
console.error(" Create a .mcp.json file first, or specify --config <path>");
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
console.error(` Config: ${configInfo.path}`);
|
|
161
|
+
console.error(` Type: ${configInfo.type === "claude-desktop" ? "Claude Desktop" : "MCP JSON"}`);
|
|
162
|
+
console.error("");
|
|
163
|
+
const backupPath = configInfo.path + ".solongate-backup";
|
|
164
|
+
if (options.restore) {
|
|
165
|
+
if (!existsSync2(backupPath)) {
|
|
166
|
+
console.error(" No backup found. Nothing to restore.");
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
copyFileSync(backupPath, configInfo.path);
|
|
170
|
+
console.error(" Restored original config from backup.");
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
const config = readConfig(configInfo.path);
|
|
174
|
+
const serverNames = Object.keys(config.mcpServers);
|
|
175
|
+
if (serverNames.length === 0) {
|
|
176
|
+
console.error(" No MCP servers found in config.");
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
console.error(` Found ${serverNames.length} MCP server(s):`);
|
|
180
|
+
console.error("");
|
|
181
|
+
const toProtect = [];
|
|
182
|
+
const alreadyProtected = [];
|
|
183
|
+
const skipped = [];
|
|
184
|
+
for (const name of serverNames) {
|
|
185
|
+
const server = config.mcpServers[name];
|
|
186
|
+
if (isAlreadyProtected(server)) {
|
|
187
|
+
alreadyProtected.push(name);
|
|
188
|
+
console.error(` [protected] ${name}`);
|
|
189
|
+
} else {
|
|
190
|
+
console.error(` [exposed] ${name} \u2192 ${server.command} ${(server.args ?? []).join(" ")}`);
|
|
191
|
+
if (options.all) {
|
|
192
|
+
toProtect.push(name);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
console.error("");
|
|
197
|
+
if (alreadyProtected.length === serverNames.length) {
|
|
198
|
+
console.error(" All servers are already protected by SolonGate!");
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
if (!options.all) {
|
|
202
|
+
const exposed = serverNames.filter((n) => !alreadyProtected.includes(n));
|
|
203
|
+
for (const name of exposed) {
|
|
204
|
+
const answer = await prompt(` Protect "${name}"? [Y/n] `);
|
|
205
|
+
if (answer === "" || answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
206
|
+
toProtect.push(name);
|
|
207
|
+
} else {
|
|
208
|
+
skipped.push(name);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (toProtect.length === 0) {
|
|
213
|
+
console.error(" No servers selected for protection.");
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
console.error("");
|
|
217
|
+
console.error(` Policy: ${options.policy}`);
|
|
218
|
+
console.error(` Protecting: ${toProtect.join(", ")}`);
|
|
219
|
+
console.error("");
|
|
220
|
+
const proxyPath = findProxyPath();
|
|
221
|
+
const newConfig = { mcpServers: {} };
|
|
222
|
+
for (const name of serverNames) {
|
|
223
|
+
if (toProtect.includes(name)) {
|
|
224
|
+
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy, proxyPath);
|
|
225
|
+
} else {
|
|
226
|
+
newConfig.mcpServers[name] = config.mcpServers[name];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (options.dryRun) {
|
|
230
|
+
console.error(" --- DRY RUN (no changes written) ---");
|
|
231
|
+
console.error("");
|
|
232
|
+
console.error(" New config:");
|
|
233
|
+
console.error(JSON.stringify(newConfig, null, 2));
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
if (!existsSync2(backupPath)) {
|
|
237
|
+
copyFileSync(configInfo.path, backupPath);
|
|
238
|
+
console.error(` Backup: ${backupPath}`);
|
|
239
|
+
}
|
|
240
|
+
if (configInfo.type === "claude-desktop") {
|
|
241
|
+
const original = JSON.parse(readFileSync2(configInfo.path, "utf-8"));
|
|
242
|
+
original.mcpServers = newConfig.mcpServers;
|
|
243
|
+
writeFileSync(configInfo.path, JSON.stringify(original, null, 2) + "\n");
|
|
244
|
+
} else {
|
|
245
|
+
writeFileSync(configInfo.path, JSON.stringify(newConfig, null, 2) + "\n");
|
|
246
|
+
}
|
|
247
|
+
console.error(" Config updated!");
|
|
248
|
+
console.error("");
|
|
249
|
+
console.error(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
250
|
+
console.error(" \u2502 Your MCP servers are now protected by \u2502");
|
|
251
|
+
console.error(" \u2502 SolonGate security policies. \u2502");
|
|
252
|
+
console.error(" \u2502 \u2502");
|
|
253
|
+
console.error(" \u2502 Restart your MCP client (Claude Code \u2502");
|
|
254
|
+
console.error(" \u2502 or Claude Desktop) to apply changes. \u2502");
|
|
255
|
+
console.error(" \u2502 \u2502");
|
|
256
|
+
console.error(" \u2502 To undo: solongate-init --restore \u2502");
|
|
257
|
+
console.error(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
258
|
+
console.error("");
|
|
259
|
+
for (const name of toProtect) {
|
|
260
|
+
console.error(` \u2713 ${name} \u2014 protected (${options.policy})`);
|
|
261
|
+
}
|
|
262
|
+
for (const name of alreadyProtected) {
|
|
263
|
+
console.error(` \u25CF ${name} \u2014 already protected`);
|
|
264
|
+
}
|
|
265
|
+
for (const name of skipped) {
|
|
266
|
+
console.error(` \u25CB ${name} \u2014 skipped`);
|
|
267
|
+
}
|
|
268
|
+
console.error("");
|
|
269
|
+
}
|
|
270
|
+
var POLICY_PRESETS, SEARCH_PATHS, CLAUDE_DESKTOP_PATHS;
|
|
271
|
+
var init_init = __esm({
|
|
272
|
+
"src/init.ts"() {
|
|
273
|
+
"use strict";
|
|
274
|
+
POLICY_PRESETS = ["restricted", "read-only", "permissive", "deny-all"];
|
|
275
|
+
SEARCH_PATHS = [
|
|
276
|
+
".mcp.json",
|
|
277
|
+
"mcp.json",
|
|
278
|
+
".claude/mcp.json"
|
|
279
|
+
];
|
|
280
|
+
CLAUDE_DESKTOP_PATHS = process.platform === "win32" ? [join(process.env["APPDATA"] ?? "", "Claude", "claude_desktop_config.json")] : process.platform === "darwin" ? [join(process.env["HOME"] ?? "", "Library", "Application Support", "Claude", "claude_desktop_config.json")] : [join(process.env["HOME"] ?? "", ".config", "claude", "claude_desktop_config.json")];
|
|
281
|
+
main().catch((err) => {
|
|
282
|
+
console.error(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
2
287
|
|
|
3
288
|
// src/config.ts
|
|
4
289
|
import { readFileSync, existsSync } from "fs";
|
|
5
290
|
import { resolve } from "path";
|
|
291
|
+
async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
|
|
292
|
+
const url = `${apiUrl}/api/v1/policies/${policyId ?? "default"}`;
|
|
293
|
+
const res = await fetch(url, {
|
|
294
|
+
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
295
|
+
});
|
|
296
|
+
if (!res.ok) {
|
|
297
|
+
const body = await res.text().catch(() => "");
|
|
298
|
+
throw new Error(`Failed to fetch policy from cloud (${res.status}): ${body}`);
|
|
299
|
+
}
|
|
300
|
+
const data = await res.json();
|
|
301
|
+
return {
|
|
302
|
+
id: String(data.id ?? "cloud"),
|
|
303
|
+
name: String(data.name ?? "Cloud Policy"),
|
|
304
|
+
version: Number(data._version ?? 1),
|
|
305
|
+
rules: data.rules ?? [],
|
|
306
|
+
createdAt: String(data._created_at ?? ""),
|
|
307
|
+
updatedAt: ""
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async function sendAuditLog(apiKey, apiUrl, entry) {
|
|
311
|
+
try {
|
|
312
|
+
await fetch(`${apiUrl}/api/v1/audit-logs`, {
|
|
313
|
+
method: "POST",
|
|
314
|
+
headers: {
|
|
315
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
316
|
+
"Content-Type": "application/json"
|
|
317
|
+
},
|
|
318
|
+
body: JSON.stringify(entry)
|
|
319
|
+
});
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
}
|
|
6
323
|
var PRESETS = {
|
|
7
324
|
restricted: {
|
|
8
325
|
id: "restricted",
|
|
@@ -185,6 +502,8 @@ function parseArgs(argv) {
|
|
|
185
502
|
let rateLimitPerTool;
|
|
186
503
|
let globalRateLimit;
|
|
187
504
|
let configFile;
|
|
505
|
+
let apiKey;
|
|
506
|
+
let apiUrl;
|
|
188
507
|
let separatorIndex = args.indexOf("--");
|
|
189
508
|
const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
|
|
190
509
|
const upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
|
|
@@ -211,6 +530,12 @@ function parseArgs(argv) {
|
|
|
211
530
|
case "--config":
|
|
212
531
|
configFile = flags[++i];
|
|
213
532
|
break;
|
|
533
|
+
case "--api-key":
|
|
534
|
+
apiKey = flags[++i];
|
|
535
|
+
break;
|
|
536
|
+
case "--api-url":
|
|
537
|
+
apiUrl = flags[++i];
|
|
538
|
+
break;
|
|
214
539
|
}
|
|
215
540
|
}
|
|
216
541
|
if (configFile) {
|
|
@@ -227,7 +552,9 @@ function parseArgs(argv) {
|
|
|
227
552
|
verbose: fileConfig.verbose ?? verbose,
|
|
228
553
|
validateInput: fileConfig.validateInput ?? validateInput,
|
|
229
554
|
rateLimitPerTool: fileConfig.rateLimitPerTool ?? rateLimitPerTool,
|
|
230
|
-
globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit
|
|
555
|
+
globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit,
|
|
556
|
+
apiKey: apiKey ?? fileConfig.apiKey,
|
|
557
|
+
apiUrl: apiUrl ?? fileConfig.apiUrl
|
|
231
558
|
};
|
|
232
559
|
}
|
|
233
560
|
if (upstreamArgs.length === 0) {
|
|
@@ -247,7 +574,9 @@ function parseArgs(argv) {
|
|
|
247
574
|
verbose,
|
|
248
575
|
validateInput,
|
|
249
576
|
rateLimitPerTool,
|
|
250
|
-
globalRateLimit
|
|
577
|
+
globalRateLimit,
|
|
578
|
+
apiKey,
|
|
579
|
+
apiUrl
|
|
251
580
|
};
|
|
252
581
|
}
|
|
253
582
|
|
|
@@ -1647,8 +1976,8 @@ var Mutex = class {
|
|
|
1647
1976
|
this.locked = true;
|
|
1648
1977
|
return;
|
|
1649
1978
|
}
|
|
1650
|
-
return new Promise((
|
|
1651
|
-
this.queue.push(
|
|
1979
|
+
return new Promise((resolve3) => {
|
|
1980
|
+
this.queue.push(resolve3);
|
|
1652
1981
|
});
|
|
1653
1982
|
}
|
|
1654
1983
|
release() {
|
|
@@ -1689,6 +2018,17 @@ var SolonGateProxy = class {
|
|
|
1689
2018
|
*/
|
|
1690
2019
|
async start() {
|
|
1691
2020
|
log("Starting SolonGate Proxy...");
|
|
2021
|
+
if (this.config.apiKey) {
|
|
2022
|
+
const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
|
|
2023
|
+
log(`Cloud API: ${apiUrl}`);
|
|
2024
|
+
try {
|
|
2025
|
+
const cloudPolicy = await fetchCloudPolicy(this.config.apiKey, apiUrl);
|
|
2026
|
+
this.config.policy = cloudPolicy;
|
|
2027
|
+
log(`Loaded cloud policy: ${cloudPolicy.name} (${cloudPolicy.rules.length} rules)`);
|
|
2028
|
+
} catch (err) {
|
|
2029
|
+
log(`Cloud policy fetch failed, using local policy: ${err instanceof Error ? err.message : String(err)}`);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
1692
2032
|
log(`Policy: ${this.config.policy.name} (${this.config.policy.rules.length} rules)`);
|
|
1693
2033
|
log(`Upstream: ${this.config.upstream.command} ${(this.config.upstream.args ?? []).join(" ")}`);
|
|
1694
2034
|
await this.connectUpstream();
|
|
@@ -1755,6 +2095,7 @@ var SolonGateProxy = class {
|
|
|
1755
2095
|
const { name, arguments: args } = request.params;
|
|
1756
2096
|
log(`Tool call: ${name}`);
|
|
1757
2097
|
await this.callMutex.acquire();
|
|
2098
|
+
const startTime = Date.now();
|
|
1758
2099
|
try {
|
|
1759
2100
|
const result = await this.gate.executeToolCall(
|
|
1760
2101
|
{ name, arguments: args ?? {} },
|
|
@@ -1767,7 +2108,19 @@ var SolonGateProxy = class {
|
|
|
1767
2108
|
return upstreamResult;
|
|
1768
2109
|
}
|
|
1769
2110
|
);
|
|
1770
|
-
|
|
2111
|
+
const decision = result.isError ? "DENY" : "ALLOW";
|
|
2112
|
+
const evaluationTimeMs = Date.now() - startTime;
|
|
2113
|
+
log(`Result: ${decision} (${evaluationTimeMs}ms)`);
|
|
2114
|
+
if (this.config.apiKey) {
|
|
2115
|
+
const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
|
|
2116
|
+
sendAuditLog(this.config.apiKey, apiUrl, {
|
|
2117
|
+
tool: name,
|
|
2118
|
+
arguments: args ?? {},
|
|
2119
|
+
decision,
|
|
2120
|
+
reason: result.isError ? result.content[0]?.text ?? "denied" : "allowed",
|
|
2121
|
+
evaluationTimeMs
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
1771
2124
|
return {
|
|
1772
2125
|
content: [...result.content],
|
|
1773
2126
|
isError: result.isError
|
|
@@ -1837,7 +2190,13 @@ console.error = (...args) => {
|
|
|
1837
2190
|
process.stderr.write(`[SolonGate ERROR] ${args.map(String).join(" ")}
|
|
1838
2191
|
`);
|
|
1839
2192
|
};
|
|
1840
|
-
async function
|
|
2193
|
+
async function main2() {
|
|
2194
|
+
const subcommand = process.argv[2];
|
|
2195
|
+
if (subcommand === "init") {
|
|
2196
|
+
process.argv.splice(2, 1);
|
|
2197
|
+
await Promise.resolve().then(() => (init_init(), init_exports));
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
1841
2200
|
try {
|
|
1842
2201
|
const config = parseArgs(process.argv);
|
|
1843
2202
|
const proxy = new SolonGateProxy(config);
|
|
@@ -1849,4 +2208,4 @@ async function main() {
|
|
|
1849
2208
|
process.exit(1);
|
|
1850
2209
|
}
|
|
1851
2210
|
}
|
|
1852
|
-
|
|
2211
|
+
main2();
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "MCP security proxy — protect any MCP server with policies, input validation, rate limiting, and audit logging. Zero code changes required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"solongate-proxy": "./dist/index.js",
|
|
8
|
-
"solongate-init": "./dist/init.js"
|
|
8
|
+
"solongate-init": "./dist/init.js",
|
|
9
|
+
"proxy": "./dist/index.js"
|
|
9
10
|
},
|
|
10
11
|
"main": "./dist/index.js",
|
|
11
12
|
"exports": {
|