@solongate/proxy 0.1.2 → 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/dist/index.js CHANGED
@@ -285,315 +285,601 @@ var init_init = __esm({
285
285
  }
286
286
  });
287
287
 
288
- // src/config.ts
289
- import { readFileSync, existsSync } from "fs";
290
- import { resolve } from "path";
291
- async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
292
- const url = `${apiUrl}/api/v1/policies/${policyId ?? "default"}`;
293
- const res = await fetch(url, {
294
- headers: { "Authorization": `Bearer ${apiKey}` }
295
- });
296
- if (!res.ok) {
297
- const body = await res.text().catch(() => "");
298
- throw new Error(`Failed to fetch policy from cloud (${res.status}): ${body}`);
299
- }
300
- const data = await res.json();
301
- return {
302
- id: String(data.id ?? "cloud"),
303
- name: String(data.name ?? "Cloud Policy"),
304
- version: Number(data._version ?? 1),
305
- rules: data.rules ?? [],
306
- createdAt: String(data._created_at ?? ""),
307
- updatedAt: ""
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
308
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;
309
321
  }
310
- async function sendAuditLog(apiKey, apiUrl, entry) {
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;
311
352
  try {
312
- await fetch(`${apiUrl}/api/v1/audit-logs`, {
313
- method: "POST",
314
- headers: {
315
- "Authorization": `Bearer ${apiKey}`,
316
- "Content-Type": "application/json"
317
- },
318
- body: JSON.stringify(entry)
319
- });
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"]);
320
356
  } catch {
357
+ return false;
321
358
  }
322
359
  }
323
- var PRESETS = {
324
- restricted: {
325
- id: "restricted",
326
- name: "Restricted",
327
- description: "Blocks dangerous tools (shell, web), allows safe tools",
328
- version: 1,
329
- rules: [
330
- {
331
- id: "deny-shell",
332
- description: "Block shell execution",
333
- effect: "DENY",
334
- priority: 100,
335
- toolPattern: "*shell*",
336
- permission: "EXECUTE",
337
- minimumTrustLevel: "UNTRUSTED",
338
- enabled: true,
339
- createdAt: "",
340
- updatedAt: ""
341
- },
342
- {
343
- id: "deny-exec",
344
- description: "Block command execution",
345
- effect: "DENY",
346
- priority: 101,
347
- toolPattern: "*exec*",
348
- permission: "EXECUTE",
349
- minimumTrustLevel: "UNTRUSTED",
350
- enabled: true,
351
- createdAt: "",
352
- updatedAt: ""
353
- },
354
- {
355
- id: "deny-eval",
356
- description: "Block code eval",
357
- effect: "DENY",
358
- priority: 102,
359
- toolPattern: "*eval*",
360
- permission: "EXECUTE",
361
- minimumTrustLevel: "UNTRUSTED",
362
- enabled: true,
363
- createdAt: "",
364
- updatedAt: ""
365
- },
366
- {
367
- id: "allow-rest",
368
- description: "Allow all other tools",
369
- effect: "ALLOW",
370
- priority: 1e3,
371
- toolPattern: "*",
372
- permission: "EXECUTE",
373
- minimumTrustLevel: "UNTRUSTED",
374
- enabled: true,
375
- createdAt: "",
376
- updatedAt: ""
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);
377
369
  }
378
- ],
379
- createdAt: "",
380
- updatedAt: ""
381
- },
382
- "read-only": {
383
- id: "read-only",
384
- name: "Read Only",
385
- description: "Only allows read operations, blocks writes and execution",
386
- version: 1,
387
- rules: [
388
- {
389
- id: "allow-read",
390
- description: "Allow read tools",
391
- effect: "ALLOW",
392
- priority: 100,
393
- toolPattern: "*read*",
394
- permission: "EXECUTE",
395
- minimumTrustLevel: "UNTRUSTED",
396
- enabled: true,
397
- createdAt: "",
398
- updatedAt: ""
399
- },
400
- {
401
- id: "allow-list",
402
- description: "Allow list tools",
403
- effect: "ALLOW",
404
- priority: 101,
405
- toolPattern: "*list*",
406
- permission: "EXECUTE",
407
- minimumTrustLevel: "UNTRUSTED",
408
- enabled: true,
409
- createdAt: "",
410
- updatedAt: ""
411
- },
412
- {
413
- id: "allow-get",
414
- description: "Allow get tools",
415
- effect: "ALLOW",
416
- priority: 102,
417
- toolPattern: "*get*",
418
- permission: "EXECUTE",
419
- minimumTrustLevel: "UNTRUSTED",
420
- enabled: true,
421
- createdAt: "",
422
- updatedAt: ""
423
- },
424
- {
425
- id: "allow-search",
426
- description: "Allow search tools",
427
- effect: "ALLOW",
428
- priority: 103,
429
- toolPattern: "*search*",
430
- permission: "EXECUTE",
431
- minimumTrustLevel: "UNTRUSTED",
432
- enabled: true,
433
- createdAt: "",
434
- updatedAt: ""
435
- },
436
- {
437
- id: "allow-query",
438
- description: "Allow query tools",
439
- effect: "ALLOW",
440
- priority: 104,
441
- toolPattern: "*query*",
442
- permission: "EXECUTE",
443
- minimumTrustLevel: "UNTRUSTED",
444
- enabled: true,
445
- createdAt: "",
446
- updatedAt: ""
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 {
447
394
  }
448
- ],
449
- createdAt: "",
450
- updatedAt: ""
451
- },
452
- permissive: {
453
- id: "permissive",
454
- name: "Permissive",
455
- description: "Allows all tool calls (monitoring only)",
456
- version: 1,
457
- rules: [
458
- {
459
- id: "allow-all",
460
- description: "Allow all",
461
- effect: "ALLOW",
462
- priority: 1e3,
463
- toolPattern: "*",
464
- permission: "EXECUTE",
465
- minimumTrustLevel: "UNTRUSTED",
466
- enabled: true,
467
- createdAt: "",
468
- updatedAt: ""
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");
469
464
  }
470
- ],
471
- createdAt: "",
472
- updatedAt: ""
473
- },
474
- "deny-all": {
475
- id: "deny-all",
476
- name: "Deny All",
477
- description: "Blocks all tool calls",
478
- version: 1,
479
- rules: [],
480
- createdAt: "",
481
- updatedAt: ""
465
+ importReplaced = true;
466
+ break;
467
+ }
482
468
  }
483
- };
484
- function loadPolicy(source) {
485
- if (typeof source === "object") return source;
486
- if (PRESETS[source]) return PRESETS[source];
487
- const filePath = resolve(source);
488
- if (existsSync(filePath)) {
489
- const content = readFileSync(filePath, "utf-8");
490
- return JSON.parse(content);
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");
491
474
  }
492
- throw new Error(
493
- `Unknown policy "${source}". Use a preset (${Object.keys(PRESETS).join(", ")}), a JSON file path, or a PolicySet object.`
494
- );
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 };
495
482
  }
496
- function parseArgs(argv) {
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) {
497
641
  const args = argv.slice(2);
498
- let policySource = "restricted";
499
- let name = "solongate-proxy";
500
- let verbose = false;
501
- let validateInput = true;
502
- let rateLimitPerTool;
503
- let globalRateLimit;
504
- let configFile;
505
- let apiKey;
506
- let apiUrl;
507
- let separatorIndex = args.indexOf("--");
508
- const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
509
- const upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
510
- for (let i = 0; i < flags.length; i++) {
511
- switch (flags[i]) {
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]) {
512
649
  case "--policy":
513
- policySource = flags[++i];
514
- break;
515
- case "--name":
516
- name = flags[++i];
650
+ opts.policy = args[++i];
517
651
  break;
518
- case "--verbose":
519
- verbose = true;
520
- break;
521
- case "--no-input-guard":
522
- validateInput = false;
523
- break;
524
- case "--rate-limit":
525
- rateLimitPerTool = parseInt(flags[++i], 10);
526
- break;
527
- case "--global-rate-limit":
528
- globalRateLimit = parseInt(flags[++i], 10);
529
- break;
530
- case "--config":
531
- configFile = flags[++i];
532
- break;
533
- case "--api-key":
534
- apiKey = flags[++i];
652
+ case "--no-install":
653
+ opts.noInstall = true;
535
654
  break;
536
- case "--api-url":
537
- apiUrl = flags[++i];
655
+ case "--help":
656
+ case "-h":
657
+ printHelp3();
658
+ process.exit(0);
538
659
  break;
660
+ default:
661
+ if (!args[i].startsWith("-") && !opts.name) {
662
+ opts.name = args[i];
663
+ }
539
664
  }
540
665
  }
541
- if (configFile) {
542
- const filePath = resolve(configFile);
543
- const content = readFileSync(filePath, "utf-8");
544
- const fileConfig = JSON.parse(content);
545
- if (!fileConfig.upstream) {
546
- throw new Error('Config file must include "upstream" with at least "command"');
547
- }
548
- return {
549
- upstream: fileConfig.upstream,
550
- policy: loadPolicy(fileConfig.policy ?? policySource),
551
- name: fileConfig.name ?? name,
552
- verbose: fileConfig.verbose ?? verbose,
553
- validateInput: fileConfig.validateInput ?? validateInput,
554
- rateLimitPerTool: fileConfig.rateLimitPerTool ?? rateLimitPerTool,
555
- globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit,
556
- apiKey: apiKey ?? fileConfig.apiKey,
557
- apiUrl: apiUrl ?? fileConfig.apiUrl
558
- };
559
- }
560
- if (upstreamArgs.length === 0) {
561
- throw new Error(
562
- "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"
563
- );
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);
564
676
  }
565
- const [command, ...commandArgs] = upstreamArgs;
566
- return {
567
- upstream: {
568
- command,
569
- args: commandArgs,
570
- env: { ...process.env }
571
- },
572
- policy: loadPolicy(policySource),
573
- name,
574
- verbose,
575
- validateInput,
576
- rateLimitPerTool,
577
- globalRateLimit,
578
- apiKey,
579
- apiUrl
580
- };
677
+ return opts;
581
678
  }
679
+ function printHelp3() {
680
+ log3(`
681
+ SolonGate Create \u2014 Scaffold a secure MCP server in seconds
582
682
 
583
- // src/proxy.ts
584
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
585
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
586
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
587
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
588
- import {
589
- ListToolsRequestSchema,
590
- CallToolRequestSchema,
591
- ListResourcesRequestSchema,
592
- ListPromptsRequestSchema,
593
- GetPromptRequestSchema,
594
- ReadResourceRequestSchema,
595
- ListResourceTemplatesRequestSchema
596
- } from "@modelcontextprotocol/sdk/types.js";
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
+ });
597
883
 
598
884
  // ../core/dist/index.js
599
885
  import { z } from "zod";
@@ -746,7 +1032,9 @@ var DEFAULT_INPUT_GUARD_CONFIG = Object.freeze({
746
1032
  shellInjection: true,
747
1033
  wildcardAbuse: true,
748
1034
  lengthLimit: 4096,
749
- entropyLimit: true
1035
+ entropyLimit: true,
1036
+ ssrf: true,
1037
+ sqlInjection: true
750
1038
  });
751
1039
  var PATH_TRAVERSAL_PATTERNS = [
752
1040
  /\.\.\//,
@@ -772,7 +1060,23 @@ var SENSITIVE_PATHS = [
772
1060
  /c:\\windows\\system32/i,
773
1061
  /c:\\windows\\syswow64/i,
774
1062
  /\/root\//i,
775
- /~\//
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
776
1080
  ];
777
1081
  function detectPathTraversal(value) {
778
1082
  for (const pattern of PATH_TRAVERSAL_PATTERNS) {
@@ -802,8 +1106,18 @@ var SHELL_INJECTION_PATTERNS = [
802
1106
  // eval command
803
1107
  /\bexec\b/i,
804
1108
  // exec command
805
- /\bsystem\b/i
1109
+ /\bsystem\b/i,
806
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)
807
1121
  ];
808
1122
  function detectShellInjection(value) {
809
1123
  for (const pattern of SHELL_INJECTION_PATTERNS) {
@@ -818,6 +1132,91 @@ function detectWildcardAbuse(value) {
818
1132
  if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
819
1133
  return false;
820
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
+ }
821
1220
  function checkLengthLimits(value, maxLength = 4096) {
822
1221
  return value.length <= maxLength;
823
1222
  }
@@ -841,79 +1240,470 @@ function calculateShannonEntropy(str) {
841
1240
  entropy -= p * Math.log2(p);
842
1241
  }
843
1242
  }
844
- return entropy;
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);
1536
+ }
1537
+ throw new Error(
1538
+ `Unknown policy "${source}". Use a preset (${Object.keys(PRESETS).join(", ")}), a JSON file path, or a PolicySet object.`
1539
+ );
845
1540
  }
846
- function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG) {
847
- const threats = [];
848
- if (typeof value !== "string") {
849
- if (typeof value === "object" && value !== null) {
850
- return sanitizeObject(field, value, config);
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;
851
1596
  }
852
- return { safe: true, threats: [] };
853
- }
854
- if (config.pathTraversal && detectPathTraversal(value)) {
855
- threats.push({
856
- type: "PATH_TRAVERSAL",
857
- field,
858
- value: truncate(value, 100),
859
- description: "Path traversal pattern detected"
860
- });
861
- }
862
- if (config.shellInjection && detectShellInjection(value)) {
863
- threats.push({
864
- type: "SHELL_INJECTION",
865
- field,
866
- value: truncate(value, 100),
867
- description: "Shell injection pattern detected"
868
- });
869
- }
870
- if (config.wildcardAbuse && detectWildcardAbuse(value)) {
871
- threats.push({
872
- type: "WILDCARD_ABUSE",
873
- field,
874
- value: truncate(value, 100),
875
- description: "Wildcard abuse pattern detected"
876
- });
877
1597
  }
878
- if (!checkLengthLimits(value, config.lengthLimit)) {
879
- threats.push({
880
- type: "LENGTH_EXCEEDED",
881
- field,
882
- value: `[${value.length} chars]`,
883
- description: `Value exceeds maximum length of ${config.lengthLimit}`
884
- });
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
+ }
885
1607
  }
886
- if (config.entropyLimit && !checkEntropyLimits(value)) {
887
- threats.push({
888
- type: "HIGH_ENTROPY",
889
- field,
890
- value: truncate(value, 100),
891
- description: "High entropy string detected - possible encoded payload"
892
- });
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
+ );
893
1612
  }
894
- return { safe: threats.length === 0, threats };
895
- }
896
- function sanitizeObject(basePath, obj, config) {
897
- const threats = [];
898
- if (Array.isArray(obj)) {
899
- for (let i = 0; i < obj.length; i++) {
900
- const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
901
- 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"');
902
1619
  }
903
- } else {
904
- for (const [key, val] of Object.entries(obj)) {
905
- const result = sanitizeInput(`${basePath}.${key}`, val, config);
906
- threats.push(...result.threats);
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
+ );
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
+ };
1637
+ }
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
+ );
907
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
+ };
908
1663
  }
909
- return { safe: threats.length === 0, threats };
910
- }
911
- function truncate(str, maxLen) {
912
- return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
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
+ };
913
1687
  }
914
- var DEFAULT_TOKEN_TTL_SECONDS = 30;
915
- var TOKEN_ALGORITHM = "HS256";
916
- var MIN_SECRET_LENGTH = 32;
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";
917
1707
 
918
1708
  // ../policy-engine/dist/index.js
919
1709
  import { createHash } from "crypto";
@@ -1064,8 +1854,51 @@ function trustLevelMeetsMinimum(actual, minimum) {
1064
1854
  function argumentConstraintsMatch(constraints, args) {
1065
1855
  for (const [key, constraint] of Object.entries(constraints)) {
1066
1856
  if (!(key in args)) return false;
1067
- if (typeof constraint === "string" && typeof args[key] === "string") {
1068
- if (constraint !== "*" && args[key] !== constraint) return false;
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;
1069
1902
  }
1070
1903
  }
1071
1904
  return true;
@@ -1098,7 +1931,15 @@ function evaluatePolicy(policySet, request) {
1098
1931
  matchedRule: null,
1099
1932
  reason: "No matching policy rule found. Default action: DENY.",
1100
1933
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1101
- 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
+ }
1102
1943
  };
1103
1944
  }
1104
1945
  function validatePolicyRule(input) {
@@ -1420,6 +2261,7 @@ var PolicyStore = class {
1420
2261
 
1421
2262
  // ../sdk-ts/dist/index.js
1422
2263
  import { randomUUID, createHmac } from "crypto";
2264
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1423
2265
  var DEFAULT_CONFIG = Object.freeze({
1424
2266
  validateSchemas: true,
1425
2267
  enableLogging: true,
@@ -1514,7 +2356,7 @@ async function interceptToolCall(params, upstreamCall, options) {
1514
2356
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1515
2357
  };
1516
2358
  options.onDecision?.(result);
1517
- const reason = options.verboseErrors ? `Input validation failed: ${threatDescriptions.join("; ")}` : "Input validation failed.";
2359
+ const reason = options.verboseErrors ? `Input validation failed: ${sanitization.threats.length} threat(s) detected` : "Input validation failed.";
1518
2360
  return createDeniedToolResult(reason);
1519
2361
  }
1520
2362
  }
@@ -1832,6 +2674,25 @@ var RateLimiter = class {
1832
2674
  const resetAt = this.globalRecords.length > 0 ? this.globalRecords[0].timestamp + this.windowMs : now + this.windowMs;
1833
2675
  return { allowed, remaining, resetAt };
1834
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
+ }
1835
2696
  /**
1836
2697
  * Records a tool call for rate limiting.
1837
2698
  * Call this after successful execution.
@@ -1887,6 +2748,16 @@ var RateLimiter = class {
1887
2748
  return active;
1888
2749
  }
1889
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
+ };
1890
2761
  var SolonGate = class {
1891
2762
  policyEngine;
1892
2763
  config;
@@ -1895,7 +2766,19 @@ var SolonGate = class {
1895
2766
  tokenIssuer;
1896
2767
  serverVerifier;
1897
2768
  rateLimiter;
2769
+ apiKey;
2770
+ licenseValidated = false;
1898
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;
1899
2782
  const { config, warnings } = resolveConfig(options.config);
1900
2783
  this.config = config;
1901
2784
  this.configWarnings = warnings;
@@ -1921,12 +2804,47 @@ var SolonGate = class {
1921
2804
  this.serverVerifier = config.gatewaySecret ? new ServerVerifier({ gatewaySecret: config.gatewaySecret }) : null;
1922
2805
  this.rateLimiter = new RateLimiter();
1923
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
+ }
1924
2841
  /**
1925
2842
  * Intercept and evaluate a tool call against the full security pipeline.
1926
2843
  * If denied at any stage, returns an error result without calling upstream.
1927
2844
  * If allowed, calls upstream and returns the result.
1928
2845
  */
1929
2846
  async executeToolCall(params, upstreamCall) {
2847
+ await this.validateLicense();
1930
2848
  return interceptToolCall(params, upstreamCall, {
1931
2849
  policyEngine: this.policyEngine,
1932
2850
  validateSchemas: this.config.validateSchemas,
@@ -1976,8 +2894,8 @@ var Mutex = class {
1976
2894
  this.locked = true;
1977
2895
  return;
1978
2896
  }
1979
- return new Promise((resolve3) => {
1980
- this.queue.push(resolve3);
2897
+ return new Promise((resolve5) => {
2898
+ this.queue.push(resolve5);
1981
2899
  });
1982
2900
  }
1983
2901
  release() {
@@ -2018,9 +2936,31 @@ var SolonGateProxy = class {
2018
2936
  */
2019
2937
  async start() {
2020
2938
  log("Starting SolonGate Proxy...");
2939
+ const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
2021
2940
  if (this.config.apiKey) {
2022
- const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
2023
- log(`Cloud API: ${apiUrl}`);
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
+ }
2024
2964
  try {
2025
2965
  const cloudPolicy = await fetchCloudPolicy(this.config.apiKey, apiUrl);
2026
2966
  this.config.policy = cloudPolicy;
@@ -2030,29 +2970,54 @@ var SolonGateProxy = class {
2030
2970
  }
2031
2971
  }
2032
2972
  log(`Policy: ${this.config.policy.name} (${this.config.policy.rules.length} rules)`);
2033
- log(`Upstream: ${this.config.upstream.command} ${(this.config.upstream.args ?? []).join(" ")}`);
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
+ }
2034
2979
  await this.connectUpstream();
2035
2980
  await this.discoverTools();
2036
2981
  this.createServer();
2037
2982
  await this.serve();
2038
2983
  }
2039
2984
  /**
2040
- * Connect to the upstream MCP server by spawning it as a child process.
2985
+ * Connect to the upstream MCP server.
2986
+ * Supports stdio (child process), SSE, and StreamableHTTP transports.
2041
2987
  */
2042
2988
  async connectUpstream() {
2043
2989
  this.client = new Client(
2044
2990
  { name: "solongate-proxy-client", version: "0.1.0" },
2045
2991
  { capabilities: {} }
2046
2992
  );
2047
- const transport = new StdioClientTransport({
2048
- command: this.config.upstream.command,
2049
- args: this.config.upstream.args,
2050
- env: this.config.upstream.env,
2051
- cwd: this.config.upstream.cwd,
2052
- stderr: "pipe"
2053
- });
2054
- await this.client.connect(transport);
2055
- log("Connected to upstream server");
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})`);
2056
3021
  }
2057
3022
  /**
2058
3023
  * Discover tools from the upstream server.
@@ -2091,8 +3056,17 @@ var SolonGateProxy = class {
2091
3056
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
2092
3057
  return { tools: this.upstreamTools };
2093
3058
  });
3059
+ const MAX_ARGUMENT_SIZE = 1024 * 1024;
2094
3060
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
2095
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
+ }
2096
3070
  log(`Tool call: ${name}`);
2097
3071
  await this.callMutex.acquire();
2098
3072
  const startTime = Date.now();
@@ -2166,14 +3140,38 @@ var SolonGateProxy = class {
2166
3140
  });
2167
3141
  }
2168
3142
  /**
2169
- * Start serving on stdio (downstream to Claude).
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).
2170
3146
  */
2171
3147
  async serve() {
2172
3148
  if (!this.server) throw new Error("Server not created");
2173
- const transport = new StdioServerTransport();
2174
- await this.server.connect(transport);
2175
- log("Proxy is live. All tool calls are now protected by SolonGate.");
2176
- log("Waiting for requests...");
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
+ }
2177
3175
  }
2178
3176
  };
2179
3177
 
@@ -2190,13 +3188,23 @@ console.error = (...args) => {
2190
3188
  process.stderr.write(`[SolonGate ERROR] ${args.map(String).join(" ")}
2191
3189
  `);
2192
3190
  };
2193
- async function main2() {
3191
+ async function main4() {
2194
3192
  const subcommand = process.argv[2];
2195
3193
  if (subcommand === "init") {
2196
3194
  process.argv.splice(2, 1);
2197
3195
  await Promise.resolve().then(() => (init_init(), init_exports));
2198
3196
  return;
2199
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
+ }
2200
3208
  try {
2201
3209
  const config = parseArgs(process.argv);
2202
3210
  const proxy = new SolonGateProxy(config);
@@ -2208,4 +3216,4 @@ async function main2() {
2208
3216
  process.exit(1);
2209
3217
  }
2210
3218
  }
2211
- main2();
3219
+ main4();