@solongate/proxy 0.1.1 → 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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +366 -7
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -117,6 +117,8 @@ Options:
117
117
  --rate-limit <n> Per-tool rate limit (calls/min)
118
118
  --global-rate-limit <n> Global rate limit (calls/min)
119
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)
120
122
  ```
121
123
 
122
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((resolve2) => {
1651
- this.queue.push(resolve2);
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
- log(`Result: ${result.isError ? "DENIED/ERROR" : "ALLOWED"}`);
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 main() {
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
- main();
2211
+ main2();
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.1.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": {