@solongate/proxy 0.1.2 → 0.1.4
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/dist/create.js +263 -0
- package/dist/index.js +1213 -84
- package/dist/inject.js +339 -0
- package/package.json +3 -3
package/dist/inject.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/inject.ts
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync } from "fs";
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
function parseInjectArgs(argv) {
|
|
8
|
+
const args = argv.slice(2);
|
|
9
|
+
const opts = {
|
|
10
|
+
dryRun: false,
|
|
11
|
+
restore: false,
|
|
12
|
+
skipInstall: false
|
|
13
|
+
};
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
switch (args[i]) {
|
|
16
|
+
case "--file":
|
|
17
|
+
opts.file = args[++i];
|
|
18
|
+
break;
|
|
19
|
+
case "--dry-run":
|
|
20
|
+
opts.dryRun = true;
|
|
21
|
+
break;
|
|
22
|
+
case "--restore":
|
|
23
|
+
opts.restore = true;
|
|
24
|
+
break;
|
|
25
|
+
case "--skip-install":
|
|
26
|
+
opts.skipInstall = true;
|
|
27
|
+
break;
|
|
28
|
+
case "--help":
|
|
29
|
+
case "-h":
|
|
30
|
+
printHelp();
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return opts;
|
|
35
|
+
}
|
|
36
|
+
function printHelp() {
|
|
37
|
+
log(`
|
|
38
|
+
SolonGate Inject \u2014 Add security to your MCP server in seconds
|
|
39
|
+
|
|
40
|
+
USAGE
|
|
41
|
+
npx @solongate/proxy inject [options]
|
|
42
|
+
|
|
43
|
+
OPTIONS
|
|
44
|
+
--file <path> Entry file to modify (default: auto-detect)
|
|
45
|
+
--dry-run Preview changes without writing
|
|
46
|
+
--restore Restore original file from backup
|
|
47
|
+
--skip-install Don't install SDK package
|
|
48
|
+
-h, --help Show this help message
|
|
49
|
+
|
|
50
|
+
EXAMPLES
|
|
51
|
+
npx @solongate/proxy inject # Auto-detect and inject
|
|
52
|
+
npx @solongate/proxy inject --file src/main.ts # Specify entry file
|
|
53
|
+
npx @solongate/proxy inject --dry-run # Preview changes
|
|
54
|
+
npx @solongate/proxy inject --restore # Undo injection
|
|
55
|
+
|
|
56
|
+
WHAT IT DOES
|
|
57
|
+
Replaces McpServer with SecureMcpServer (2 lines changed)
|
|
58
|
+
All tool() calls are automatically protected by SolonGate.
|
|
59
|
+
`);
|
|
60
|
+
}
|
|
61
|
+
function log(msg) {
|
|
62
|
+
process.stderr.write(msg + "\n");
|
|
63
|
+
}
|
|
64
|
+
function detectProject() {
|
|
65
|
+
if (!existsSync(resolve("package.json"))) return false;
|
|
66
|
+
try {
|
|
67
|
+
const pkg = JSON.parse(readFileSync(resolve("package.json"), "utf-8"));
|
|
68
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
69
|
+
return !!(allDeps["@modelcontextprotocol/sdk"] || allDeps["@modelcontextprotocol/server"]);
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function findTsEntryFile() {
|
|
75
|
+
try {
|
|
76
|
+
const pkg = JSON.parse(readFileSync(resolve("package.json"), "utf-8"));
|
|
77
|
+
if (pkg.bin) {
|
|
78
|
+
const binPath = typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin)[0];
|
|
79
|
+
if (typeof binPath === "string") {
|
|
80
|
+
const srcPath = binPath.replace(/^\.\/dist\//, "./src/").replace(/\.js$/, ".ts");
|
|
81
|
+
if (existsSync(resolve(srcPath))) return resolve(srcPath);
|
|
82
|
+
if (existsSync(resolve(binPath))) return resolve(binPath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (pkg.main) {
|
|
86
|
+
const srcPath = pkg.main.replace(/^\.\/dist\//, "./src/").replace(/\.js$/, ".ts");
|
|
87
|
+
if (existsSync(resolve(srcPath))) return resolve(srcPath);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
const candidates = [
|
|
92
|
+
"src/index.ts",
|
|
93
|
+
"src/server.ts",
|
|
94
|
+
"src/main.ts",
|
|
95
|
+
"index.ts",
|
|
96
|
+
"server.ts",
|
|
97
|
+
"main.ts"
|
|
98
|
+
];
|
|
99
|
+
for (const c of candidates) {
|
|
100
|
+
const full = resolve(c);
|
|
101
|
+
if (existsSync(full)) {
|
|
102
|
+
try {
|
|
103
|
+
const content = readFileSync(full, "utf-8");
|
|
104
|
+
if (content.includes("McpServer") || content.includes("McpServer")) {
|
|
105
|
+
return full;
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const c of candidates) {
|
|
112
|
+
if (existsSync(resolve(c))) return resolve(c);
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
function detectPackageManager() {
|
|
117
|
+
if (existsSync(resolve("pnpm-lock.yaml"))) return "pnpm";
|
|
118
|
+
if (existsSync(resolve("yarn.lock"))) return "yarn";
|
|
119
|
+
return "npm";
|
|
120
|
+
}
|
|
121
|
+
function installSdk() {
|
|
122
|
+
try {
|
|
123
|
+
const pkg = JSON.parse(readFileSync(resolve("package.json"), "utf-8"));
|
|
124
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
125
|
+
if (allDeps["@solongate/sdk"]) {
|
|
126
|
+
log(" @solongate/sdk already installed");
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
const pm = detectPackageManager();
|
|
132
|
+
const cmd = pm === "yarn" ? "yarn add @solongate/sdk" : `${pm} install @solongate/sdk`;
|
|
133
|
+
log(` Installing @solongate/sdk via ${pm}...`);
|
|
134
|
+
try {
|
|
135
|
+
execSync(cmd, { stdio: "pipe", cwd: process.cwd() });
|
|
136
|
+
return true;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
log(` Failed to install: ${err instanceof Error ? err.message : String(err)}`);
|
|
139
|
+
log(" You can install manually: npm install @solongate/sdk");
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function injectTypeScript(filePath) {
|
|
144
|
+
const original = readFileSync(filePath, "utf-8");
|
|
145
|
+
const changes = [];
|
|
146
|
+
let modified = original;
|
|
147
|
+
if (modified.includes("SecureMcpServer")) {
|
|
148
|
+
return { file: filePath, changes: ["Already injected \u2014 skipping"], original, modified };
|
|
149
|
+
}
|
|
150
|
+
const mcpImportPatterns = [
|
|
151
|
+
// Solo import: import { McpServer } from '...'
|
|
152
|
+
/import\s*\{\s*McpServer\s*\}\s*from\s*['"][^'"]+['"]/,
|
|
153
|
+
// Import with others: import { McpServer, ... } from '...'
|
|
154
|
+
/import\s*\{[^}]*McpServer[^}]*\}\s*from\s*['"][^'"]+['"]/
|
|
155
|
+
];
|
|
156
|
+
let importReplaced = false;
|
|
157
|
+
for (const pattern of mcpImportPatterns) {
|
|
158
|
+
const match = modified.match(pattern);
|
|
159
|
+
if (match) {
|
|
160
|
+
const importLine = match[0];
|
|
161
|
+
const namedImports = importLine.match(/\{([^}]+)\}/)?.[1] ?? "";
|
|
162
|
+
const importNames = namedImports.split(",").map((s) => s.trim()).filter(Boolean);
|
|
163
|
+
if (importNames.length === 1 && importNames[0] === "McpServer") {
|
|
164
|
+
modified = modified.replace(
|
|
165
|
+
importLine,
|
|
166
|
+
`import { SecureMcpServer } from '@solongate/sdk'`
|
|
167
|
+
);
|
|
168
|
+
changes.push("Replaced McpServer import with SecureMcpServer from @solongate/sdk");
|
|
169
|
+
} else {
|
|
170
|
+
const otherImports = importNames.filter((n) => n !== "McpServer");
|
|
171
|
+
const fromModule = importLine.match(/from\s*['"]([^'"]+)['"]/)?.[1] ?? "";
|
|
172
|
+
modified = modified.replace(
|
|
173
|
+
importLine,
|
|
174
|
+
`import { ${otherImports.join(", ")} } from '${fromModule}';
|
|
175
|
+
import { SecureMcpServer } from '@solongate/sdk'`
|
|
176
|
+
);
|
|
177
|
+
changes.push("Removed McpServer from existing import, added SecureMcpServer import from @solongate/sdk");
|
|
178
|
+
}
|
|
179
|
+
importReplaced = true;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!importReplaced) {
|
|
184
|
+
const insertPos = findImportInsertPosition(modified);
|
|
185
|
+
modified = modified.slice(0, insertPos) + `import { SecureMcpServer } from '@solongate/sdk';
|
|
186
|
+
` + modified.slice(insertPos);
|
|
187
|
+
changes.push("Added SecureMcpServer import from @solongate/sdk");
|
|
188
|
+
}
|
|
189
|
+
const constructorPattern = /new\s+McpServer\s*\(/g;
|
|
190
|
+
const constructorCount = (modified.match(constructorPattern) || []).length;
|
|
191
|
+
if (constructorCount > 0) {
|
|
192
|
+
modified = modified.replace(constructorPattern, "new SecureMcpServer(");
|
|
193
|
+
changes.push(`Replaced ${constructorCount} McpServer constructor(s) with SecureMcpServer`);
|
|
194
|
+
}
|
|
195
|
+
return { file: filePath, changes, original, modified };
|
|
196
|
+
}
|
|
197
|
+
function findImportInsertPosition(content) {
|
|
198
|
+
let pos = 0;
|
|
199
|
+
if (content.startsWith("#!")) {
|
|
200
|
+
pos = content.indexOf("\n") + 1;
|
|
201
|
+
}
|
|
202
|
+
const lines = content.slice(pos).split("\n");
|
|
203
|
+
let offset = pos;
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
const trimmed = line.trim();
|
|
206
|
+
if (trimmed === "" || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("*/")) {
|
|
207
|
+
offset += line.length + 1;
|
|
208
|
+
} else {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const importRegex = /^import\s+.+$/gm;
|
|
213
|
+
let lastImportEnd = offset;
|
|
214
|
+
let importMatch;
|
|
215
|
+
while ((importMatch = importRegex.exec(content)) !== null) {
|
|
216
|
+
lastImportEnd = importMatch.index + importMatch[0].length + 1;
|
|
217
|
+
}
|
|
218
|
+
return lastImportEnd > offset ? lastImportEnd : offset;
|
|
219
|
+
}
|
|
220
|
+
function showDiff(result) {
|
|
221
|
+
if (result.original === result.modified) {
|
|
222
|
+
log(" No changes needed.");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const origLines = result.original.split("\n");
|
|
226
|
+
const modLines = result.modified.split("\n");
|
|
227
|
+
log("");
|
|
228
|
+
log(` File: ${result.file}`);
|
|
229
|
+
log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
230
|
+
const maxLines = Math.max(origLines.length, modLines.length);
|
|
231
|
+
let diffCount = 0;
|
|
232
|
+
for (let i = 0; i < maxLines; i++) {
|
|
233
|
+
const orig = origLines[i];
|
|
234
|
+
const mod = modLines[i];
|
|
235
|
+
if (orig !== mod) {
|
|
236
|
+
if (diffCount < 30) {
|
|
237
|
+
if (orig !== void 0) log(` - ${orig}`);
|
|
238
|
+
if (mod !== void 0) log(` + ${mod}`);
|
|
239
|
+
}
|
|
240
|
+
diffCount++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (diffCount > 30) {
|
|
244
|
+
log(` ... and ${diffCount - 30} more line changes`);
|
|
245
|
+
}
|
|
246
|
+
log("");
|
|
247
|
+
}
|
|
248
|
+
async function main() {
|
|
249
|
+
const opts = parseInjectArgs(process.argv);
|
|
250
|
+
log("");
|
|
251
|
+
log(" \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");
|
|
252
|
+
log(" \u2551 SolonGate \u2014 Inject SDK \u2551");
|
|
253
|
+
log(" \u2551 Add security to your MCP server \u2551");
|
|
254
|
+
log(" \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");
|
|
255
|
+
log("");
|
|
256
|
+
if (!detectProject()) {
|
|
257
|
+
log(" Could not detect a TypeScript MCP server project.");
|
|
258
|
+
log(" Make sure you are in a project directory with:");
|
|
259
|
+
log(" package.json + @modelcontextprotocol/sdk in dependencies");
|
|
260
|
+
log("");
|
|
261
|
+
log(" To create a new MCP server: npx @solongate/proxy create <name>");
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
log(" Language: TypeScript");
|
|
265
|
+
const entryFile = opts.file ? resolve(opts.file) : findTsEntryFile();
|
|
266
|
+
if (!entryFile || !existsSync(entryFile)) {
|
|
267
|
+
log(` Could not find entry file.${opts.file ? ` File not found: ${opts.file}` : ""}`);
|
|
268
|
+
log("");
|
|
269
|
+
log(" Specify it manually: --file <path>");
|
|
270
|
+
log("");
|
|
271
|
+
log(" Common entry points:");
|
|
272
|
+
log(" src/index.ts, src/server.ts, index.ts");
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
log(` Entry: ${entryFile}`);
|
|
276
|
+
log("");
|
|
277
|
+
const backupPath = entryFile + ".solongate-backup";
|
|
278
|
+
if (opts.restore) {
|
|
279
|
+
if (!existsSync(backupPath)) {
|
|
280
|
+
log(" No backup found. Nothing to restore.");
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
copyFileSync(backupPath, entryFile);
|
|
284
|
+
log(` Restored original file from backup.`);
|
|
285
|
+
log(` Backup: ${backupPath}`);
|
|
286
|
+
process.exit(0);
|
|
287
|
+
}
|
|
288
|
+
if (!opts.skipInstall && !opts.dryRun) {
|
|
289
|
+
installSdk();
|
|
290
|
+
log("");
|
|
291
|
+
}
|
|
292
|
+
const result = injectTypeScript(entryFile);
|
|
293
|
+
log(` Changes (${result.changes.length}):`);
|
|
294
|
+
for (const change of result.changes) {
|
|
295
|
+
log(` - ${change}`);
|
|
296
|
+
}
|
|
297
|
+
if (result.changes.length === 1 && result.changes[0].includes("Already injected")) {
|
|
298
|
+
log("");
|
|
299
|
+
log(" Your MCP server is already protected by SolonGate!");
|
|
300
|
+
process.exit(0);
|
|
301
|
+
}
|
|
302
|
+
if (result.original === result.modified) {
|
|
303
|
+
log("");
|
|
304
|
+
log(" No changes were made. The file may not contain recognizable MCP patterns.");
|
|
305
|
+
log(" See docs: https://solongate.com/docs/integration");
|
|
306
|
+
process.exit(0);
|
|
307
|
+
}
|
|
308
|
+
if (opts.dryRun) {
|
|
309
|
+
log("");
|
|
310
|
+
log(" --- DRY RUN (no changes written) ---");
|
|
311
|
+
showDiff(result);
|
|
312
|
+
log(" To apply: npx @solongate/proxy inject");
|
|
313
|
+
process.exit(0);
|
|
314
|
+
}
|
|
315
|
+
if (!existsSync(backupPath)) {
|
|
316
|
+
copyFileSync(entryFile, backupPath);
|
|
317
|
+
log("");
|
|
318
|
+
log(` Backup: ${backupPath}`);
|
|
319
|
+
}
|
|
320
|
+
writeFileSync(entryFile, result.modified);
|
|
321
|
+
log("");
|
|
322
|
+
log(" \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");
|
|
323
|
+
log(" \u2502 SolonGate SDK injected successfully! \u2502");
|
|
324
|
+
log(" \u2502 \u2502");
|
|
325
|
+
log(" \u2502 McpServer \u2192 SecureMcpServer \u2502");
|
|
326
|
+
log(" \u2502 All tool() calls are now auto-protected. \u2502");
|
|
327
|
+
log(" \u2502 \u2502");
|
|
328
|
+
log(" \u2502 Set your API key: \u2502");
|
|
329
|
+
log(" \u2502 export SOLONGATE_API_KEY=sg_live_xxx \u2502");
|
|
330
|
+
log(" \u2502 \u2502");
|
|
331
|
+
log(" \u2502 To undo: \u2502");
|
|
332
|
+
log(" \u2502 npx @solongate/proxy inject --restore \u2502");
|
|
333
|
+
log(" \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");
|
|
334
|
+
log("");
|
|
335
|
+
}
|
|
336
|
+
main().catch((err) => {
|
|
337
|
+
log(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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": {
|
|
@@ -51,13 +51,13 @@
|
|
|
51
51
|
"node": ">=18.0.0"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
54
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
55
55
|
"zod": "^3.25.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@solongate/sdk": "workspace:*",
|
|
59
58
|
"@solongate/core": "workspace:*",
|
|
60
59
|
"@solongate/policy-engine": "workspace:*",
|
|
60
|
+
"@solongate/sdk": "workspace:*",
|
|
61
61
|
"@solongate/tsconfig": "workspace:*",
|
|
62
62
|
"tsup": "^8.3.0",
|
|
63
63
|
"tsx": "^4.19.0",
|