@solongate/proxy 0.1.1 → 0.1.3
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 +2 -0
- package/dist/create.js +247 -0
- package/dist/index.js +1716 -349
- package/dist/inject.js +339 -0
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -1,270 +1,885 @@
|
|
|
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
|
+
};
|
|
2
6
|
|
|
3
|
-
// src/
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
id: "deny-exec",
|
|
27
|
-
description: "Block command execution",
|
|
28
|
-
effect: "DENY",
|
|
29
|
-
priority: 101,
|
|
30
|
-
toolPattern: "*exec*",
|
|
31
|
-
permission: "EXECUTE",
|
|
32
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
33
|
-
enabled: true,
|
|
34
|
-
createdAt: "",
|
|
35
|
-
updatedAt: ""
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
id: "deny-eval",
|
|
39
|
-
description: "Block code eval",
|
|
40
|
-
effect: "DENY",
|
|
41
|
-
priority: 102,
|
|
42
|
-
toolPattern: "*eval*",
|
|
43
|
-
permission: "EXECUTE",
|
|
44
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
45
|
-
enabled: true,
|
|
46
|
-
createdAt: "",
|
|
47
|
-
updatedAt: ""
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: "allow-rest",
|
|
51
|
-
description: "Allow all other tools",
|
|
52
|
-
effect: "ALLOW",
|
|
53
|
-
priority: 1e3,
|
|
54
|
-
toolPattern: "*",
|
|
55
|
-
permission: "EXECUTE",
|
|
56
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
57
|
-
enabled: true,
|
|
58
|
-
createdAt: "",
|
|
59
|
-
updatedAt: ""
|
|
60
|
-
}
|
|
61
|
-
],
|
|
62
|
-
createdAt: "",
|
|
63
|
-
updatedAt: ""
|
|
64
|
-
},
|
|
65
|
-
"read-only": {
|
|
66
|
-
id: "read-only",
|
|
67
|
-
name: "Read Only",
|
|
68
|
-
description: "Only allows read operations, blocks writes and execution",
|
|
69
|
-
version: 1,
|
|
70
|
-
rules: [
|
|
71
|
-
{
|
|
72
|
-
id: "allow-read",
|
|
73
|
-
description: "Allow read tools",
|
|
74
|
-
effect: "ALLOW",
|
|
75
|
-
priority: 100,
|
|
76
|
-
toolPattern: "*read*",
|
|
77
|
-
permission: "EXECUTE",
|
|
78
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
79
|
-
enabled: true,
|
|
80
|
-
createdAt: "",
|
|
81
|
-
updatedAt: ""
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
id: "allow-list",
|
|
85
|
-
description: "Allow list tools",
|
|
86
|
-
effect: "ALLOW",
|
|
87
|
-
priority: 101,
|
|
88
|
-
toolPattern: "*list*",
|
|
89
|
-
permission: "EXECUTE",
|
|
90
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
91
|
-
enabled: true,
|
|
92
|
-
createdAt: "",
|
|
93
|
-
updatedAt: ""
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: "allow-get",
|
|
97
|
-
description: "Allow get tools",
|
|
98
|
-
effect: "ALLOW",
|
|
99
|
-
priority: 102,
|
|
100
|
-
toolPattern: "*get*",
|
|
101
|
-
permission: "EXECUTE",
|
|
102
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
103
|
-
enabled: true,
|
|
104
|
-
createdAt: "",
|
|
105
|
-
updatedAt: ""
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
id: "allow-search",
|
|
109
|
-
description: "Allow search tools",
|
|
110
|
-
effect: "ALLOW",
|
|
111
|
-
priority: 103,
|
|
112
|
-
toolPattern: "*search*",
|
|
113
|
-
permission: "EXECUTE",
|
|
114
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
115
|
-
enabled: true,
|
|
116
|
-
createdAt: "",
|
|
117
|
-
updatedAt: ""
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: "allow-query",
|
|
121
|
-
description: "Allow query tools",
|
|
122
|
-
effect: "ALLOW",
|
|
123
|
-
priority: 104,
|
|
124
|
-
toolPattern: "*query*",
|
|
125
|
-
permission: "EXECUTE",
|
|
126
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
127
|
-
enabled: true,
|
|
128
|
-
createdAt: "",
|
|
129
|
-
updatedAt: ""
|
|
130
|
-
}
|
|
131
|
-
],
|
|
132
|
-
createdAt: "",
|
|
133
|
-
updatedAt: ""
|
|
134
|
-
},
|
|
135
|
-
permissive: {
|
|
136
|
-
id: "permissive",
|
|
137
|
-
name: "Permissive",
|
|
138
|
-
description: "Allows all tool calls (monitoring only)",
|
|
139
|
-
version: 1,
|
|
140
|
-
rules: [
|
|
141
|
-
{
|
|
142
|
-
id: "allow-all",
|
|
143
|
-
description: "Allow all",
|
|
144
|
-
effect: "ALLOW",
|
|
145
|
-
priority: 1e3,
|
|
146
|
-
toolPattern: "*",
|
|
147
|
-
permission: "EXECUTE",
|
|
148
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
149
|
-
enabled: true,
|
|
150
|
-
createdAt: "",
|
|
151
|
-
updatedAt: ""
|
|
152
|
-
}
|
|
153
|
-
],
|
|
154
|
-
createdAt: "",
|
|
155
|
-
updatedAt: ""
|
|
156
|
-
},
|
|
157
|
-
"deny-all": {
|
|
158
|
-
id: "deny-all",
|
|
159
|
-
name: "Deny All",
|
|
160
|
-
description: "Blocks all tool calls",
|
|
161
|
-
version: 1,
|
|
162
|
-
rules: [],
|
|
163
|
-
createdAt: "",
|
|
164
|
-
updatedAt: ""
|
|
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;
|
|
165
29
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (PRESETS[source]) return PRESETS[source];
|
|
170
|
-
const filePath = resolve(source);
|
|
171
|
-
if (existsSync(filePath)) {
|
|
172
|
-
const content = readFileSync(filePath, "utf-8");
|
|
173
|
-
return JSON.parse(content);
|
|
30
|
+
for (const searchPath of SEARCH_PATHS) {
|
|
31
|
+
const full = resolve2(searchPath);
|
|
32
|
+
if (existsSync2(full)) return { path: full, type: "mcp" };
|
|
174
33
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
34
|
+
for (const desktopPath of CLAUDE_DESKTOP_PATHS) {
|
|
35
|
+
if (existsSync2(desktopPath)) return { path: desktopPath, type: "claude-desktop" };
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
178
38
|
}
|
|
179
|
-
function
|
|
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) {
|
|
180
74
|
const args = argv.slice(2);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
let
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
for (let i = 0; i < flags.length; i++) {
|
|
192
|
-
switch (flags[i]) {
|
|
193
|
-
case "--policy":
|
|
194
|
-
policySource = flags[++i];
|
|
195
|
-
break;
|
|
196
|
-
case "--name":
|
|
197
|
-
name = flags[++i];
|
|
198
|
-
break;
|
|
199
|
-
case "--verbose":
|
|
200
|
-
verbose = true;
|
|
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];
|
|
201
85
|
break;
|
|
202
|
-
case "--
|
|
203
|
-
|
|
86
|
+
case "--policy":
|
|
87
|
+
options.policy = args[++i];
|
|
204
88
|
break;
|
|
205
|
-
case "--
|
|
206
|
-
|
|
89
|
+
case "--all":
|
|
90
|
+
options.all = true;
|
|
207
91
|
break;
|
|
208
|
-
case "--
|
|
209
|
-
|
|
92
|
+
case "--dry-run":
|
|
93
|
+
options.dryRun = true;
|
|
210
94
|
break;
|
|
211
|
-
case "--
|
|
212
|
-
|
|
95
|
+
case "--restore":
|
|
96
|
+
options.restore = true;
|
|
213
97
|
break;
|
|
98
|
+
case "--help":
|
|
99
|
+
case "-h":
|
|
100
|
+
printHelp();
|
|
101
|
+
process.exit(0);
|
|
214
102
|
}
|
|
215
103
|
}
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
throw new Error('Config file must include "upstream" with at least "command"');
|
|
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);
|
|
222
109
|
}
|
|
223
|
-
return {
|
|
224
|
-
upstream: fileConfig.upstream,
|
|
225
|
-
policy: loadPolicy(fileConfig.policy ?? policySource),
|
|
226
|
-
name: fileConfig.name ?? name,
|
|
227
|
-
verbose: fileConfig.verbose ?? verbose,
|
|
228
|
-
validateInput: fileConfig.validateInput ?? validateInput,
|
|
229
|
-
rateLimitPerTool: fileConfig.rateLimitPerTool ?? rateLimitPerTool,
|
|
230
|
-
globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
if (upstreamArgs.length === 0) {
|
|
234
|
-
throw new Error(
|
|
235
|
-
"No upstream server command provided.\n\nUsage: solongate-proxy [options] -- <command> [args...]\n\nExamples:\n solongate-proxy -- node my-server.js\n solongate-proxy --policy restricted -- npx @openclaw/server\n solongate-proxy --config solongate.json\n"
|
|
236
|
-
);
|
|
237
110
|
}
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
upstream: {
|
|
241
|
-
command,
|
|
242
|
-
args: commandArgs,
|
|
243
|
-
env: { ...process.env }
|
|
244
|
-
},
|
|
245
|
-
policy: loadPolicy(policySource),
|
|
246
|
-
name,
|
|
247
|
-
verbose,
|
|
248
|
-
validateInput,
|
|
249
|
-
rateLimitPerTool,
|
|
250
|
-
globalRateLimit
|
|
251
|
-
};
|
|
111
|
+
return options;
|
|
252
112
|
}
|
|
113
|
+
function printHelp() {
|
|
114
|
+
const help = `
|
|
115
|
+
SolonGate Init \u2014 Protect your MCP servers in seconds
|
|
253
116
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
});
|
|
287
|
+
|
|
288
|
+
// src/inject.ts
|
|
289
|
+
var inject_exports = {};
|
|
290
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, copyFileSync as copyFileSync2 } from "fs";
|
|
291
|
+
import { resolve as resolve3 } from "path";
|
|
292
|
+
import { execSync } from "child_process";
|
|
293
|
+
function parseInjectArgs(argv) {
|
|
294
|
+
const args = argv.slice(2);
|
|
295
|
+
const opts = {
|
|
296
|
+
dryRun: false,
|
|
297
|
+
restore: false,
|
|
298
|
+
skipInstall: false
|
|
299
|
+
};
|
|
300
|
+
for (let i = 0; i < args.length; i++) {
|
|
301
|
+
switch (args[i]) {
|
|
302
|
+
case "--file":
|
|
303
|
+
opts.file = args[++i];
|
|
304
|
+
break;
|
|
305
|
+
case "--dry-run":
|
|
306
|
+
opts.dryRun = true;
|
|
307
|
+
break;
|
|
308
|
+
case "--restore":
|
|
309
|
+
opts.restore = true;
|
|
310
|
+
break;
|
|
311
|
+
case "--skip-install":
|
|
312
|
+
opts.skipInstall = true;
|
|
313
|
+
break;
|
|
314
|
+
case "--help":
|
|
315
|
+
case "-h":
|
|
316
|
+
printHelp2();
|
|
317
|
+
process.exit(0);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return opts;
|
|
321
|
+
}
|
|
322
|
+
function printHelp2() {
|
|
323
|
+
log2(`
|
|
324
|
+
SolonGate Inject \u2014 Add security to your MCP server in seconds
|
|
325
|
+
|
|
326
|
+
USAGE
|
|
327
|
+
npx @solongate/proxy inject [options]
|
|
328
|
+
|
|
329
|
+
OPTIONS
|
|
330
|
+
--file <path> Entry file to modify (default: auto-detect)
|
|
331
|
+
--dry-run Preview changes without writing
|
|
332
|
+
--restore Restore original file from backup
|
|
333
|
+
--skip-install Don't install SDK package
|
|
334
|
+
-h, --help Show this help message
|
|
335
|
+
|
|
336
|
+
EXAMPLES
|
|
337
|
+
npx @solongate/proxy inject # Auto-detect and inject
|
|
338
|
+
npx @solongate/proxy inject --file src/main.ts # Specify entry file
|
|
339
|
+
npx @solongate/proxy inject --dry-run # Preview changes
|
|
340
|
+
npx @solongate/proxy inject --restore # Undo injection
|
|
341
|
+
|
|
342
|
+
WHAT IT DOES
|
|
343
|
+
Replaces McpServer with SecureMcpServer (2 lines changed)
|
|
344
|
+
All tool() calls are automatically protected by SolonGate.
|
|
345
|
+
`);
|
|
346
|
+
}
|
|
347
|
+
function log2(msg) {
|
|
348
|
+
process.stderr.write(msg + "\n");
|
|
349
|
+
}
|
|
350
|
+
function detectProject() {
|
|
351
|
+
if (!existsSync3(resolve3("package.json"))) return false;
|
|
352
|
+
try {
|
|
353
|
+
const pkg = JSON.parse(readFileSync3(resolve3("package.json"), "utf-8"));
|
|
354
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
355
|
+
return !!(allDeps["@modelcontextprotocol/sdk"] || allDeps["@modelcontextprotocol/server"]);
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function findTsEntryFile() {
|
|
361
|
+
try {
|
|
362
|
+
const pkg = JSON.parse(readFileSync3(resolve3("package.json"), "utf-8"));
|
|
363
|
+
if (pkg.bin) {
|
|
364
|
+
const binPath = typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin)[0];
|
|
365
|
+
if (typeof binPath === "string") {
|
|
366
|
+
const srcPath = binPath.replace(/^\.\/dist\//, "./src/").replace(/\.js$/, ".ts");
|
|
367
|
+
if (existsSync3(resolve3(srcPath))) return resolve3(srcPath);
|
|
368
|
+
if (existsSync3(resolve3(binPath))) return resolve3(binPath);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (pkg.main) {
|
|
372
|
+
const srcPath = pkg.main.replace(/^\.\/dist\//, "./src/").replace(/\.js$/, ".ts");
|
|
373
|
+
if (existsSync3(resolve3(srcPath))) return resolve3(srcPath);
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
const candidates = [
|
|
378
|
+
"src/index.ts",
|
|
379
|
+
"src/server.ts",
|
|
380
|
+
"src/main.ts",
|
|
381
|
+
"index.ts",
|
|
382
|
+
"server.ts",
|
|
383
|
+
"main.ts"
|
|
384
|
+
];
|
|
385
|
+
for (const c of candidates) {
|
|
386
|
+
const full = resolve3(c);
|
|
387
|
+
if (existsSync3(full)) {
|
|
388
|
+
try {
|
|
389
|
+
const content = readFileSync3(full, "utf-8");
|
|
390
|
+
if (content.includes("McpServer") || content.includes("McpServer")) {
|
|
391
|
+
return full;
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
for (const c of candidates) {
|
|
398
|
+
if (existsSync3(resolve3(c))) return resolve3(c);
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
function detectPackageManager() {
|
|
403
|
+
if (existsSync3(resolve3("pnpm-lock.yaml"))) return "pnpm";
|
|
404
|
+
if (existsSync3(resolve3("yarn.lock"))) return "yarn";
|
|
405
|
+
return "npm";
|
|
406
|
+
}
|
|
407
|
+
function installSdk() {
|
|
408
|
+
try {
|
|
409
|
+
const pkg = JSON.parse(readFileSync3(resolve3("package.json"), "utf-8"));
|
|
410
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
411
|
+
if (allDeps["@solongate/sdk"]) {
|
|
412
|
+
log2(" @solongate/sdk already installed");
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
const pm = detectPackageManager();
|
|
418
|
+
const cmd = pm === "yarn" ? "yarn add @solongate/sdk" : `${pm} install @solongate/sdk`;
|
|
419
|
+
log2(` Installing @solongate/sdk via ${pm}...`);
|
|
420
|
+
try {
|
|
421
|
+
execSync(cmd, { stdio: "pipe", cwd: process.cwd() });
|
|
422
|
+
return true;
|
|
423
|
+
} catch (err) {
|
|
424
|
+
log2(` Failed to install: ${err instanceof Error ? err.message : String(err)}`);
|
|
425
|
+
log2(" You can install manually: npm install @solongate/sdk");
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function injectTypeScript(filePath) {
|
|
430
|
+
const original = readFileSync3(filePath, "utf-8");
|
|
431
|
+
const changes = [];
|
|
432
|
+
let modified = original;
|
|
433
|
+
if (modified.includes("SecureMcpServer")) {
|
|
434
|
+
return { file: filePath, changes: ["Already injected \u2014 skipping"], original, modified };
|
|
435
|
+
}
|
|
436
|
+
const mcpImportPatterns = [
|
|
437
|
+
// Solo import: import { McpServer } from '...'
|
|
438
|
+
/import\s*\{\s*McpServer\s*\}\s*from\s*['"][^'"]+['"]/,
|
|
439
|
+
// Import with others: import { McpServer, ... } from '...'
|
|
440
|
+
/import\s*\{[^}]*McpServer[^}]*\}\s*from\s*['"][^'"]+['"]/
|
|
441
|
+
];
|
|
442
|
+
let importReplaced = false;
|
|
443
|
+
for (const pattern of mcpImportPatterns) {
|
|
444
|
+
const match = modified.match(pattern);
|
|
445
|
+
if (match) {
|
|
446
|
+
const importLine = match[0];
|
|
447
|
+
const namedImports = importLine.match(/\{([^}]+)\}/)?.[1] ?? "";
|
|
448
|
+
const importNames = namedImports.split(",").map((s) => s.trim()).filter(Boolean);
|
|
449
|
+
if (importNames.length === 1 && importNames[0] === "McpServer") {
|
|
450
|
+
modified = modified.replace(
|
|
451
|
+
importLine,
|
|
452
|
+
`import { SecureMcpServer } from '@solongate/sdk'`
|
|
453
|
+
);
|
|
454
|
+
changes.push("Replaced McpServer import with SecureMcpServer from @solongate/sdk");
|
|
455
|
+
} else {
|
|
456
|
+
const otherImports = importNames.filter((n) => n !== "McpServer");
|
|
457
|
+
const fromModule = importLine.match(/from\s*['"]([^'"]+)['"]/)?.[1] ?? "";
|
|
458
|
+
modified = modified.replace(
|
|
459
|
+
importLine,
|
|
460
|
+
`import { ${otherImports.join(", ")} } from '${fromModule}';
|
|
461
|
+
import { SecureMcpServer } from '@solongate/sdk'`
|
|
462
|
+
);
|
|
463
|
+
changes.push("Removed McpServer from existing import, added SecureMcpServer import from @solongate/sdk");
|
|
464
|
+
}
|
|
465
|
+
importReplaced = true;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!importReplaced) {
|
|
470
|
+
const insertPos = findImportInsertPosition(modified);
|
|
471
|
+
modified = modified.slice(0, insertPos) + `import { SecureMcpServer } from '@solongate/sdk';
|
|
472
|
+
` + modified.slice(insertPos);
|
|
473
|
+
changes.push("Added SecureMcpServer import from @solongate/sdk");
|
|
474
|
+
}
|
|
475
|
+
const constructorPattern = /new\s+McpServer\s*\(/g;
|
|
476
|
+
const constructorCount = (modified.match(constructorPattern) || []).length;
|
|
477
|
+
if (constructorCount > 0) {
|
|
478
|
+
modified = modified.replace(constructorPattern, "new SecureMcpServer(");
|
|
479
|
+
changes.push(`Replaced ${constructorCount} McpServer constructor(s) with SecureMcpServer`);
|
|
480
|
+
}
|
|
481
|
+
return { file: filePath, changes, original, modified };
|
|
482
|
+
}
|
|
483
|
+
function findImportInsertPosition(content) {
|
|
484
|
+
let pos = 0;
|
|
485
|
+
if (content.startsWith("#!")) {
|
|
486
|
+
pos = content.indexOf("\n") + 1;
|
|
487
|
+
}
|
|
488
|
+
const lines = content.slice(pos).split("\n");
|
|
489
|
+
let offset = pos;
|
|
490
|
+
for (const line of lines) {
|
|
491
|
+
const trimmed = line.trim();
|
|
492
|
+
if (trimmed === "" || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("*/")) {
|
|
493
|
+
offset += line.length + 1;
|
|
494
|
+
} else {
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const importRegex = /^import\s+.+$/gm;
|
|
499
|
+
let lastImportEnd = offset;
|
|
500
|
+
let importMatch;
|
|
501
|
+
while ((importMatch = importRegex.exec(content)) !== null) {
|
|
502
|
+
lastImportEnd = importMatch.index + importMatch[0].length + 1;
|
|
503
|
+
}
|
|
504
|
+
return lastImportEnd > offset ? lastImportEnd : offset;
|
|
505
|
+
}
|
|
506
|
+
function showDiff(result) {
|
|
507
|
+
if (result.original === result.modified) {
|
|
508
|
+
log2(" No changes needed.");
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const origLines = result.original.split("\n");
|
|
512
|
+
const modLines = result.modified.split("\n");
|
|
513
|
+
log2("");
|
|
514
|
+
log2(` File: ${result.file}`);
|
|
515
|
+
log2(" \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");
|
|
516
|
+
const maxLines = Math.max(origLines.length, modLines.length);
|
|
517
|
+
let diffCount = 0;
|
|
518
|
+
for (let i = 0; i < maxLines; i++) {
|
|
519
|
+
const orig = origLines[i];
|
|
520
|
+
const mod = modLines[i];
|
|
521
|
+
if (orig !== mod) {
|
|
522
|
+
if (diffCount < 30) {
|
|
523
|
+
if (orig !== void 0) log2(` - ${orig}`);
|
|
524
|
+
if (mod !== void 0) log2(` + ${mod}`);
|
|
525
|
+
}
|
|
526
|
+
diffCount++;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (diffCount > 30) {
|
|
530
|
+
log2(` ... and ${diffCount - 30} more line changes`);
|
|
531
|
+
}
|
|
532
|
+
log2("");
|
|
533
|
+
}
|
|
534
|
+
async function main2() {
|
|
535
|
+
const opts = parseInjectArgs(process.argv);
|
|
536
|
+
log2("");
|
|
537
|
+
log2(" \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");
|
|
538
|
+
log2(" \u2551 SolonGate \u2014 Inject SDK \u2551");
|
|
539
|
+
log2(" \u2551 Add security to your MCP server \u2551");
|
|
540
|
+
log2(" \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");
|
|
541
|
+
log2("");
|
|
542
|
+
if (!detectProject()) {
|
|
543
|
+
log2(" Could not detect a TypeScript MCP server project.");
|
|
544
|
+
log2(" Make sure you are in a project directory with:");
|
|
545
|
+
log2(" package.json + @modelcontextprotocol/sdk in dependencies");
|
|
546
|
+
log2("");
|
|
547
|
+
log2(" To create a new MCP server: npx @solongate/proxy create <name>");
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
log2(" Language: TypeScript");
|
|
551
|
+
const entryFile = opts.file ? resolve3(opts.file) : findTsEntryFile();
|
|
552
|
+
if (!entryFile || !existsSync3(entryFile)) {
|
|
553
|
+
log2(` Could not find entry file.${opts.file ? ` File not found: ${opts.file}` : ""}`);
|
|
554
|
+
log2("");
|
|
555
|
+
log2(" Specify it manually: --file <path>");
|
|
556
|
+
log2("");
|
|
557
|
+
log2(" Common entry points:");
|
|
558
|
+
log2(" src/index.ts, src/server.ts, index.ts");
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
log2(` Entry: ${entryFile}`);
|
|
562
|
+
log2("");
|
|
563
|
+
const backupPath = entryFile + ".solongate-backup";
|
|
564
|
+
if (opts.restore) {
|
|
565
|
+
if (!existsSync3(backupPath)) {
|
|
566
|
+
log2(" No backup found. Nothing to restore.");
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
copyFileSync2(backupPath, entryFile);
|
|
570
|
+
log2(` Restored original file from backup.`);
|
|
571
|
+
log2(` Backup: ${backupPath}`);
|
|
572
|
+
process.exit(0);
|
|
573
|
+
}
|
|
574
|
+
if (!opts.skipInstall && !opts.dryRun) {
|
|
575
|
+
installSdk();
|
|
576
|
+
log2("");
|
|
577
|
+
}
|
|
578
|
+
const result = injectTypeScript(entryFile);
|
|
579
|
+
log2(` Changes (${result.changes.length}):`);
|
|
580
|
+
for (const change of result.changes) {
|
|
581
|
+
log2(` - ${change}`);
|
|
582
|
+
}
|
|
583
|
+
if (result.changes.length === 1 && result.changes[0].includes("Already injected")) {
|
|
584
|
+
log2("");
|
|
585
|
+
log2(" Your MCP server is already protected by SolonGate!");
|
|
586
|
+
process.exit(0);
|
|
587
|
+
}
|
|
588
|
+
if (result.original === result.modified) {
|
|
589
|
+
log2("");
|
|
590
|
+
log2(" No changes were made. The file may not contain recognizable MCP patterns.");
|
|
591
|
+
log2(" See docs: https://solongate.com/docs/integration");
|
|
592
|
+
process.exit(0);
|
|
593
|
+
}
|
|
594
|
+
if (opts.dryRun) {
|
|
595
|
+
log2("");
|
|
596
|
+
log2(" --- DRY RUN (no changes written) ---");
|
|
597
|
+
showDiff(result);
|
|
598
|
+
log2(" To apply: npx @solongate/proxy inject");
|
|
599
|
+
process.exit(0);
|
|
600
|
+
}
|
|
601
|
+
if (!existsSync3(backupPath)) {
|
|
602
|
+
copyFileSync2(entryFile, backupPath);
|
|
603
|
+
log2("");
|
|
604
|
+
log2(` Backup: ${backupPath}`);
|
|
605
|
+
}
|
|
606
|
+
writeFileSync2(entryFile, result.modified);
|
|
607
|
+
log2("");
|
|
608
|
+
log2(" \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\u2500\u2500\u2500\u2500\u2510");
|
|
609
|
+
log2(" \u2502 SolonGate SDK injected successfully! \u2502");
|
|
610
|
+
log2(" \u2502 \u2502");
|
|
611
|
+
log2(" \u2502 McpServer \u2192 SecureMcpServer \u2502");
|
|
612
|
+
log2(" \u2502 All tool() calls are now auto-protected. \u2502");
|
|
613
|
+
log2(" \u2502 \u2502");
|
|
614
|
+
log2(" \u2502 Set your API key: \u2502");
|
|
615
|
+
log2(" \u2502 export SOLONGATE_API_KEY=sg_live_xxx \u2502");
|
|
616
|
+
log2(" \u2502 \u2502");
|
|
617
|
+
log2(" \u2502 To undo: \u2502");
|
|
618
|
+
log2(" \u2502 npx @solongate/proxy inject --restore \u2502");
|
|
619
|
+
log2(" \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\u2500\u2500\u2500\u2500\u2518");
|
|
620
|
+
log2("");
|
|
621
|
+
}
|
|
622
|
+
var init_inject = __esm({
|
|
623
|
+
"src/inject.ts"() {
|
|
624
|
+
"use strict";
|
|
625
|
+
main2().catch((err) => {
|
|
626
|
+
log2(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// src/create.ts
|
|
633
|
+
var create_exports = {};
|
|
634
|
+
import { mkdirSync, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
635
|
+
import { resolve as resolve4, join as join2 } from "path";
|
|
636
|
+
import { execSync as execSync2 } from "child_process";
|
|
637
|
+
function log3(msg) {
|
|
638
|
+
process.stderr.write(msg + "\n");
|
|
639
|
+
}
|
|
640
|
+
function parseCreateArgs(argv) {
|
|
641
|
+
const args = argv.slice(2);
|
|
642
|
+
const opts = {
|
|
643
|
+
name: "",
|
|
644
|
+
policy: "restricted",
|
|
645
|
+
noInstall: false
|
|
646
|
+
};
|
|
647
|
+
for (let i = 0; i < args.length; i++) {
|
|
648
|
+
switch (args[i]) {
|
|
649
|
+
case "--policy":
|
|
650
|
+
opts.policy = args[++i];
|
|
651
|
+
break;
|
|
652
|
+
case "--no-install":
|
|
653
|
+
opts.noInstall = true;
|
|
654
|
+
break;
|
|
655
|
+
case "--help":
|
|
656
|
+
case "-h":
|
|
657
|
+
printHelp3();
|
|
658
|
+
process.exit(0);
|
|
659
|
+
break;
|
|
660
|
+
default:
|
|
661
|
+
if (!args[i].startsWith("-") && !opts.name) {
|
|
662
|
+
opts.name = args[i];
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (!opts.name) {
|
|
667
|
+
log3("");
|
|
668
|
+
log3(" Error: Project name required.");
|
|
669
|
+
log3("");
|
|
670
|
+
log3(" Usage: npx @solongate/proxy create <name>");
|
|
671
|
+
log3("");
|
|
672
|
+
log3(" Examples:");
|
|
673
|
+
log3(" npx @solongate/proxy create my-mcp-server");
|
|
674
|
+
log3(" npx @solongate/proxy create weather-api");
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
return opts;
|
|
678
|
+
}
|
|
679
|
+
function printHelp3() {
|
|
680
|
+
log3(`
|
|
681
|
+
SolonGate Create \u2014 Scaffold a secure MCP server in seconds
|
|
682
|
+
|
|
683
|
+
USAGE
|
|
684
|
+
npx @solongate/proxy create <name> [options]
|
|
685
|
+
|
|
686
|
+
OPTIONS
|
|
687
|
+
--policy <preset> Policy preset (default: restricted)
|
|
688
|
+
--no-install Skip dependency installation
|
|
689
|
+
-h, --help Show this help message
|
|
690
|
+
|
|
691
|
+
EXAMPLES
|
|
692
|
+
npx @solongate/proxy create my-server
|
|
693
|
+
npx @solongate/proxy create db-tools --policy read-only
|
|
694
|
+
`);
|
|
695
|
+
}
|
|
696
|
+
function createProject(dir, name, _policy) {
|
|
697
|
+
writeFileSync3(
|
|
698
|
+
join2(dir, "package.json"),
|
|
699
|
+
JSON.stringify(
|
|
700
|
+
{
|
|
701
|
+
name,
|
|
702
|
+
version: "0.1.0",
|
|
703
|
+
type: "module",
|
|
704
|
+
private: true,
|
|
705
|
+
bin: { [name]: "./dist/index.js" },
|
|
706
|
+
scripts: {
|
|
707
|
+
build: "tsup src/index.ts --format esm",
|
|
708
|
+
dev: "tsx src/index.ts",
|
|
709
|
+
start: "node dist/index.js"
|
|
710
|
+
},
|
|
711
|
+
dependencies: {
|
|
712
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
713
|
+
"@solongate/sdk": "latest",
|
|
714
|
+
zod: "^3.25.0"
|
|
715
|
+
},
|
|
716
|
+
devDependencies: {
|
|
717
|
+
tsup: "^8.3.0",
|
|
718
|
+
tsx: "^4.19.0",
|
|
719
|
+
typescript: "^5.7.0"
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
null,
|
|
723
|
+
2
|
|
724
|
+
) + "\n"
|
|
725
|
+
);
|
|
726
|
+
writeFileSync3(
|
|
727
|
+
join2(dir, "tsconfig.json"),
|
|
728
|
+
JSON.stringify(
|
|
729
|
+
{
|
|
730
|
+
compilerOptions: {
|
|
731
|
+
target: "ES2022",
|
|
732
|
+
module: "ESNext",
|
|
733
|
+
moduleResolution: "bundler",
|
|
734
|
+
esModuleInterop: true,
|
|
735
|
+
strict: true,
|
|
736
|
+
outDir: "dist",
|
|
737
|
+
rootDir: "src",
|
|
738
|
+
declaration: true,
|
|
739
|
+
skipLibCheck: true
|
|
740
|
+
},
|
|
741
|
+
include: ["src"]
|
|
742
|
+
},
|
|
743
|
+
null,
|
|
744
|
+
2
|
|
745
|
+
) + "\n"
|
|
746
|
+
);
|
|
747
|
+
mkdirSync(join2(dir, "src"), { recursive: true });
|
|
748
|
+
writeFileSync3(
|
|
749
|
+
join2(dir, "src", "index.ts"),
|
|
750
|
+
`#!/usr/bin/env node
|
|
751
|
+
|
|
752
|
+
// MCP uses stdout for JSON-RPC \u2014 redirect console to stderr
|
|
753
|
+
console.log = (...args: unknown[]) => {
|
|
754
|
+
process.stderr.write(args.map(String).join(' ') + '\\n');
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
import { SecureMcpServer } from '@solongate/sdk';
|
|
758
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
759
|
+
import { z } from 'zod';
|
|
760
|
+
|
|
761
|
+
// Create a secure MCP server (API key from SOLONGATE_API_KEY env var)
|
|
762
|
+
const server = new SecureMcpServer({
|
|
763
|
+
name: '${name}',
|
|
764
|
+
version: '0.1.0',
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// \u2500\u2500 Register your tools below \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
|
|
768
|
+
|
|
769
|
+
server.tool(
|
|
770
|
+
'hello',
|
|
771
|
+
'Say hello to someone',
|
|
772
|
+
{ name: z.string().describe('Name of the person to greet') },
|
|
773
|
+
async ({ name }) => ({
|
|
774
|
+
content: [{ type: 'text', text: \`Hello, \${name}! Welcome to ${name}.\` }],
|
|
775
|
+
}),
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
// Example: Add more tools here
|
|
779
|
+
// server.tool(
|
|
780
|
+
// 'read_data',
|
|
781
|
+
// 'Read data from a source',
|
|
782
|
+
// { query: z.string().describe('What to read') },
|
|
783
|
+
// async ({ query }) => ({
|
|
784
|
+
// content: [{ type: 'text', text: \`Result for: \${query}\` }],
|
|
785
|
+
// }),
|
|
786
|
+
// );
|
|
787
|
+
|
|
788
|
+
// \u2500\u2500 Start the server \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
789
|
+
|
|
790
|
+
const transport = new StdioServerTransport();
|
|
791
|
+
await server.connect(transport);
|
|
792
|
+
console.log('${name} is running');
|
|
793
|
+
`
|
|
794
|
+
);
|
|
795
|
+
writeFileSync3(
|
|
796
|
+
join2(dir, ".mcp.json"),
|
|
797
|
+
JSON.stringify(
|
|
798
|
+
{
|
|
799
|
+
mcpServers: {
|
|
800
|
+
[name]: {
|
|
801
|
+
command: "node",
|
|
802
|
+
args: ["dist/index.js"],
|
|
803
|
+
env: {
|
|
804
|
+
SOLONGATE_API_KEY: "sg_test_e4460d32_replace_with_your_key"
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
null,
|
|
810
|
+
2
|
|
811
|
+
) + "\n"
|
|
812
|
+
);
|
|
813
|
+
writeFileSync3(
|
|
814
|
+
join2(dir, ".gitignore"),
|
|
815
|
+
`node_modules/
|
|
816
|
+
dist/
|
|
817
|
+
*.solongate-backup
|
|
818
|
+
.env
|
|
819
|
+
.env.local
|
|
820
|
+
`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
function installDeps(dir) {
|
|
824
|
+
log3(" Installing dependencies with npm...");
|
|
825
|
+
try {
|
|
826
|
+
execSync2("npm install", { cwd: dir, stdio: "pipe" });
|
|
827
|
+
} catch {
|
|
828
|
+
log3(" npm install failed \u2014 run it manually.");
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async function main3() {
|
|
832
|
+
const opts = parseCreateArgs(process.argv);
|
|
833
|
+
const dir = resolve4(opts.name);
|
|
834
|
+
log3("");
|
|
835
|
+
log3(" \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");
|
|
836
|
+
log3(" \u2551 SolonGate \u2014 Create MCP Server \u2551");
|
|
837
|
+
log3(" \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");
|
|
838
|
+
log3("");
|
|
839
|
+
if (existsSync4(dir)) {
|
|
840
|
+
log3(` Error: Directory "${opts.name}" already exists.`);
|
|
841
|
+
process.exit(1);
|
|
842
|
+
}
|
|
843
|
+
mkdirSync(dir, { recursive: true });
|
|
844
|
+
log3(` Project: ${opts.name}`);
|
|
845
|
+
log3(` Language: TypeScript`);
|
|
846
|
+
log3(` Policy: ${opts.policy}`);
|
|
847
|
+
log3("");
|
|
848
|
+
createProject(dir, opts.name, opts.policy);
|
|
849
|
+
log3(" Files created:");
|
|
850
|
+
log3(" package.json");
|
|
851
|
+
log3(" tsconfig.json");
|
|
852
|
+
log3(" src/index.ts");
|
|
853
|
+
log3(" .mcp.json");
|
|
854
|
+
log3(" .gitignore");
|
|
855
|
+
log3("");
|
|
856
|
+
if (!opts.noInstall) {
|
|
857
|
+
installDeps(dir);
|
|
858
|
+
log3("");
|
|
859
|
+
}
|
|
860
|
+
log3(" \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\u2500\u2500\u2500\u2500\u2510");
|
|
861
|
+
log3(" \u2502 Project created! \u2502");
|
|
862
|
+
log3(" \u2502 \u2502");
|
|
863
|
+
log3(` \u2502 cd ${opts.name.padEnd(39)}\u2502`);
|
|
864
|
+
log3(" \u2502 \u2502");
|
|
865
|
+
log3(" \u2502 npm run build # Build \u2502");
|
|
866
|
+
log3(" \u2502 npm run dev # Dev mode (tsx) \u2502");
|
|
867
|
+
log3(" \u2502 npm start # Run built server \u2502");
|
|
868
|
+
log3(" \u2502 \u2502");
|
|
869
|
+
log3(" \u2502 Set your API key: \u2502");
|
|
870
|
+
log3(" \u2502 export SOLONGATE_API_KEY=sg_live_xxx \u2502");
|
|
871
|
+
log3(" \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\u2500\u2500\u2500\u2500\u2518");
|
|
872
|
+
log3("");
|
|
873
|
+
}
|
|
874
|
+
var init_create = __esm({
|
|
875
|
+
"src/create.ts"() {
|
|
876
|
+
"use strict";
|
|
877
|
+
main3().catch((err) => {
|
|
878
|
+
log3(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
879
|
+
process.exit(1);
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
});
|
|
268
883
|
|
|
269
884
|
// ../core/dist/index.js
|
|
270
885
|
import { z } from "zod";
|
|
@@ -417,7 +1032,9 @@ var DEFAULT_INPUT_GUARD_CONFIG = Object.freeze({
|
|
|
417
1032
|
shellInjection: true,
|
|
418
1033
|
wildcardAbuse: true,
|
|
419
1034
|
lengthLimit: 4096,
|
|
420
|
-
entropyLimit: true
|
|
1035
|
+
entropyLimit: true,
|
|
1036
|
+
ssrf: true,
|
|
1037
|
+
sqlInjection: true
|
|
421
1038
|
});
|
|
422
1039
|
var PATH_TRAVERSAL_PATTERNS = [
|
|
423
1040
|
/\.\.\//,
|
|
@@ -443,7 +1060,23 @@ var SENSITIVE_PATHS = [
|
|
|
443
1060
|
/c:\\windows\\system32/i,
|
|
444
1061
|
/c:\\windows\\syswow64/i,
|
|
445
1062
|
/\/root\//i,
|
|
446
|
-
|
|
1063
|
+
/~\//,
|
|
1064
|
+
/\.env(\.|$)/i,
|
|
1065
|
+
// .env, .env.local, .env.production
|
|
1066
|
+
/\.aws\/credentials/i,
|
|
1067
|
+
// AWS credentials
|
|
1068
|
+
/\.ssh\/id_/i,
|
|
1069
|
+
// SSH keys
|
|
1070
|
+
/\.kube\/config/i,
|
|
1071
|
+
// Kubernetes config
|
|
1072
|
+
/wp-config\.php/i,
|
|
1073
|
+
// WordPress config
|
|
1074
|
+
/\.git\/config/i,
|
|
1075
|
+
// Git config
|
|
1076
|
+
/\.npmrc/i,
|
|
1077
|
+
// npm credentials
|
|
1078
|
+
/\.pypirc/i
|
|
1079
|
+
// PyPI credentials
|
|
447
1080
|
];
|
|
448
1081
|
function detectPathTraversal(value) {
|
|
449
1082
|
for (const pattern of PATH_TRAVERSAL_PATTERNS) {
|
|
@@ -473,8 +1106,18 @@ var SHELL_INJECTION_PATTERNS = [
|
|
|
473
1106
|
// eval command
|
|
474
1107
|
/\bexec\b/i,
|
|
475
1108
|
// exec command
|
|
476
|
-
/\bsystem\b/i
|
|
1109
|
+
/\bsystem\b/i,
|
|
477
1110
|
// system call
|
|
1111
|
+
/%0a/i,
|
|
1112
|
+
// URL-encoded newline
|
|
1113
|
+
/%0d/i,
|
|
1114
|
+
// URL-encoded carriage return
|
|
1115
|
+
/%09/i,
|
|
1116
|
+
// URL-encoded tab
|
|
1117
|
+
/\r\n/,
|
|
1118
|
+
// CRLF injection
|
|
1119
|
+
/\n/
|
|
1120
|
+
// Newline (command separator on Unix)
|
|
478
1121
|
];
|
|
479
1122
|
function detectShellInjection(value) {
|
|
480
1123
|
for (const pattern of SHELL_INJECTION_PATTERNS) {
|
|
@@ -489,6 +1132,91 @@ function detectWildcardAbuse(value) {
|
|
|
489
1132
|
if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
|
|
490
1133
|
return false;
|
|
491
1134
|
}
|
|
1135
|
+
var SSRF_PATTERNS = [
|
|
1136
|
+
/^https?:\/\/localhost\b/i,
|
|
1137
|
+
/^https?:\/\/127\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
1138
|
+
/^https?:\/\/0\.0\.0\.0/,
|
|
1139
|
+
/^https?:\/\/\[::1\]/,
|
|
1140
|
+
// IPv6 loopback
|
|
1141
|
+
/^https?:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
1142
|
+
// 10.x.x.x
|
|
1143
|
+
/^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
|
|
1144
|
+
// 172.16-31.x.x
|
|
1145
|
+
/^https?:\/\/192\.168\./,
|
|
1146
|
+
// 192.168.x.x
|
|
1147
|
+
/^https?:\/\/169\.254\./,
|
|
1148
|
+
// Link-local / AWS metadata
|
|
1149
|
+
/metadata\.google\.internal/i,
|
|
1150
|
+
// GCP metadata
|
|
1151
|
+
/^https?:\/\/metadata\b/i,
|
|
1152
|
+
// Generic metadata endpoint
|
|
1153
|
+
// IPv6 bypass patterns
|
|
1154
|
+
/^https?:\/\/\[fe80:/i,
|
|
1155
|
+
// IPv6 link-local
|
|
1156
|
+
/^https?:\/\/\[fc00:/i,
|
|
1157
|
+
// IPv6 unique local
|
|
1158
|
+
/^https?:\/\/\[fd[0-9a-f]{2}:/i,
|
|
1159
|
+
// IPv6 unique local (fd00::/8)
|
|
1160
|
+
/^https?:\/\/\[::ffff:127\./i,
|
|
1161
|
+
// IPv4-mapped IPv6 loopback
|
|
1162
|
+
/^https?:\/\/\[::ffff:10\./i,
|
|
1163
|
+
// IPv4-mapped IPv6 private
|
|
1164
|
+
/^https?:\/\/\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
|
|
1165
|
+
// IPv4-mapped IPv6 private
|
|
1166
|
+
/^https?:\/\/\[::ffff:192\.168\./i,
|
|
1167
|
+
// IPv4-mapped IPv6 private
|
|
1168
|
+
/^https?:\/\/\[::ffff:169\.254\./i,
|
|
1169
|
+
// IPv4-mapped IPv6 link-local
|
|
1170
|
+
// Hex IP bypass (e.g., 0x7f000001 = 127.0.0.1)
|
|
1171
|
+
/^https?:\/\/0x[0-9a-f]+\b/i,
|
|
1172
|
+
// Octal IP bypass (e.g., 0177.0.0.1 = 127.0.0.1)
|
|
1173
|
+
/^https?:\/\/0[0-7]{1,3}\./
|
|
1174
|
+
];
|
|
1175
|
+
function detectDecimalIP(value) {
|
|
1176
|
+
const match = value.match(/^https?:\/\/(\d{8,10})(?:[:/]|$)/);
|
|
1177
|
+
if (!match || !match[1]) return false;
|
|
1178
|
+
const decimal = parseInt(match[1], 10);
|
|
1179
|
+
if (isNaN(decimal) || decimal > 4294967295) return false;
|
|
1180
|
+
return decimal >= 2130706432 && decimal <= 2147483647 || // 127.0.0.0/8
|
|
1181
|
+
decimal >= 167772160 && decimal <= 184549375 || // 10.0.0.0/8
|
|
1182
|
+
decimal >= 2886729728 && decimal <= 2887778303 || // 172.16.0.0/12
|
|
1183
|
+
decimal >= 3232235520 && decimal <= 3232301055 || // 192.168.0.0/16
|
|
1184
|
+
decimal >= 2851995648 && decimal <= 2852061183 || // 169.254.0.0/16
|
|
1185
|
+
decimal === 0;
|
|
1186
|
+
}
|
|
1187
|
+
function detectSSRF(value) {
|
|
1188
|
+
for (const pattern of SSRF_PATTERNS) {
|
|
1189
|
+
if (pattern.test(value)) return true;
|
|
1190
|
+
}
|
|
1191
|
+
if (detectDecimalIP(value)) return true;
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
var SQL_INJECTION_PATTERNS = [
|
|
1195
|
+
/'\s{0,20}(OR|AND)\s{0,20}'.{0,200}'/i,
|
|
1196
|
+
// ' OR '1'='1 — bounded to prevent ReDoS
|
|
1197
|
+
/'\s{0,10};\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
|
|
1198
|
+
// '; DROP TABLE
|
|
1199
|
+
/UNION\s+(ALL\s+)?SELECT/i,
|
|
1200
|
+
// UNION SELECT
|
|
1201
|
+
/--\s*$/m,
|
|
1202
|
+
// SQL comment at end of line
|
|
1203
|
+
/\/\*.{0,500}?\*\//,
|
|
1204
|
+
// SQL block comment — bounded + non-greedy
|
|
1205
|
+
/\bSLEEP\s*\(/i,
|
|
1206
|
+
// Time-based injection
|
|
1207
|
+
/\bBENCHMARK\s*\(/i,
|
|
1208
|
+
// MySQL benchmark
|
|
1209
|
+
/\bWAITFOR\s+DELAY/i,
|
|
1210
|
+
// MSSQL delay
|
|
1211
|
+
/\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b/i
|
|
1212
|
+
// File operations
|
|
1213
|
+
];
|
|
1214
|
+
function detectSQLInjection(value) {
|
|
1215
|
+
for (const pattern of SQL_INJECTION_PATTERNS) {
|
|
1216
|
+
if (pattern.test(value)) return true;
|
|
1217
|
+
}
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
492
1220
|
function checkLengthLimits(value, maxLength = 4096) {
|
|
493
1221
|
return value.length <= maxLength;
|
|
494
1222
|
}
|
|
@@ -504,87 +1232,478 @@ function calculateShannonEntropy(str) {
|
|
|
504
1232
|
for (const char of str) {
|
|
505
1233
|
freq.set(char, (freq.get(char) ?? 0) + 1);
|
|
506
1234
|
}
|
|
507
|
-
let entropy = 0;
|
|
508
|
-
const len = str.length;
|
|
509
|
-
for (const count of freq.values()) {
|
|
510
|
-
const p = count / len;
|
|
511
|
-
if (p > 0) {
|
|
512
|
-
entropy -= p * Math.log2(p);
|
|
513
|
-
}
|
|
1235
|
+
let entropy = 0;
|
|
1236
|
+
const len = str.length;
|
|
1237
|
+
for (const count of freq.values()) {
|
|
1238
|
+
const p = count / len;
|
|
1239
|
+
if (p > 0) {
|
|
1240
|
+
entropy -= p * Math.log2(p);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return entropy;
|
|
1244
|
+
}
|
|
1245
|
+
function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG) {
|
|
1246
|
+
const threats = [];
|
|
1247
|
+
if (typeof value !== "string") {
|
|
1248
|
+
if (typeof value === "object" && value !== null) {
|
|
1249
|
+
return sanitizeObject(field, value, config);
|
|
1250
|
+
}
|
|
1251
|
+
return { safe: true, threats: [] };
|
|
1252
|
+
}
|
|
1253
|
+
if (config.pathTraversal && detectPathTraversal(value)) {
|
|
1254
|
+
threats.push({
|
|
1255
|
+
type: "PATH_TRAVERSAL",
|
|
1256
|
+
field,
|
|
1257
|
+
value: truncate(value, 100),
|
|
1258
|
+
description: "Path traversal pattern detected"
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
if (config.shellInjection && detectShellInjection(value)) {
|
|
1262
|
+
threats.push({
|
|
1263
|
+
type: "SHELL_INJECTION",
|
|
1264
|
+
field,
|
|
1265
|
+
value: truncate(value, 100),
|
|
1266
|
+
description: "Shell injection pattern detected"
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
if (config.wildcardAbuse && detectWildcardAbuse(value)) {
|
|
1270
|
+
threats.push({
|
|
1271
|
+
type: "WILDCARD_ABUSE",
|
|
1272
|
+
field,
|
|
1273
|
+
value: truncate(value, 100),
|
|
1274
|
+
description: "Wildcard abuse pattern detected"
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
if (!checkLengthLimits(value, config.lengthLimit)) {
|
|
1278
|
+
threats.push({
|
|
1279
|
+
type: "LENGTH_EXCEEDED",
|
|
1280
|
+
field,
|
|
1281
|
+
value: `[${value.length} chars]`,
|
|
1282
|
+
description: `Value exceeds maximum length of ${config.lengthLimit}`
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
if (config.entropyLimit && !checkEntropyLimits(value)) {
|
|
1286
|
+
threats.push({
|
|
1287
|
+
type: "HIGH_ENTROPY",
|
|
1288
|
+
field,
|
|
1289
|
+
value: truncate(value, 100),
|
|
1290
|
+
description: "High entropy string detected - possible encoded payload"
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
if (config.ssrf && detectSSRF(value)) {
|
|
1294
|
+
threats.push({
|
|
1295
|
+
type: "SSRF",
|
|
1296
|
+
field,
|
|
1297
|
+
value: truncate(value, 100),
|
|
1298
|
+
description: "Server-side request forgery pattern detected \u2014 internal/metadata URL blocked"
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
if (config.sqlInjection && detectSQLInjection(value)) {
|
|
1302
|
+
threats.push({
|
|
1303
|
+
type: "SQL_INJECTION",
|
|
1304
|
+
field,
|
|
1305
|
+
value: truncate(value, 100),
|
|
1306
|
+
description: "SQL injection pattern detected"
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
return { safe: threats.length === 0, threats };
|
|
1310
|
+
}
|
|
1311
|
+
function sanitizeObject(basePath, obj, config) {
|
|
1312
|
+
const threats = [];
|
|
1313
|
+
if (Array.isArray(obj)) {
|
|
1314
|
+
for (let i = 0; i < obj.length; i++) {
|
|
1315
|
+
const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
|
|
1316
|
+
threats.push(...result.threats);
|
|
1317
|
+
}
|
|
1318
|
+
} else {
|
|
1319
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
1320
|
+
const result = sanitizeInput(`${basePath}.${key}`, val, config);
|
|
1321
|
+
threats.push(...result.threats);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return { safe: threats.length === 0, threats };
|
|
1325
|
+
}
|
|
1326
|
+
function truncate(str, maxLen) {
|
|
1327
|
+
return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
|
|
1328
|
+
}
|
|
1329
|
+
var DEFAULT_TOKEN_TTL_SECONDS = 30;
|
|
1330
|
+
var TOKEN_ALGORITHM = "HS256";
|
|
1331
|
+
var MIN_SECRET_LENGTH = 32;
|
|
1332
|
+
|
|
1333
|
+
// src/config.ts
|
|
1334
|
+
import { readFileSync, existsSync } from "fs";
|
|
1335
|
+
import { resolve } from "path";
|
|
1336
|
+
async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
|
|
1337
|
+
const url = `${apiUrl}/api/v1/policies/${policyId ?? "default"}`;
|
|
1338
|
+
const res = await fetch(url, {
|
|
1339
|
+
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
1340
|
+
});
|
|
1341
|
+
if (!res.ok) {
|
|
1342
|
+
const body = await res.text().catch(() => "");
|
|
1343
|
+
throw new Error(`Failed to fetch policy from cloud (${res.status}): ${body}`);
|
|
1344
|
+
}
|
|
1345
|
+
const data = await res.json();
|
|
1346
|
+
return {
|
|
1347
|
+
id: String(data.id ?? "cloud"),
|
|
1348
|
+
name: String(data.name ?? "Cloud Policy"),
|
|
1349
|
+
version: Number(data._version ?? 1),
|
|
1350
|
+
rules: data.rules ?? [],
|
|
1351
|
+
createdAt: String(data._created_at ?? ""),
|
|
1352
|
+
updatedAt: ""
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
async function sendAuditLog(apiKey, apiUrl, entry) {
|
|
1356
|
+
try {
|
|
1357
|
+
await fetch(`${apiUrl}/api/v1/audit-logs`, {
|
|
1358
|
+
method: "POST",
|
|
1359
|
+
headers: {
|
|
1360
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
1361
|
+
"Content-Type": "application/json"
|
|
1362
|
+
},
|
|
1363
|
+
body: JSON.stringify(entry)
|
|
1364
|
+
});
|
|
1365
|
+
} catch {
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
var PRESETS = {
|
|
1369
|
+
restricted: {
|
|
1370
|
+
id: "restricted",
|
|
1371
|
+
name: "Restricted",
|
|
1372
|
+
description: "Blocks dangerous tools (shell, web), allows safe tools",
|
|
1373
|
+
version: 1,
|
|
1374
|
+
rules: [
|
|
1375
|
+
{
|
|
1376
|
+
id: "deny-shell",
|
|
1377
|
+
description: "Block shell execution",
|
|
1378
|
+
effect: "DENY",
|
|
1379
|
+
priority: 100,
|
|
1380
|
+
toolPattern: "*shell*",
|
|
1381
|
+
permission: "EXECUTE",
|
|
1382
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1383
|
+
enabled: true,
|
|
1384
|
+
createdAt: "",
|
|
1385
|
+
updatedAt: ""
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
id: "deny-exec",
|
|
1389
|
+
description: "Block command execution",
|
|
1390
|
+
effect: "DENY",
|
|
1391
|
+
priority: 101,
|
|
1392
|
+
toolPattern: "*exec*",
|
|
1393
|
+
permission: "EXECUTE",
|
|
1394
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1395
|
+
enabled: true,
|
|
1396
|
+
createdAt: "",
|
|
1397
|
+
updatedAt: ""
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
id: "deny-eval",
|
|
1401
|
+
description: "Block code eval",
|
|
1402
|
+
effect: "DENY",
|
|
1403
|
+
priority: 102,
|
|
1404
|
+
toolPattern: "*eval*",
|
|
1405
|
+
permission: "EXECUTE",
|
|
1406
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1407
|
+
enabled: true,
|
|
1408
|
+
createdAt: "",
|
|
1409
|
+
updatedAt: ""
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
id: "allow-rest",
|
|
1413
|
+
description: "Allow all other tools",
|
|
1414
|
+
effect: "ALLOW",
|
|
1415
|
+
priority: 1e3,
|
|
1416
|
+
toolPattern: "*",
|
|
1417
|
+
permission: "EXECUTE",
|
|
1418
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1419
|
+
enabled: true,
|
|
1420
|
+
createdAt: "",
|
|
1421
|
+
updatedAt: ""
|
|
1422
|
+
}
|
|
1423
|
+
],
|
|
1424
|
+
createdAt: "",
|
|
1425
|
+
updatedAt: ""
|
|
1426
|
+
},
|
|
1427
|
+
"read-only": {
|
|
1428
|
+
id: "read-only",
|
|
1429
|
+
name: "Read Only",
|
|
1430
|
+
description: "Only allows read operations, blocks writes and execution",
|
|
1431
|
+
version: 1,
|
|
1432
|
+
rules: [
|
|
1433
|
+
{
|
|
1434
|
+
id: "allow-read",
|
|
1435
|
+
description: "Allow read tools",
|
|
1436
|
+
effect: "ALLOW",
|
|
1437
|
+
priority: 100,
|
|
1438
|
+
toolPattern: "*read*",
|
|
1439
|
+
permission: "EXECUTE",
|
|
1440
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1441
|
+
enabled: true,
|
|
1442
|
+
createdAt: "",
|
|
1443
|
+
updatedAt: ""
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
id: "allow-list",
|
|
1447
|
+
description: "Allow list tools",
|
|
1448
|
+
effect: "ALLOW",
|
|
1449
|
+
priority: 101,
|
|
1450
|
+
toolPattern: "*list*",
|
|
1451
|
+
permission: "EXECUTE",
|
|
1452
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1453
|
+
enabled: true,
|
|
1454
|
+
createdAt: "",
|
|
1455
|
+
updatedAt: ""
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
id: "allow-get",
|
|
1459
|
+
description: "Allow get tools",
|
|
1460
|
+
effect: "ALLOW",
|
|
1461
|
+
priority: 102,
|
|
1462
|
+
toolPattern: "*get*",
|
|
1463
|
+
permission: "EXECUTE",
|
|
1464
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1465
|
+
enabled: true,
|
|
1466
|
+
createdAt: "",
|
|
1467
|
+
updatedAt: ""
|
|
1468
|
+
},
|
|
1469
|
+
{
|
|
1470
|
+
id: "allow-search",
|
|
1471
|
+
description: "Allow search tools",
|
|
1472
|
+
effect: "ALLOW",
|
|
1473
|
+
priority: 103,
|
|
1474
|
+
toolPattern: "*search*",
|
|
1475
|
+
permission: "EXECUTE",
|
|
1476
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1477
|
+
enabled: true,
|
|
1478
|
+
createdAt: "",
|
|
1479
|
+
updatedAt: ""
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
id: "allow-query",
|
|
1483
|
+
description: "Allow query tools",
|
|
1484
|
+
effect: "ALLOW",
|
|
1485
|
+
priority: 104,
|
|
1486
|
+
toolPattern: "*query*",
|
|
1487
|
+
permission: "EXECUTE",
|
|
1488
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1489
|
+
enabled: true,
|
|
1490
|
+
createdAt: "",
|
|
1491
|
+
updatedAt: ""
|
|
1492
|
+
}
|
|
1493
|
+
],
|
|
1494
|
+
createdAt: "",
|
|
1495
|
+
updatedAt: ""
|
|
1496
|
+
},
|
|
1497
|
+
permissive: {
|
|
1498
|
+
id: "permissive",
|
|
1499
|
+
name: "Permissive",
|
|
1500
|
+
description: "Allows all tool calls (monitoring only)",
|
|
1501
|
+
version: 1,
|
|
1502
|
+
rules: [
|
|
1503
|
+
{
|
|
1504
|
+
id: "allow-all",
|
|
1505
|
+
description: "Allow all",
|
|
1506
|
+
effect: "ALLOW",
|
|
1507
|
+
priority: 1e3,
|
|
1508
|
+
toolPattern: "*",
|
|
1509
|
+
permission: "EXECUTE",
|
|
1510
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1511
|
+
enabled: true,
|
|
1512
|
+
createdAt: "",
|
|
1513
|
+
updatedAt: ""
|
|
1514
|
+
}
|
|
1515
|
+
],
|
|
1516
|
+
createdAt: "",
|
|
1517
|
+
updatedAt: ""
|
|
1518
|
+
},
|
|
1519
|
+
"deny-all": {
|
|
1520
|
+
id: "deny-all",
|
|
1521
|
+
name: "Deny All",
|
|
1522
|
+
description: "Blocks all tool calls",
|
|
1523
|
+
version: 1,
|
|
1524
|
+
rules: [],
|
|
1525
|
+
createdAt: "",
|
|
1526
|
+
updatedAt: ""
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
function loadPolicy(source) {
|
|
1530
|
+
if (typeof source === "object") return source;
|
|
1531
|
+
if (PRESETS[source]) return PRESETS[source];
|
|
1532
|
+
const filePath = resolve(source);
|
|
1533
|
+
if (existsSync(filePath)) {
|
|
1534
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1535
|
+
return JSON.parse(content);
|
|
514
1536
|
}
|
|
515
|
-
|
|
1537
|
+
throw new Error(
|
|
1538
|
+
`Unknown policy "${source}". Use a preset (${Object.keys(PRESETS).join(", ")}), a JSON file path, or a PolicySet object.`
|
|
1539
|
+
);
|
|
516
1540
|
}
|
|
517
|
-
function
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
1541
|
+
function parseArgs(argv) {
|
|
1542
|
+
const args = argv.slice(2);
|
|
1543
|
+
let policySource = "restricted";
|
|
1544
|
+
let name = "solongate-proxy";
|
|
1545
|
+
let verbose = false;
|
|
1546
|
+
let validateInput = true;
|
|
1547
|
+
let rateLimitPerTool;
|
|
1548
|
+
let globalRateLimit;
|
|
1549
|
+
let configFile;
|
|
1550
|
+
let apiKey;
|
|
1551
|
+
let apiUrl;
|
|
1552
|
+
let upstreamUrl;
|
|
1553
|
+
let upstreamTransport;
|
|
1554
|
+
let port;
|
|
1555
|
+
let separatorIndex = args.indexOf("--");
|
|
1556
|
+
const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
|
|
1557
|
+
const upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
|
|
1558
|
+
for (let i = 0; i < flags.length; i++) {
|
|
1559
|
+
switch (flags[i]) {
|
|
1560
|
+
case "--policy":
|
|
1561
|
+
policySource = flags[++i];
|
|
1562
|
+
break;
|
|
1563
|
+
case "--name":
|
|
1564
|
+
name = flags[++i];
|
|
1565
|
+
break;
|
|
1566
|
+
case "--verbose":
|
|
1567
|
+
verbose = true;
|
|
1568
|
+
break;
|
|
1569
|
+
case "--no-input-guard":
|
|
1570
|
+
validateInput = false;
|
|
1571
|
+
break;
|
|
1572
|
+
case "--rate-limit":
|
|
1573
|
+
rateLimitPerTool = parseInt(flags[++i], 10);
|
|
1574
|
+
break;
|
|
1575
|
+
case "--global-rate-limit":
|
|
1576
|
+
globalRateLimit = parseInt(flags[++i], 10);
|
|
1577
|
+
break;
|
|
1578
|
+
case "--config":
|
|
1579
|
+
configFile = flags[++i];
|
|
1580
|
+
break;
|
|
1581
|
+
case "--api-key":
|
|
1582
|
+
apiKey = flags[++i];
|
|
1583
|
+
break;
|
|
1584
|
+
case "--api-url":
|
|
1585
|
+
apiUrl = flags[++i];
|
|
1586
|
+
break;
|
|
1587
|
+
case "--upstream-url":
|
|
1588
|
+
upstreamUrl = flags[++i];
|
|
1589
|
+
break;
|
|
1590
|
+
case "--upstream-transport":
|
|
1591
|
+
upstreamTransport = flags[++i];
|
|
1592
|
+
break;
|
|
1593
|
+
case "--port":
|
|
1594
|
+
port = parseInt(flags[++i], 10);
|
|
1595
|
+
break;
|
|
522
1596
|
}
|
|
523
|
-
return { safe: true, threats: [] };
|
|
524
|
-
}
|
|
525
|
-
if (config.pathTraversal && detectPathTraversal(value)) {
|
|
526
|
-
threats.push({
|
|
527
|
-
type: "PATH_TRAVERSAL",
|
|
528
|
-
field,
|
|
529
|
-
value: truncate(value, 100),
|
|
530
|
-
description: "Path traversal pattern detected"
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
if (config.shellInjection && detectShellInjection(value)) {
|
|
534
|
-
threats.push({
|
|
535
|
-
type: "SHELL_INJECTION",
|
|
536
|
-
field,
|
|
537
|
-
value: truncate(value, 100),
|
|
538
|
-
description: "Shell injection pattern detected"
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
if (config.wildcardAbuse && detectWildcardAbuse(value)) {
|
|
542
|
-
threats.push({
|
|
543
|
-
type: "WILDCARD_ABUSE",
|
|
544
|
-
field,
|
|
545
|
-
value: truncate(value, 100),
|
|
546
|
-
description: "Wildcard abuse pattern detected"
|
|
547
|
-
});
|
|
548
1597
|
}
|
|
549
|
-
if (!
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1598
|
+
if (!apiKey) {
|
|
1599
|
+
const envKey = process.env.SOLONGATE_API_KEY;
|
|
1600
|
+
if (envKey) {
|
|
1601
|
+
apiKey = envKey;
|
|
1602
|
+
} else {
|
|
1603
|
+
throw new Error(
|
|
1604
|
+
"A valid SolonGate API key is required.\n\nUsage: solongate-proxy --api-key sg_live_xxx -- <command>\n or: set SOLONGATE_API_KEY=sg_live_xxx\n\nGet your API key at https://solongate.com\n"
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
556
1607
|
}
|
|
557
|
-
if (
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
value: truncate(value, 100),
|
|
562
|
-
description: "High entropy string detected - possible encoded payload"
|
|
563
|
-
});
|
|
1608
|
+
if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
|
|
1609
|
+
throw new Error(
|
|
1610
|
+
"Invalid API key format. Keys must start with 'sg_live_' or 'sg_test_'.\nGet your API key at https://solongate.com\n"
|
|
1611
|
+
);
|
|
564
1612
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
|
|
572
|
-
threats.push(...result.threats);
|
|
1613
|
+
if (configFile) {
|
|
1614
|
+
const filePath = resolve(configFile);
|
|
1615
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1616
|
+
const fileConfig = JSON.parse(content);
|
|
1617
|
+
if (!fileConfig.upstream) {
|
|
1618
|
+
throw new Error('Config file must include "upstream" with at least "command" or "url"');
|
|
573
1619
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1620
|
+
if (fileConfig.upstream.url && detectSSRF(fileConfig.upstream.url)) {
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
`Upstream URL blocked: "${fileConfig.upstream.url}" points to an internal or private network address.`
|
|
1623
|
+
);
|
|
578
1624
|
}
|
|
1625
|
+
return {
|
|
1626
|
+
upstream: fileConfig.upstream,
|
|
1627
|
+
policy: loadPolicy(fileConfig.policy ?? policySource),
|
|
1628
|
+
name: fileConfig.name ?? name,
|
|
1629
|
+
verbose: fileConfig.verbose ?? verbose,
|
|
1630
|
+
validateInput: fileConfig.validateInput ?? validateInput,
|
|
1631
|
+
rateLimitPerTool: fileConfig.rateLimitPerTool ?? rateLimitPerTool,
|
|
1632
|
+
globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit,
|
|
1633
|
+
apiKey: apiKey ?? fileConfig.apiKey,
|
|
1634
|
+
apiUrl: apiUrl ?? fileConfig.apiUrl,
|
|
1635
|
+
port: port ?? fileConfig.port
|
|
1636
|
+
};
|
|
579
1637
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1638
|
+
if (upstreamUrl) {
|
|
1639
|
+
if (detectSSRF(upstreamUrl)) {
|
|
1640
|
+
throw new Error(
|
|
1641
|
+
`Upstream URL blocked: "${upstreamUrl}" points to an internal or private network address.
|
|
1642
|
+
SSRF protection prevents connecting to localhost, private IPs, or cloud metadata endpoints.`
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
const transport = upstreamTransport ?? (upstreamUrl.includes("/sse") ? "sse" : "http");
|
|
1646
|
+
return {
|
|
1647
|
+
upstream: {
|
|
1648
|
+
transport,
|
|
1649
|
+
command: "",
|
|
1650
|
+
// not used for URL-based transports
|
|
1651
|
+
url: upstreamUrl
|
|
1652
|
+
},
|
|
1653
|
+
policy: loadPolicy(policySource),
|
|
1654
|
+
name,
|
|
1655
|
+
verbose,
|
|
1656
|
+
validateInput,
|
|
1657
|
+
rateLimitPerTool,
|
|
1658
|
+
globalRateLimit,
|
|
1659
|
+
apiKey,
|
|
1660
|
+
apiUrl,
|
|
1661
|
+
port
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
if (upstreamArgs.length === 0) {
|
|
1665
|
+
throw new Error(
|
|
1666
|
+
"No upstream server command provided.\n\nUsage: solongate-proxy [options] -- <command> [args...]\n\nExamples:\n solongate-proxy -- node my-server.js\n solongate-proxy --policy restricted -- npx @openclaw/server\n solongate-proxy --upstream-url http://localhost:3001/mcp\n solongate-proxy --config solongate.json\n"
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
const [command, ...commandArgs] = upstreamArgs;
|
|
1670
|
+
return {
|
|
1671
|
+
upstream: {
|
|
1672
|
+
transport: upstreamTransport ?? "stdio",
|
|
1673
|
+
command,
|
|
1674
|
+
args: commandArgs,
|
|
1675
|
+
env: { ...process.env }
|
|
1676
|
+
},
|
|
1677
|
+
policy: loadPolicy(policySource),
|
|
1678
|
+
name,
|
|
1679
|
+
verbose,
|
|
1680
|
+
validateInput,
|
|
1681
|
+
rateLimitPerTool,
|
|
1682
|
+
globalRateLimit,
|
|
1683
|
+
apiKey,
|
|
1684
|
+
apiUrl,
|
|
1685
|
+
port
|
|
1686
|
+
};
|
|
584
1687
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
1688
|
+
|
|
1689
|
+
// src/proxy.ts
|
|
1690
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1691
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1692
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1693
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1694
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
1695
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1696
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
1697
|
+
import {
|
|
1698
|
+
ListToolsRequestSchema,
|
|
1699
|
+
CallToolRequestSchema,
|
|
1700
|
+
ListResourcesRequestSchema,
|
|
1701
|
+
ListPromptsRequestSchema,
|
|
1702
|
+
GetPromptRequestSchema,
|
|
1703
|
+
ReadResourceRequestSchema,
|
|
1704
|
+
ListResourceTemplatesRequestSchema
|
|
1705
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
1706
|
+
import { createServer as createHttpServer } from "http";
|
|
588
1707
|
|
|
589
1708
|
// ../policy-engine/dist/index.js
|
|
590
1709
|
import { createHash } from "crypto";
|
|
@@ -735,8 +1854,51 @@ function trustLevelMeetsMinimum(actual, minimum) {
|
|
|
735
1854
|
function argumentConstraintsMatch(constraints, args) {
|
|
736
1855
|
for (const [key, constraint] of Object.entries(constraints)) {
|
|
737
1856
|
if (!(key in args)) return false;
|
|
738
|
-
|
|
739
|
-
|
|
1857
|
+
const argValue = args[key];
|
|
1858
|
+
if (typeof constraint === "string") {
|
|
1859
|
+
if (constraint === "*") continue;
|
|
1860
|
+
if (typeof argValue === "string") {
|
|
1861
|
+
if (argValue !== constraint) return false;
|
|
1862
|
+
} else {
|
|
1863
|
+
return false;
|
|
1864
|
+
}
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
if (typeof constraint === "object" && constraint !== null && !Array.isArray(constraint)) {
|
|
1868
|
+
const ops = constraint;
|
|
1869
|
+
const strValue = typeof argValue === "string" ? argValue : void 0;
|
|
1870
|
+
const numValue = typeof argValue === "number" ? argValue : void 0;
|
|
1871
|
+
if ("$contains" in ops && typeof ops.$contains === "string") {
|
|
1872
|
+
if (!strValue || !strValue.includes(ops.$contains)) return false;
|
|
1873
|
+
}
|
|
1874
|
+
if ("$notContains" in ops && typeof ops.$notContains === "string") {
|
|
1875
|
+
if (strValue && strValue.includes(ops.$notContains)) return false;
|
|
1876
|
+
}
|
|
1877
|
+
if ("$startsWith" in ops && typeof ops.$startsWith === "string") {
|
|
1878
|
+
if (!strValue || !strValue.startsWith(ops.$startsWith)) return false;
|
|
1879
|
+
}
|
|
1880
|
+
if ("$endsWith" in ops && typeof ops.$endsWith === "string") {
|
|
1881
|
+
if (!strValue || !strValue.endsWith(ops.$endsWith)) return false;
|
|
1882
|
+
}
|
|
1883
|
+
if ("$in" in ops && Array.isArray(ops.$in)) {
|
|
1884
|
+
if (!ops.$in.includes(argValue)) return false;
|
|
1885
|
+
}
|
|
1886
|
+
if ("$notIn" in ops && Array.isArray(ops.$notIn)) {
|
|
1887
|
+
if (ops.$notIn.includes(argValue)) return false;
|
|
1888
|
+
}
|
|
1889
|
+
if ("$gt" in ops && typeof ops.$gt === "number") {
|
|
1890
|
+
if (numValue === void 0 || numValue <= ops.$gt) return false;
|
|
1891
|
+
}
|
|
1892
|
+
if ("$lt" in ops && typeof ops.$lt === "number") {
|
|
1893
|
+
if (numValue === void 0 || numValue >= ops.$lt) return false;
|
|
1894
|
+
}
|
|
1895
|
+
if ("$gte" in ops && typeof ops.$gte === "number") {
|
|
1896
|
+
if (numValue === void 0 || numValue < ops.$gte) return false;
|
|
1897
|
+
}
|
|
1898
|
+
if ("$lte" in ops && typeof ops.$lte === "number") {
|
|
1899
|
+
if (numValue === void 0 || numValue > ops.$lte) return false;
|
|
1900
|
+
}
|
|
1901
|
+
continue;
|
|
740
1902
|
}
|
|
741
1903
|
}
|
|
742
1904
|
return true;
|
|
@@ -769,7 +1931,15 @@ function evaluatePolicy(policySet, request) {
|
|
|
769
1931
|
matchedRule: null,
|
|
770
1932
|
reason: "No matching policy rule found. Default action: DENY.",
|
|
771
1933
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
772
|
-
evaluationTimeMs: endTime - startTime
|
|
1934
|
+
evaluationTimeMs: endTime - startTime,
|
|
1935
|
+
metadata: {
|
|
1936
|
+
evaluatedRules: sortedRules.length,
|
|
1937
|
+
ruleIds: sortedRules.map((r) => r.id),
|
|
1938
|
+
requestContext: {
|
|
1939
|
+
tool: request.toolName,
|
|
1940
|
+
arguments: Object.keys(request.arguments ?? {})
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
773
1943
|
};
|
|
774
1944
|
}
|
|
775
1945
|
function validatePolicyRule(input) {
|
|
@@ -1091,6 +2261,7 @@ var PolicyStore = class {
|
|
|
1091
2261
|
|
|
1092
2262
|
// ../sdk-ts/dist/index.js
|
|
1093
2263
|
import { randomUUID, createHmac } from "crypto";
|
|
2264
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1094
2265
|
var DEFAULT_CONFIG = Object.freeze({
|
|
1095
2266
|
validateSchemas: true,
|
|
1096
2267
|
enableLogging: true,
|
|
@@ -1185,7 +2356,7 @@ async function interceptToolCall(params, upstreamCall, options) {
|
|
|
1185
2356
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1186
2357
|
};
|
|
1187
2358
|
options.onDecision?.(result);
|
|
1188
|
-
const reason = options.verboseErrors ? `Input validation failed: ${
|
|
2359
|
+
const reason = options.verboseErrors ? `Input validation failed: ${sanitization.threats.length} threat(s) detected` : "Input validation failed.";
|
|
1189
2360
|
return createDeniedToolResult(reason);
|
|
1190
2361
|
}
|
|
1191
2362
|
}
|
|
@@ -1503,6 +2674,25 @@ var RateLimiter = class {
|
|
|
1503
2674
|
const resetAt = this.globalRecords.length > 0 ? this.globalRecords[0].timestamp + this.windowMs : now + this.windowMs;
|
|
1504
2675
|
return { allowed, remaining, resetAt };
|
|
1505
2676
|
}
|
|
2677
|
+
/**
|
|
2678
|
+
* Atomically checks and records a tool call.
|
|
2679
|
+
* Prevents TOCTOU race conditions between check and record.
|
|
2680
|
+
* Returns the rate limit result; if allowed, the call is already recorded.
|
|
2681
|
+
*/
|
|
2682
|
+
checkAndRecord(toolName, limitPerWindow, globalLimit) {
|
|
2683
|
+
const result = this.checkLimit(toolName, limitPerWindow);
|
|
2684
|
+
if (!result.allowed) {
|
|
2685
|
+
return result;
|
|
2686
|
+
}
|
|
2687
|
+
if (globalLimit !== void 0) {
|
|
2688
|
+
const globalResult = this.checkGlobalLimit(globalLimit);
|
|
2689
|
+
if (!globalResult.allowed) {
|
|
2690
|
+
return globalResult;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
this.recordCall(toolName);
|
|
2694
|
+
return result;
|
|
2695
|
+
}
|
|
1506
2696
|
/**
|
|
1507
2697
|
* Records a tool call for rate limiting.
|
|
1508
2698
|
* Call this after successful execution.
|
|
@@ -1558,6 +2748,16 @@ var RateLimiter = class {
|
|
|
1558
2748
|
return active;
|
|
1559
2749
|
}
|
|
1560
2750
|
};
|
|
2751
|
+
var LicenseError = class extends Error {
|
|
2752
|
+
constructor(message) {
|
|
2753
|
+
super(
|
|
2754
|
+
`${message}
|
|
2755
|
+
Get your API key at https://solongate.com
|
|
2756
|
+
Usage: new SolonGate({ name: '...', apiKey: 'sg_live_xxx' })`
|
|
2757
|
+
);
|
|
2758
|
+
this.name = "LicenseError";
|
|
2759
|
+
}
|
|
2760
|
+
};
|
|
1561
2761
|
var SolonGate = class {
|
|
1562
2762
|
policyEngine;
|
|
1563
2763
|
config;
|
|
@@ -1566,7 +2766,19 @@ var SolonGate = class {
|
|
|
1566
2766
|
tokenIssuer;
|
|
1567
2767
|
serverVerifier;
|
|
1568
2768
|
rateLimiter;
|
|
2769
|
+
apiKey;
|
|
2770
|
+
licenseValidated = false;
|
|
1569
2771
|
constructor(options) {
|
|
2772
|
+
const apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
|
|
2773
|
+
if (!apiKey) {
|
|
2774
|
+
throw new LicenseError("A valid SolonGate API key is required.");
|
|
2775
|
+
}
|
|
2776
|
+
if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
|
|
2777
|
+
throw new LicenseError(
|
|
2778
|
+
"Invalid API key format. Keys must start with 'sg_live_' or 'sg_test_'."
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
this.apiKey = apiKey;
|
|
1570
2782
|
const { config, warnings } = resolveConfig(options.config);
|
|
1571
2783
|
this.config = config;
|
|
1572
2784
|
this.configWarnings = warnings;
|
|
@@ -1592,12 +2804,47 @@ var SolonGate = class {
|
|
|
1592
2804
|
this.serverVerifier = config.gatewaySecret ? new ServerVerifier({ gatewaySecret: config.gatewaySecret }) : null;
|
|
1593
2805
|
this.rateLimiter = new RateLimiter();
|
|
1594
2806
|
}
|
|
2807
|
+
/**
|
|
2808
|
+
* Validate the API key against the SolonGate cloud API.
|
|
2809
|
+
* Called once on first executeToolCall. Throws LicenseError if invalid.
|
|
2810
|
+
* Test keys (sg_test_) skip online validation.
|
|
2811
|
+
*/
|
|
2812
|
+
async validateLicense() {
|
|
2813
|
+
if (this.licenseValidated) return;
|
|
2814
|
+
if (this.apiKey.startsWith("sg_test_")) {
|
|
2815
|
+
this.licenseValidated = true;
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
|
|
2819
|
+
try {
|
|
2820
|
+
const res = await fetch(`${apiUrl}/api/v1/auth/me`, {
|
|
2821
|
+
headers: {
|
|
2822
|
+
"X-API-Key": this.apiKey,
|
|
2823
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
2824
|
+
},
|
|
2825
|
+
signal: AbortSignal.timeout(1e4)
|
|
2826
|
+
});
|
|
2827
|
+
if (res.status === 401) {
|
|
2828
|
+
throw new LicenseError("Invalid or expired API key.");
|
|
2829
|
+
}
|
|
2830
|
+
if (res.status === 403) {
|
|
2831
|
+
throw new LicenseError("Your subscription is inactive. Renew at https://solongate.com");
|
|
2832
|
+
}
|
|
2833
|
+
this.licenseValidated = true;
|
|
2834
|
+
} catch (err) {
|
|
2835
|
+
if (err instanceof LicenseError) throw err;
|
|
2836
|
+
throw new LicenseError(
|
|
2837
|
+
"Unable to reach SolonGate license server. Check your internet connection."
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
1595
2841
|
/**
|
|
1596
2842
|
* Intercept and evaluate a tool call against the full security pipeline.
|
|
1597
2843
|
* If denied at any stage, returns an error result without calling upstream.
|
|
1598
2844
|
* If allowed, calls upstream and returns the result.
|
|
1599
2845
|
*/
|
|
1600
2846
|
async executeToolCall(params, upstreamCall) {
|
|
2847
|
+
await this.validateLicense();
|
|
1601
2848
|
return interceptToolCall(params, upstreamCall, {
|
|
1602
2849
|
policyEngine: this.policyEngine,
|
|
1603
2850
|
validateSchemas: this.config.validateSchemas,
|
|
@@ -1647,8 +2894,8 @@ var Mutex = class {
|
|
|
1647
2894
|
this.locked = true;
|
|
1648
2895
|
return;
|
|
1649
2896
|
}
|
|
1650
|
-
return new Promise((
|
|
1651
|
-
this.queue.push(
|
|
2897
|
+
return new Promise((resolve5) => {
|
|
2898
|
+
this.queue.push(resolve5);
|
|
1652
2899
|
});
|
|
1653
2900
|
}
|
|
1654
2901
|
release() {
|
|
@@ -1689,30 +2936,88 @@ var SolonGateProxy = class {
|
|
|
1689
2936
|
*/
|
|
1690
2937
|
async start() {
|
|
1691
2938
|
log("Starting SolonGate Proxy...");
|
|
2939
|
+
const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
|
|
2940
|
+
if (this.config.apiKey) {
|
|
2941
|
+
log(`Validating license with ${apiUrl}...`);
|
|
2942
|
+
try {
|
|
2943
|
+
const res = await fetch(`${apiUrl}/api/v1/auth/me`, {
|
|
2944
|
+
headers: {
|
|
2945
|
+
"X-API-Key": this.config.apiKey,
|
|
2946
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
2947
|
+
},
|
|
2948
|
+
signal: AbortSignal.timeout(1e4)
|
|
2949
|
+
});
|
|
2950
|
+
if (res.status === 401) {
|
|
2951
|
+
log("ERROR: Invalid or expired API key.");
|
|
2952
|
+
process.exit(1);
|
|
2953
|
+
}
|
|
2954
|
+
if (res.status === 403) {
|
|
2955
|
+
log("ERROR: Your subscription is inactive. Renew at https://solongate.com");
|
|
2956
|
+
process.exit(1);
|
|
2957
|
+
}
|
|
2958
|
+
log("License validated.");
|
|
2959
|
+
} catch (err) {
|
|
2960
|
+
log(`ERROR: Unable to reach SolonGate license server. Check your internet connection.`);
|
|
2961
|
+
log(`Details: ${err instanceof Error ? err.message : String(err)}`);
|
|
2962
|
+
process.exit(1);
|
|
2963
|
+
}
|
|
2964
|
+
try {
|
|
2965
|
+
const cloudPolicy = await fetchCloudPolicy(this.config.apiKey, apiUrl);
|
|
2966
|
+
this.config.policy = cloudPolicy;
|
|
2967
|
+
log(`Loaded cloud policy: ${cloudPolicy.name} (${cloudPolicy.rules.length} rules)`);
|
|
2968
|
+
} catch (err) {
|
|
2969
|
+
log(`Cloud policy fetch failed, using local policy: ${err instanceof Error ? err.message : String(err)}`);
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
1692
2972
|
log(`Policy: ${this.config.policy.name} (${this.config.policy.rules.length} rules)`);
|
|
1693
|
-
|
|
2973
|
+
const transport = this.config.upstream.transport ?? "stdio";
|
|
2974
|
+
if (transport === "stdio") {
|
|
2975
|
+
log(`Upstream: [stdio] ${this.config.upstream.command} ${(this.config.upstream.args ?? []).join(" ")}`);
|
|
2976
|
+
} else {
|
|
2977
|
+
log(`Upstream: [${transport}] ${this.config.upstream.url}`);
|
|
2978
|
+
}
|
|
1694
2979
|
await this.connectUpstream();
|
|
1695
2980
|
await this.discoverTools();
|
|
1696
2981
|
this.createServer();
|
|
1697
2982
|
await this.serve();
|
|
1698
2983
|
}
|
|
1699
2984
|
/**
|
|
1700
|
-
* Connect to the upstream MCP server
|
|
2985
|
+
* Connect to the upstream MCP server.
|
|
2986
|
+
* Supports stdio (child process), SSE, and StreamableHTTP transports.
|
|
1701
2987
|
*/
|
|
1702
2988
|
async connectUpstream() {
|
|
1703
2989
|
this.client = new Client(
|
|
1704
2990
|
{ name: "solongate-proxy-client", version: "0.1.0" },
|
|
1705
2991
|
{ capabilities: {} }
|
|
1706
2992
|
);
|
|
1707
|
-
const
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
2993
|
+
const upstreamTransport = this.config.upstream.transport ?? "stdio";
|
|
2994
|
+
switch (upstreamTransport) {
|
|
2995
|
+
case "sse": {
|
|
2996
|
+
if (!this.config.upstream.url) throw new Error("--upstream-url required for SSE transport");
|
|
2997
|
+
const transport = new SSEClientTransport(new URL(this.config.upstream.url));
|
|
2998
|
+
await this.client.connect(transport);
|
|
2999
|
+
break;
|
|
3000
|
+
}
|
|
3001
|
+
case "http": {
|
|
3002
|
+
if (!this.config.upstream.url) throw new Error("--upstream-url required for HTTP transport");
|
|
3003
|
+
const transport = new StreamableHTTPClientTransport(new URL(this.config.upstream.url));
|
|
3004
|
+
await this.client.connect(transport);
|
|
3005
|
+
break;
|
|
3006
|
+
}
|
|
3007
|
+
case "stdio":
|
|
3008
|
+
default: {
|
|
3009
|
+
const transport = new StdioClientTransport({
|
|
3010
|
+
command: this.config.upstream.command,
|
|
3011
|
+
args: this.config.upstream.args,
|
|
3012
|
+
env: this.config.upstream.env,
|
|
3013
|
+
cwd: this.config.upstream.cwd,
|
|
3014
|
+
stderr: "pipe"
|
|
3015
|
+
});
|
|
3016
|
+
await this.client.connect(transport);
|
|
3017
|
+
break;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
log(`Connected to upstream server (${upstreamTransport})`);
|
|
1716
3021
|
}
|
|
1717
3022
|
/**
|
|
1718
3023
|
* Discover tools from the upstream server.
|
|
@@ -1751,10 +3056,20 @@ var SolonGateProxy = class {
|
|
|
1751
3056
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1752
3057
|
return { tools: this.upstreamTools };
|
|
1753
3058
|
});
|
|
3059
|
+
const MAX_ARGUMENT_SIZE = 1024 * 1024;
|
|
1754
3060
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1755
3061
|
const { name, arguments: args } = request.params;
|
|
3062
|
+
const argsSize = JSON.stringify(args ?? {}).length;
|
|
3063
|
+
if (argsSize > MAX_ARGUMENT_SIZE) {
|
|
3064
|
+
log(`DENY: ${name} \u2014 payload size ${argsSize} exceeds limit ${MAX_ARGUMENT_SIZE}`);
|
|
3065
|
+
return {
|
|
3066
|
+
content: [{ type: "text", text: `Request payload too large (${Math.round(argsSize / 1024)}KB > ${Math.round(MAX_ARGUMENT_SIZE / 1024)}KB limit)` }],
|
|
3067
|
+
isError: true
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
1756
3070
|
log(`Tool call: ${name}`);
|
|
1757
3071
|
await this.callMutex.acquire();
|
|
3072
|
+
const startTime = Date.now();
|
|
1758
3073
|
try {
|
|
1759
3074
|
const result = await this.gate.executeToolCall(
|
|
1760
3075
|
{ name, arguments: args ?? {} },
|
|
@@ -1767,7 +3082,19 @@ var SolonGateProxy = class {
|
|
|
1767
3082
|
return upstreamResult;
|
|
1768
3083
|
}
|
|
1769
3084
|
);
|
|
1770
|
-
|
|
3085
|
+
const decision = result.isError ? "DENY" : "ALLOW";
|
|
3086
|
+
const evaluationTimeMs = Date.now() - startTime;
|
|
3087
|
+
log(`Result: ${decision} (${evaluationTimeMs}ms)`);
|
|
3088
|
+
if (this.config.apiKey) {
|
|
3089
|
+
const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
|
|
3090
|
+
sendAuditLog(this.config.apiKey, apiUrl, {
|
|
3091
|
+
tool: name,
|
|
3092
|
+
arguments: args ?? {},
|
|
3093
|
+
decision,
|
|
3094
|
+
reason: result.isError ? result.content[0]?.text ?? "denied" : "allowed",
|
|
3095
|
+
evaluationTimeMs
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
1771
3098
|
return {
|
|
1772
3099
|
content: [...result.content],
|
|
1773
3100
|
isError: result.isError
|
|
@@ -1813,14 +3140,38 @@ var SolonGateProxy = class {
|
|
|
1813
3140
|
});
|
|
1814
3141
|
}
|
|
1815
3142
|
/**
|
|
1816
|
-
* Start serving
|
|
3143
|
+
* Start serving downstream.
|
|
3144
|
+
* If --port is set, serves via StreamableHTTP on that port.
|
|
3145
|
+
* Otherwise, serves on stdio (default for Claude Code / Cursor / etc).
|
|
1817
3146
|
*/
|
|
1818
3147
|
async serve() {
|
|
1819
3148
|
if (!this.server) throw new Error("Server not created");
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
3149
|
+
if (this.config.port) {
|
|
3150
|
+
const httpTransport = new StreamableHTTPServerTransport({
|
|
3151
|
+
sessionIdGenerator: () => crypto.randomUUID()
|
|
3152
|
+
});
|
|
3153
|
+
await this.server.connect(httpTransport);
|
|
3154
|
+
const httpServer = createHttpServer(async (req, res) => {
|
|
3155
|
+
if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) {
|
|
3156
|
+
await httpTransport.handleRequest(req, res);
|
|
3157
|
+
} else if (req.url === "/health") {
|
|
3158
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3159
|
+
res.end(JSON.stringify({ status: "healthy", proxy: this.config.name ?? "solongate-proxy" }));
|
|
3160
|
+
} else {
|
|
3161
|
+
res.writeHead(404);
|
|
3162
|
+
res.end("Not found. Use /mcp for MCP protocol or /health for health check.");
|
|
3163
|
+
}
|
|
3164
|
+
});
|
|
3165
|
+
httpServer.listen(this.config.port, () => {
|
|
3166
|
+
log(`Proxy is live on http://localhost:${this.config.port}/mcp`);
|
|
3167
|
+
log("All tool calls are now protected by SolonGate.");
|
|
3168
|
+
});
|
|
3169
|
+
} else {
|
|
3170
|
+
const transport = new StdioServerTransport();
|
|
3171
|
+
await this.server.connect(transport);
|
|
3172
|
+
log("Proxy is live. All tool calls are now protected by SolonGate.");
|
|
3173
|
+
log("Waiting for requests...");
|
|
3174
|
+
}
|
|
1824
3175
|
}
|
|
1825
3176
|
};
|
|
1826
3177
|
|
|
@@ -1837,7 +3188,23 @@ console.error = (...args) => {
|
|
|
1837
3188
|
process.stderr.write(`[SolonGate ERROR] ${args.map(String).join(" ")}
|
|
1838
3189
|
`);
|
|
1839
3190
|
};
|
|
1840
|
-
async function
|
|
3191
|
+
async function main4() {
|
|
3192
|
+
const subcommand = process.argv[2];
|
|
3193
|
+
if (subcommand === "init") {
|
|
3194
|
+
process.argv.splice(2, 1);
|
|
3195
|
+
await Promise.resolve().then(() => (init_init(), init_exports));
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
if (subcommand === "inject") {
|
|
3199
|
+
process.argv.splice(2, 1);
|
|
3200
|
+
await Promise.resolve().then(() => (init_inject(), inject_exports));
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
if (subcommand === "create") {
|
|
3204
|
+
process.argv.splice(2, 1);
|
|
3205
|
+
await Promise.resolve().then(() => (init_create(), create_exports));
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
1841
3208
|
try {
|
|
1842
3209
|
const config = parseArgs(process.argv);
|
|
1843
3210
|
const proxy = new SolonGateProxy(config);
|
|
@@ -1849,4 +3216,4 @@ async function main() {
|
|
|
1849
3216
|
process.exit(1);
|
|
1850
3217
|
}
|
|
1851
3218
|
}
|
|
1852
|
-
|
|
3219
|
+
main4();
|