mcp-squared 0.2.0 → 0.3.1

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
@@ -1,18 +1,133 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
4
8
  var __export = (target, all) => {
5
9
  for (var name in all)
6
10
  __defProp(target, name, {
7
11
  get: all[name],
8
12
  enumerable: true,
9
13
  configurable: true,
10
- set: (newValue) => all[name] = () => newValue
14
+ set: __exportSetter.bind(all, name)
11
15
  });
12
16
  };
13
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
18
  var __require = import.meta.require;
15
19
 
20
+ // src/config/paths.ts
21
+ import { existsSync, mkdirSync } from "fs";
22
+ import { homedir, platform } from "os";
23
+ import { dirname, join, resolve } from "path";
24
+ function getEnv(key) {
25
+ return Bun.env[key];
26
+ }
27
+ function getXdgConfigHome() {
28
+ return getEnv("XDG_CONFIG_HOME") || join(homedir(), ".config");
29
+ }
30
+ function getUserConfigDir() {
31
+ const os = platform();
32
+ if (os === "win32") {
33
+ return join(getEnv("APPDATA") || join(homedir(), "AppData", "Roaming"), APP_NAME);
34
+ }
35
+ return join(getXdgConfigHome(), APP_NAME);
36
+ }
37
+ function ensureDir(dir) {
38
+ if (!existsSync(dir)) {
39
+ mkdirSync(dir, { recursive: true });
40
+ }
41
+ }
42
+ function getUserConfigPath() {
43
+ return join(getUserConfigDir(), "config.toml");
44
+ }
45
+ function getSocketFilePath(instanceId) {
46
+ if (!instanceId) {
47
+ return join(getUserConfigDir(), SOCKET_FILENAME);
48
+ }
49
+ return join(getSocketDir(), `mcp-squared.${instanceId}.sock`);
50
+ }
51
+ function getDaemonDir(configHash) {
52
+ if (configHash) {
53
+ return join(getUserConfigDir(), DAEMON_DIR_NAME, configHash);
54
+ }
55
+ return join(getUserConfigDir(), DAEMON_DIR_NAME);
56
+ }
57
+ function getDaemonRegistryPath(configHash) {
58
+ return join(getDaemonDir(configHash), DAEMON_REGISTRY_FILENAME);
59
+ }
60
+ function getDaemonSocketPath(configHash) {
61
+ return join(getDaemonDir(configHash), DAEMON_SOCKET_FILENAME);
62
+ }
63
+ function getInstanceRegistryDir() {
64
+ return join(getUserConfigDir(), INSTANCE_DIR_NAME);
65
+ }
66
+ function getSocketDir() {
67
+ return join(getUserConfigDir(), SOCKET_DIR_NAME);
68
+ }
69
+ function ensureInstanceRegistryDir() {
70
+ ensureDir(getInstanceRegistryDir());
71
+ }
72
+ function ensureSocketDir() {
73
+ ensureDir(getSocketDir());
74
+ }
75
+ function ensureDaemonDir(configHash) {
76
+ const daemonDir = getDaemonDir(configHash);
77
+ if (!existsSync(daemonDir)) {
78
+ mkdirSync(daemonDir, { recursive: true, mode: 448 });
79
+ }
80
+ }
81
+ function findProjectConfig(startDir) {
82
+ let currentDir = resolve(startDir);
83
+ const root = dirname(currentDir);
84
+ while (currentDir !== root) {
85
+ const directPath = join(currentDir, CONFIG_FILENAME);
86
+ if (existsSync(directPath)) {
87
+ return directPath;
88
+ }
89
+ const hiddenDirPath = join(currentDir, CONFIG_DIR_NAME, "config.toml");
90
+ if (existsSync(hiddenDirPath)) {
91
+ return hiddenDirPath;
92
+ }
93
+ const parentDir = dirname(currentDir);
94
+ if (parentDir === currentDir)
95
+ break;
96
+ currentDir = parentDir;
97
+ }
98
+ return null;
99
+ }
100
+ function discoverConfigPath(cwd = process.cwd()) {
101
+ const envPath = getEnv("MCP_SQUARED_CONFIG");
102
+ if (envPath) {
103
+ const resolvedEnvPath = resolve(envPath);
104
+ if (existsSync(resolvedEnvPath)) {
105
+ return { path: resolvedEnvPath, source: "env" };
106
+ }
107
+ }
108
+ const projectPath = findProjectConfig(cwd);
109
+ if (projectPath) {
110
+ return { path: projectPath, source: "project" };
111
+ }
112
+ const userPath = getUserConfigPath();
113
+ if (existsSync(userPath)) {
114
+ return { path: userPath, source: "user" };
115
+ }
116
+ return null;
117
+ }
118
+ function getDefaultConfigPath() {
119
+ return {
120
+ path: getUserConfigPath(),
121
+ source: "user"
122
+ };
123
+ }
124
+ function ensureConfigDir(configPath) {
125
+ const dir = dirname(configPath);
126
+ ensureDir(dir);
127
+ }
128
+ var SOCKET_FILENAME = "mcp-squared.sock", INSTANCE_DIR_NAME = "instances", SOCKET_DIR_NAME = "sockets", DAEMON_DIR_NAME = "daemon", DAEMON_REGISTRY_FILENAME = "daemon.json", DAEMON_SOCKET_FILENAME = "daemon.sock", CONFIG_FILENAME = "mcp-squared.toml", CONFIG_DIR_NAME = ".mcp-squared", APP_NAME = "mcp-squared";
129
+ var init_paths = () => {};
130
+
16
131
  // src/embeddings/generator.ts
17
132
  import {
18
133
  env,
@@ -206,6 +321,84 @@ async function runConfigTui() {
206
321
  return _run();
207
322
  }
208
323
 
324
+ // src/init/runner.ts
325
+ var exports_runner = {};
326
+ __export(exports_runner, {
327
+ runInit: () => runInit,
328
+ generateConfigToml: () => generateConfigToml
329
+ });
330
+ import { existsSync as existsSync11 } from "fs";
331
+ import { join as join7 } from "path";
332
+ function generateConfigToml(profile) {
333
+ const securityBlock = profile === "permissive" ? `[security.tools]
334
+ # Permissive: all tools are allowed without confirmation.
335
+ # Change to hardened defaults with: allow = [], confirm = ["*:*"]
336
+ allow = ["*:*"]
337
+ block = []
338
+ confirm = []` : `[security.tools]
339
+ # Hardened: all tools require confirmation before execution.
340
+ # To allow specific tools without confirmation, add patterns to 'allow':
341
+ # allow = ["github:*", "fs:read_file"]
342
+ # To block tools entirely:
343
+ # block = ["dangerous:*"]
344
+ # To revert to permissive mode: allow = ["*:*"], confirm = []
345
+ allow = []
346
+ block = []
347
+ confirm = ["*:*"]`;
348
+ return `# MCP\xB2 Configuration
349
+ # https://github.com/aditzel/mcp-squared
350
+ schemaVersion = 1
351
+
352
+ # Upstream MCP servers to aggregate.
353
+ # Add servers here or use 'mcp-squared import' to import from other tools.
354
+ #
355
+ # [upstreams.example]
356
+ # transport = "stdio"
357
+ # [upstreams.example.stdio]
358
+ # command = "npx"
359
+ # args = ["-y", "@modelcontextprotocol/server-example"]
360
+
361
+ ${securityBlock}
362
+
363
+ [operations.findTools]
364
+ defaultLimit = 5
365
+ maxLimit = 50
366
+ defaultMode = "fast"
367
+ defaultDetailLevel = "L1"
368
+
369
+ [operations.embeddings]
370
+ # Enable to use semantic or hybrid search modes.
371
+ # Requires onnxruntime shared library on the system.
372
+ enabled = false
373
+
374
+ [operations.logging]
375
+ level = "info"
376
+ `;
377
+ }
378
+ async function runInit(args) {
379
+ const targetPath = args.project ? join7(process.cwd(), PROJECT_CONFIG_FILENAME) : getDefaultConfigPath().path;
380
+ if (existsSync11(targetPath) && !args.force) {
381
+ console.error(`Config file already exists: ${targetPath}`);
382
+ console.error("Use --force to overwrite.");
383
+ process.exit(1);
384
+ }
385
+ ensureConfigDir(targetPath);
386
+ const content = generateConfigToml(args.security);
387
+ await Bun.write(targetPath, content);
388
+ const profileLabel = args.security === "permissive" ? "permissive (allow-all)" : "hardened (confirm-all)";
389
+ console.log(`Created ${targetPath}`);
390
+ console.log(`Security profile: ${profileLabel}`);
391
+ console.log("");
392
+ console.log("Next steps:");
393
+ console.log(" 1. Add upstream servers to [upstreams] or run: mcp-squared import");
394
+ console.log(" 2. Test connections: mcp-squared test");
395
+ console.log(" 3. Install into MCP clients: mcp-squared install");
396
+ }
397
+ var PROJECT_CONFIG_FILENAME = "mcp-squared.toml";
398
+ var init_runner = __esm(() => {
399
+ init_paths();
400
+ });
401
+
209
402
  // src/index.ts
210
403
  import { spawnSync } from "child_process";
211
404
  import { randomUUID as randomUUID2 } from "crypto";
@@ -214,6 +407,9 @@ import { Client as Client4 } from "@modelcontextprotocol/sdk/client/index.js";
214
407
  import { StreamableHTTPClientTransport as StreamableHTTPClientTransport4 } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
215
408
 
216
409
  // src/cli/index.ts
410
+ function isValidSecurityProfile(value) {
411
+ return value === "hardened" || value === "permissive";
412
+ }
217
413
  function isValidInstallMode(value) {
218
414
  return value === "replace" || value === "add";
219
415
  }
@@ -266,6 +462,11 @@ function parseArgs(args) {
266
462
  verbose: false
267
463
  },
268
464
  authTarget: undefined,
465
+ init: {
466
+ security: "hardened",
467
+ project: false,
468
+ force: false
469
+ },
269
470
  install: {
270
471
  interactive: true,
271
472
  dryRun: false,
@@ -319,6 +520,9 @@ function parseArgs(args) {
319
520
  case "install":
320
521
  result.mode = "install";
321
522
  break;
523
+ case "init":
524
+ result.mode = "init";
525
+ break;
322
526
  case "monitor":
323
527
  result.mode = "monitor";
324
528
  break;
@@ -442,9 +646,30 @@ function parseArgs(args) {
442
646
  }
443
647
  break;
444
648
  }
649
+ case "--daemon-secret": {
650
+ const value = argValue ?? args[++i];
651
+ if (value) {
652
+ result.daemon.sharedSecret = value;
653
+ result.proxy.sharedSecret = value;
654
+ }
655
+ break;
656
+ }
445
657
  case "--no-daemon-spawn":
446
658
  result.proxy.noSpawn = true;
447
659
  break;
660
+ case "--security": {
661
+ const value = argValue ?? args[++i];
662
+ if (value && isValidSecurityProfile(value)) {
663
+ result.init.security = value;
664
+ }
665
+ break;
666
+ }
667
+ case "--project":
668
+ result.init.project = true;
669
+ break;
670
+ case "--force":
671
+ result.init.force = true;
672
+ break;
448
673
  case "--no-auto-refresh":
449
674
  result.monitor.noAutoRefresh = true;
450
675
  break;
@@ -471,6 +696,7 @@ Usage:
471
696
  mcp-squared test [upstream] Test connection to upstream server(s)
472
697
  mcp-squared auth <upstream> Authenticate with an OAuth-protected upstream
473
698
  mcp-squared import [options] Import MCP configs from other tools
699
+ mcp-squared init [options] Generate a starter config file with security profile
474
700
  mcp-squared install [options] Install MCP\xB2 into other MCP clients
475
701
  mcp-squared monitor [options] Launch server monitor TUI
476
702
  mcp-squared daemon [options] Start shared MCP\xB2 daemon
@@ -483,6 +709,7 @@ Commands:
483
709
  test [name], --test, -t Test upstream connection (all if no name given)
484
710
  auth <name> Authenticate with an OAuth-protected upstream
485
711
  import Import MCP server configs from other tools
712
+ init Generate a starter config with security profile
486
713
  install Install MCP\xB2 as a server in other MCP clients
487
714
  monitor Launch server monitor TUI
488
715
  daemon Start shared daemon for multiple clients
@@ -504,6 +731,11 @@ Import Options:
504
731
  --no-interactive Disable interactive prompts (use --strategy)
505
732
  --verbose Show detailed output
506
733
 
734
+ Init Options:
735
+ --security=<profile> Security profile: hardened (default) or permissive
736
+ --project Write to project-local mcp-squared.toml (default: user-level)
737
+ --force Overwrite existing config without prompting
738
+
507
739
  Install Options:
508
740
  --tool=<tool> Target tool (skip selection prompt)
509
741
  --scope=<scope> Scope: user or project
@@ -523,10 +755,12 @@ Monitor Options:
523
755
 
524
756
  Daemon Options:
525
757
  --daemon-socket=<path> Override daemon socket path
758
+ --daemon-secret=<secret> Require shared secret for daemon IPC clients
526
759
 
527
760
  Proxy Options:
528
761
  --daemon-socket=<path> Connect to a specific daemon socket
529
762
  --no-daemon-spawn Do not auto-spawn daemon if missing
763
+ --daemon-secret=<secret> Provide shared secret for daemon handshake
530
764
 
531
765
  Supported Tools:
532
766
  ${VALID_TOOL_IDS.join(", ")}
@@ -535,6 +769,9 @@ Examples:
535
769
  mcp-squared test github Test connection to 'github' upstream
536
770
  mcp-squared test Test all configured upstreams
537
771
  mcp-squared auth vercel-mcp Authenticate with 'vercel-mcp' upstream (OAuth)
772
+ mcp-squared init Generate hardened config (confirm-all by default)
773
+ mcp-squared init --security=permissive Generate permissive config (allow-all)
774
+ mcp-squared init --project Generate project-local config
538
775
  mcp-squared import --list List all discovered MCP configs
539
776
  mcp-squared import --dry-run Preview import changes
540
777
  mcp-squared import Import with interactive conflict resolution
@@ -553,6 +790,7 @@ Examples:
553
790
  }
554
791
 
555
792
  // src/config/instance-registry.ts
793
+ init_paths();
556
794
  import {
557
795
  existsSync as existsSync2,
558
796
  mkdirSync as mkdirSync2,
@@ -565,121 +803,6 @@ import {
565
803
  import { connect } from "net";
566
804
  import { join as join2 } from "path";
567
805
 
568
- // src/config/paths.ts
569
- import { existsSync, mkdirSync } from "fs";
570
- import { homedir, platform } from "os";
571
- import { dirname, join, resolve } from "path";
572
- var SOCKET_FILENAME = "mcp-squared.sock";
573
- var INSTANCE_DIR_NAME = "instances";
574
- var SOCKET_DIR_NAME = "sockets";
575
- var DAEMON_DIR_NAME = "daemon";
576
- var DAEMON_REGISTRY_FILENAME = "daemon.json";
577
- var DAEMON_SOCKET_FILENAME = "daemon.sock";
578
- var CONFIG_FILENAME = "mcp-squared.toml";
579
- var CONFIG_DIR_NAME = ".mcp-squared";
580
- var APP_NAME = "mcp-squared";
581
- function getEnv(key) {
582
- return Bun.env[key];
583
- }
584
- function getXdgConfigHome() {
585
- return getEnv("XDG_CONFIG_HOME") || join(homedir(), ".config");
586
- }
587
- function getUserConfigDir() {
588
- const os = platform();
589
- if (os === "win32") {
590
- return join(getEnv("APPDATA") || join(homedir(), "AppData", "Roaming"), APP_NAME);
591
- }
592
- return join(getXdgConfigHome(), APP_NAME);
593
- }
594
- function ensureDir(dir) {
595
- if (!existsSync(dir)) {
596
- mkdirSync(dir, { recursive: true });
597
- }
598
- }
599
- function getUserConfigPath() {
600
- return join(getUserConfigDir(), "config.toml");
601
- }
602
- function getSocketFilePath(instanceId) {
603
- if (!instanceId) {
604
- return join(getUserConfigDir(), SOCKET_FILENAME);
605
- }
606
- return join(getSocketDir(), `mcp-squared.${instanceId}.sock`);
607
- }
608
- function getDaemonDir(configHash) {
609
- if (configHash) {
610
- return join(getUserConfigDir(), DAEMON_DIR_NAME, configHash);
611
- }
612
- return join(getUserConfigDir(), DAEMON_DIR_NAME);
613
- }
614
- function getDaemonRegistryPath(configHash) {
615
- return join(getDaemonDir(configHash), DAEMON_REGISTRY_FILENAME);
616
- }
617
- function getDaemonSocketPath(configHash) {
618
- return join(getDaemonDir(configHash), DAEMON_SOCKET_FILENAME);
619
- }
620
- function getInstanceRegistryDir() {
621
- return join(getUserConfigDir(), INSTANCE_DIR_NAME);
622
- }
623
- function getSocketDir() {
624
- return join(getUserConfigDir(), SOCKET_DIR_NAME);
625
- }
626
- function ensureInstanceRegistryDir() {
627
- ensureDir(getInstanceRegistryDir());
628
- }
629
- function ensureSocketDir() {
630
- ensureDir(getSocketDir());
631
- }
632
- function ensureDaemonDir(configHash) {
633
- ensureDir(getDaemonDir(configHash));
634
- }
635
- function findProjectConfig(startDir) {
636
- let currentDir = resolve(startDir);
637
- const root = dirname(currentDir);
638
- while (currentDir !== root) {
639
- const directPath = join(currentDir, CONFIG_FILENAME);
640
- if (existsSync(directPath)) {
641
- return directPath;
642
- }
643
- const hiddenDirPath = join(currentDir, CONFIG_DIR_NAME, "config.toml");
644
- if (existsSync(hiddenDirPath)) {
645
- return hiddenDirPath;
646
- }
647
- const parentDir = dirname(currentDir);
648
- if (parentDir === currentDir)
649
- break;
650
- currentDir = parentDir;
651
- }
652
- return null;
653
- }
654
- function discoverConfigPath(cwd = process.cwd()) {
655
- const envPath = getEnv("MCP_SQUARED_CONFIG");
656
- if (envPath) {
657
- const resolvedEnvPath = resolve(envPath);
658
- if (existsSync(resolvedEnvPath)) {
659
- return { path: resolvedEnvPath, source: "env" };
660
- }
661
- }
662
- const projectPath = findProjectConfig(cwd);
663
- if (projectPath) {
664
- return { path: projectPath, source: "project" };
665
- }
666
- const userPath = getUserConfigPath();
667
- if (existsSync(userPath)) {
668
- return { path: userPath, source: "user" };
669
- }
670
- return null;
671
- }
672
- function getDefaultConfigPath() {
673
- return {
674
- path: getUserConfigPath(),
675
- source: "user"
676
- };
677
- }
678
- function ensureConfigDir(configPath) {
679
- const dir = dirname(configPath);
680
- ensureDir(dir);
681
- }
682
-
683
806
  // src/config/pid.ts
684
807
  function isProcessRunning(pid) {
685
808
  if (pid <= 0) {
@@ -950,18 +1073,18 @@ var UpstreamServerSchema = z.discriminatedUnion("transport", [
950
1073
  UpstreamSseSchema
951
1074
  ]);
952
1075
  var SecurityToolsSchema = z.object({
953
- allow: z.array(z.string()).default(["*:*"]),
1076
+ allow: z.array(z.string()).default([]),
954
1077
  block: z.array(z.string()).default([]),
955
- confirm: z.array(z.string()).default([])
1078
+ confirm: z.array(z.string()).default(["*:*"])
956
1079
  });
957
1080
  var SecuritySchema = z.object({
958
1081
  tools: SecurityToolsSchema.default({
959
- allow: ["*:*"],
1082
+ allow: [],
960
1083
  block: [],
961
- confirm: []
1084
+ confirm: ["*:*"]
962
1085
  })
963
1086
  }).default({
964
- tools: { allow: ["*:*"], block: [], confirm: [] }
1087
+ tools: { allow: [], block: [], confirm: ["*:*"] }
965
1088
  });
966
1089
  var SearchModeSchema = z.enum(["fast", "semantic", "hybrid"]);
967
1090
  var DetailLevelSchema = z.enum(["L0", "L1", "L2"]);
@@ -977,6 +1100,9 @@ var IndexSchema = z.object({
977
1100
  var LoggingSchema = z.object({
978
1101
  level: LogLevelSchema.default("info")
979
1102
  });
1103
+ var EmbeddingsSchema = z.object({
1104
+ enabled: z.boolean().default(false)
1105
+ });
980
1106
  var SelectionCacheSchema = z.object({
981
1107
  enabled: z.boolean().default(true),
982
1108
  minCooccurrenceThreshold: z.number().int().min(1).default(2),
@@ -991,6 +1117,7 @@ var OperationsSchema = z.object({
991
1117
  }),
992
1118
  index: IndexSchema.default({ refreshIntervalMs: 30000 }),
993
1119
  logging: LoggingSchema.default({ level: "info" }),
1120
+ embeddings: EmbeddingsSchema.default({ enabled: false }),
994
1121
  selectionCache: SelectionCacheSchema.default({
995
1122
  enabled: true,
996
1123
  minCooccurrenceThreshold: 2,
@@ -1005,6 +1132,7 @@ var OperationsSchema = z.object({
1005
1132
  },
1006
1133
  index: { refreshIntervalMs: 30000 },
1007
1134
  logging: { level: "info" },
1135
+ embeddings: { enabled: false },
1008
1136
  selectionCache: {
1009
1137
  enabled: true,
1010
1138
  minCooccurrenceThreshold: 2,
@@ -1061,6 +1189,7 @@ function migrateV0ToV1(config) {
1061
1189
  }
1062
1190
 
1063
1191
  // src/config/load.ts
1192
+ init_paths();
1064
1193
  class ConfigError extends Error {
1065
1194
  cause;
1066
1195
  constructor(message, cause) {
@@ -1140,8 +1269,14 @@ async function loadConfigFromPath(filePath, source) {
1140
1269
  }
1141
1270
  return { config, path: filePath, source };
1142
1271
  }
1272
+
1273
+ // src/config/index.ts
1274
+ init_paths();
1275
+
1143
1276
  // src/config/save.ts
1277
+ init_paths();
1144
1278
  import { stringify as stringifyToml } from "smol-toml";
1279
+
1145
1280
  class ConfigSaveError extends Error {
1146
1281
  filePath;
1147
1282
  constructor(filePath, cause) {
@@ -1285,6 +1420,9 @@ function formatValidationIssues(issues) {
1285
1420
  return lines.join(`
1286
1421
  `);
1287
1422
  }
1423
+ // src/index.ts
1424
+ init_paths();
1425
+
1288
1426
  // src/daemon/config-hash.ts
1289
1427
  import { createHash } from "crypto";
1290
1428
  function computeConfigHash(config) {
@@ -1294,10 +1432,12 @@ function computeConfigHash(config) {
1294
1432
  }
1295
1433
 
1296
1434
  // src/daemon/proxy.ts
1435
+ init_paths();
1297
1436
  import { spawn } from "child_process";
1298
1437
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1299
1438
 
1300
1439
  // src/daemon/registry.ts
1440
+ init_paths();
1301
1441
  import {
1302
1442
  existsSync as existsSync3,
1303
1443
  readFileSync as readFileSync2,
@@ -1336,6 +1476,9 @@ function readDaemonRegistry(configHash) {
1336
1476
  if (typeof data.daemonId !== "string" || typeof data.endpoint !== "string" || typeof data.pid !== "number" || typeof data.startedAt !== "number") {
1337
1477
  return null;
1338
1478
  }
1479
+ if (data.sharedSecret !== undefined && typeof data.sharedSecret !== "string") {
1480
+ return null;
1481
+ }
1339
1482
  return data;
1340
1483
  } catch {
1341
1484
  return null;
@@ -1347,7 +1490,7 @@ function writeDaemonRegistry(entry) {
1347
1490
  const tempPath = `${path}.${process.pid}.tmp`;
1348
1491
  const payload = `${JSON.stringify(entry, null, 2)}
1349
1492
  `;
1350
- writeFileSync2(tempPath, payload, { encoding: "utf8" });
1493
+ writeFileSync2(tempPath, payload, { encoding: "utf8", mode: 384 });
1351
1494
  renameSync2(tempPath, path);
1352
1495
  }
1353
1496
  function deleteDaemonRegistry(configHash) {
@@ -1562,24 +1705,30 @@ async function waitForDaemon(timeoutMs, configHash) {
1562
1705
  while (Date.now() - start < timeoutMs) {
1563
1706
  const entry = await loadLiveDaemonRegistry(configHash);
1564
1707
  if (entry) {
1565
- return { endpoint: entry.endpoint };
1708
+ return entry;
1566
1709
  }
1567
1710
  await sleep(100);
1568
1711
  }
1569
1712
  return null;
1570
1713
  }
1571
- function spawnDaemonProcess() {
1714
+ function spawnDaemonProcess(sharedSecret) {
1572
1715
  const execPath = process.execPath;
1573
1716
  const scriptPath = process.argv[1];
1574
1717
  const args = scriptPath ? [scriptPath, "daemon"] : ["daemon"];
1575
1718
  const child = spawn(execPath, args, {
1576
1719
  detached: true,
1577
- stdio: "ignore"
1720
+ stdio: "ignore",
1721
+ env: {
1722
+ ...process.env,
1723
+ ...sharedSecret ? { MCP_SQUARED_DAEMON_SECRET: sharedSecret } : {}
1724
+ }
1578
1725
  });
1579
1726
  child.unref();
1580
1727
  }
1581
1728
  async function createProxyBridge(options) {
1729
+ const spawnDaemon = options.spawnDaemon ?? spawnDaemonProcess;
1582
1730
  let endpoint = options.endpoint;
1731
+ let sharedSecret = options.sharedSecret?.trim();
1583
1732
  let sessionId = null;
1584
1733
  let heartbeatTimer = null;
1585
1734
  let isOwner = false;
@@ -1590,13 +1739,15 @@ async function createProxyBridge(options) {
1590
1739
  const registry = await loadLiveDaemonRegistry(options.configHash);
1591
1740
  if (registry) {
1592
1741
  endpoint = registry.endpoint;
1742
+ sharedSecret ??= registry.sharedSecret;
1593
1743
  } else if (!options.noSpawn) {
1594
- spawnDaemonProcess();
1744
+ spawnDaemon(sharedSecret);
1595
1745
  const entry = await waitForDaemon(DEFAULT_STARTUP_TIMEOUT_MS, options.configHash);
1596
1746
  if (!entry) {
1597
1747
  throw new Error("Timed out waiting for daemon to start");
1598
1748
  }
1599
1749
  endpoint = entry.endpoint;
1750
+ sharedSecret ??= entry.sharedSecret;
1600
1751
  } else if (options.configHash) {
1601
1752
  endpoint = getDaemonSocketPath(options.configHash);
1602
1753
  }
@@ -1672,7 +1823,8 @@ async function createProxyBridge(options) {
1672
1823
  const clientId = launcherHint ? `${launcherHint}-${process.pid}` : `proxy-${process.pid}`;
1673
1824
  daemonTransport.sendControl({
1674
1825
  type: "hello",
1675
- clientId
1826
+ clientId,
1827
+ ...sharedSecret ? { sharedSecret } : {}
1676
1828
  });
1677
1829
  heartbeatTimer = setInterval(() => {
1678
1830
  if (!sessionId) {
@@ -1705,26 +1857,140 @@ async function runProxy(options = {}) {
1705
1857
  }
1706
1858
 
1707
1859
  // src/daemon/server.ts
1860
+ init_paths();
1708
1861
  import { randomUUID } from "crypto";
1709
1862
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, unlinkSync as unlinkSync3 } from "fs";
1710
- import { connect as connect4, createServer } from "net";
1863
+ import {
1864
+ connect as connect4,
1865
+ createServer,
1866
+ isIPv4,
1867
+ isIPv6
1868
+ } from "net";
1711
1869
  import { dirname as dirname2 } from "path";
1870
+
1871
+ // src/version.ts
1872
+ import { readFileSync as readFileSync3 } from "fs";
1873
+ import { createRequire } from "module";
1874
+ function normalizeVersion(value) {
1875
+ if (typeof value !== "string") {
1876
+ return;
1877
+ }
1878
+ const trimmed = value.trim();
1879
+ return trimmed.length > 0 ? trimmed : undefined;
1880
+ }
1881
+ function readManifestFile(manifestUrl) {
1882
+ const raw = readFileSync3(manifestUrl, "utf8");
1883
+ return JSON.parse(raw);
1884
+ }
1885
+ function readBundledManifestFile() {
1886
+ const require2 = createRequire(import.meta.url);
1887
+ return require2("../package.json");
1888
+ }
1889
+ function resolveVersion(options = {}) {
1890
+ const readManifest = options.readManifest ?? readManifestFile;
1891
+ const manifestUrl = options.manifestUrl ?? new URL("../package.json", import.meta.url);
1892
+ try {
1893
+ const manifest = readManifest(manifestUrl);
1894
+ const manifestVersion = normalizeVersion(manifest.version);
1895
+ if (manifestVersion) {
1896
+ return manifestVersion;
1897
+ }
1898
+ } catch {}
1899
+ const envVersion = normalizeVersion((options.env ?? process.env)["npm_package_version"]);
1900
+ if (envVersion) {
1901
+ return envVersion;
1902
+ }
1903
+ const readBundledManifest = options.readBundledManifest ?? readBundledManifestFile;
1904
+ try {
1905
+ const bundledVersion = normalizeVersion(readBundledManifest().version);
1906
+ if (bundledVersion) {
1907
+ return bundledVersion;
1908
+ }
1909
+ } catch {}
1910
+ return normalizeVersion(options.fallbackVersion) ?? "0.0.0";
1911
+ }
1912
+ var VERSION = resolveVersion();
1913
+
1914
+ // src/daemon/server.ts
1712
1915
  var DEFAULT_CONNECT_TIMEOUT_MS3 = 300;
1713
1916
  function isTcpEndpoint4(endpoint) {
1714
1917
  return endpoint.startsWith("tcp://");
1715
1918
  }
1919
+ function parseMappedIpv4Address(normalizedHost) {
1920
+ if (!normalizedHost.startsWith("::ffff:")) {
1921
+ return null;
1922
+ }
1923
+ const mappedIpv4 = normalizedHost.slice("::ffff:".length);
1924
+ if (isIPv4(mappedIpv4)) {
1925
+ return mappedIpv4;
1926
+ }
1927
+ const hextets = mappedIpv4.split(":");
1928
+ if (hextets.length !== 2) {
1929
+ return null;
1930
+ }
1931
+ if (!hextets.every((segment) => /^[0-9a-f]{1,4}$/.test(segment))) {
1932
+ return null;
1933
+ }
1934
+ const high = Number.parseInt(hextets[0] ?? "", 16);
1935
+ const low = Number.parseInt(hextets[1] ?? "", 16);
1936
+ if (Number.isNaN(high) || Number.isNaN(low) || high < 0 || high > 65535 || low < 0 || low > 65535) {
1937
+ return null;
1938
+ }
1939
+ return `${high >> 8 & 255}.${high & 255}.${low >> 8 & 255}.${low & 255}`;
1940
+ }
1716
1941
  function parseTcpEndpoint4(endpoint) {
1717
1942
  const url = new URL(endpoint);
1718
1943
  if (url.protocol !== "tcp:") {
1719
1944
  throw new Error(`Invalid TCP endpoint protocol: ${url.protocol}`);
1720
1945
  }
1721
- const host = url.hostname;
1946
+ const normalizedHost = normalizeHost(url.hostname);
1947
+ const host = parseMappedIpv4Address(normalizedHost) ?? normalizedHost;
1722
1948
  const port = Number.parseInt(url.port, 10);
1723
1949
  if (!host || Number.isNaN(port)) {
1724
1950
  throw new Error(`Invalid TCP endpoint: ${endpoint}`);
1725
1951
  }
1726
1952
  return { host, port };
1727
1953
  }
1954
+ function normalizeHost(host) {
1955
+ const lowered = host.toLowerCase();
1956
+ if (lowered.startsWith("[") && lowered.endsWith("]")) {
1957
+ return lowered.slice(1, -1);
1958
+ }
1959
+ return lowered;
1960
+ }
1961
+ function isMappedIpv4Loopback(normalizedHost) {
1962
+ const mappedIpv4 = parseMappedIpv4Address(normalizedHost);
1963
+ return mappedIpv4 !== null && mappedIpv4.split(".")[0] === "127";
1964
+ }
1965
+ function isLoopbackHost(host) {
1966
+ const normalized = normalizeHost(host);
1967
+ if (normalized === "localhost") {
1968
+ return true;
1969
+ }
1970
+ if (isIPv4(normalized)) {
1971
+ return normalized.split(".")[0] === "127";
1972
+ }
1973
+ if (isMappedIpv4Loopback(normalized)) {
1974
+ return true;
1975
+ }
1976
+ if (isIPv6(normalized)) {
1977
+ return normalized === "::1";
1978
+ }
1979
+ return false;
1980
+ }
1981
+ function formatTcpEndpoint(host, port) {
1982
+ const normalized = normalizeHost(host);
1983
+ if (normalized.includes(":")) {
1984
+ return `tcp://[${normalized}]:${port}`;
1985
+ }
1986
+ return `tcp://${normalized}:${port}`;
1987
+ }
1988
+ function assertLoopbackTcpEndpoint(endpoint) {
1989
+ const { host } = parseTcpEndpoint4(endpoint);
1990
+ if (!isLoopbackHost(host)) {
1991
+ throw new Error(`Refusing non-loopback daemon TCP endpoint: ${endpoint}. Use localhost, 127.0.0.1, or ::1.`);
1992
+ }
1993
+ }
1728
1994
  async function canConnect3(endpoint, timeoutMs = DEFAULT_CONNECT_TIMEOUT_MS3) {
1729
1995
  return new Promise((resolve2) => {
1730
1996
  let socket = null;
@@ -1758,6 +2024,7 @@ class DaemonServer {
1758
2024
  idleTimeoutMs;
1759
2025
  heartbeatTimeoutMs;
1760
2026
  configHash;
2027
+ sharedSecret;
1761
2028
  onIdleShutdown;
1762
2029
  server = null;
1763
2030
  sessions = new Map;
@@ -1770,6 +2037,10 @@ class DaemonServer {
1770
2037
  this.socketPath = options.socketPath ?? getDaemonSocketPath(this.configHash);
1771
2038
  this.idleTimeoutMs = options.idleTimeoutMs ?? 5000;
1772
2039
  this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? 15000;
2040
+ const sharedSecret = options.sharedSecret?.trim();
2041
+ if (sharedSecret) {
2042
+ this.sharedSecret = sharedSecret;
2043
+ }
1773
2044
  if (options.onIdleShutdown) {
1774
2045
  this.onIdleShutdown = options.onIdleShutdown;
1775
2046
  }
@@ -1780,6 +2051,9 @@ class DaemonServer {
1780
2051
  }
1781
2052
  ensureDaemonDir(this.configHash);
1782
2053
  const tcp = isTcpEndpoint4(this.socketPath);
2054
+ if (tcp) {
2055
+ assertLoopbackTcpEndpoint(this.socketPath);
2056
+ }
1783
2057
  if (!tcp) {
1784
2058
  mkdirSync3(dirname2(this.socketPath), { recursive: true });
1785
2059
  }
@@ -1804,9 +2078,7 @@ class DaemonServer {
1804
2078
  await new Promise((resolve2, reject) => {
1805
2079
  this.server?.once("error", (error) => reject(error));
1806
2080
  if (tcp) {
1807
- const url = new URL(this.socketPath);
1808
- const host = url.hostname;
1809
- const port = Number.parseInt(url.port, 10);
2081
+ const { host, port } = parseTcpEndpoint4(this.socketPath);
1810
2082
  if (!host || Number.isNaN(port)) {
1811
2083
  reject(new Error(`Invalid TCP endpoint: ${this.socketPath}`));
1812
2084
  return;
@@ -1819,7 +2091,7 @@ class DaemonServer {
1819
2091
  if (tcp) {
1820
2092
  const address = this.server.address();
1821
2093
  if (address && typeof address !== "string") {
1822
- this.endpoint = `tcp://${address.address}:${address.port}`;
2094
+ this.endpoint = formatTcpEndpoint(address.address, address.port);
1823
2095
  } else {
1824
2096
  this.endpoint = this.socketPath;
1825
2097
  }
@@ -1832,7 +2104,8 @@ class DaemonServer {
1832
2104
  pid: process.pid,
1833
2105
  startedAt: Date.now(),
1834
2106
  version: VERSION,
1835
- ...this.configHash ? { configHash: this.configHash } : {}
2107
+ ...this.configHash ? { configHash: this.configHash } : {},
2108
+ ...this.sharedSecret ? { sharedSecret: this.sharedSecret } : {}
1836
2109
  };
1837
2110
  writeDaemonRegistry(registryEntry);
1838
2111
  if (!this.heartbeatTimer) {
@@ -1884,35 +2157,67 @@ class DaemonServer {
1884
2157
  transport.sessionId = sessionId;
1885
2158
  const session = {
1886
2159
  id: sessionId,
2160
+ authenticated: false,
1887
2161
  connectedAt: Date.now(),
1888
2162
  lastSeen: Date.now(),
1889
2163
  server: sessionServer,
1890
2164
  transport
1891
2165
  };
1892
2166
  this.sessions.set(sessionId, session);
1893
- this.runtime.getStatsCollector().incrementActiveConnections();
1894
- this.assignOwnerIfNeeded();
1895
- this.clearIdleTimer();
2167
+ let sessionServerConnected = false;
2168
+ const connectSessionServer = async () => {
2169
+ if (sessionServerConnected) {
2170
+ return;
2171
+ }
2172
+ sessionServerConnected = true;
2173
+ await sessionServer.connect(transport);
2174
+ const original = transport.onmessage;
2175
+ transport.onmessage = (message, extra) => {
2176
+ session.lastSeen = Date.now();
2177
+ original?.(message, extra);
2178
+ };
2179
+ };
1896
2180
  transport.onclose = () => {
1897
2181
  this.handleDisconnect(sessionId);
1898
2182
  };
1899
2183
  transport.onerror = () => {
1900
2184
  this.handleDisconnect(sessionId);
1901
2185
  };
1902
- transport.oncontrol = (message) => {
2186
+ transport.oncontrol = async (message) => {
1903
2187
  switch (message.type) {
1904
2188
  case "hello":
2189
+ if (this.sharedSecret !== undefined && message.sharedSecret !== this.sharedSecret) {
2190
+ try {
2191
+ await transport.sendControl({
2192
+ type: "error",
2193
+ message: "Daemon authentication failed: invalid shared secret."
2194
+ });
2195
+ } finally {
2196
+ await this.handleDisconnect(sessionId);
2197
+ }
2198
+ break;
2199
+ }
1905
2200
  if (message.clientId !== undefined) {
1906
2201
  session.clientId = message.clientId;
1907
2202
  }
2203
+ if (!session.authenticated) {
2204
+ session.authenticated = true;
2205
+ this.runtime.getStatsCollector().incrementActiveConnections();
2206
+ this.assignOwnerIfNeeded();
2207
+ this.clearIdleTimer();
2208
+ }
1908
2209
  session.lastSeen = Date.now();
1909
2210
  transport.sendControl({
1910
2211
  type: "helloAck",
1911
2212
  sessionId,
1912
2213
  isOwner: this.ownerSessionId === sessionId
1913
2214
  });
2215
+ connectSessionServer().catch(() => this.handleDisconnect(sessionId));
1914
2216
  break;
1915
2217
  case "heartbeat":
2218
+ if (!session.authenticated) {
2219
+ break;
2220
+ }
1916
2221
  session.lastSeen = Date.now();
1917
2222
  break;
1918
2223
  case "goodbye":
@@ -1920,13 +2225,7 @@ class DaemonServer {
1920
2225
  break;
1921
2226
  }
1922
2227
  };
1923
- sessionServer.connect(transport).then(() => {
1924
- const original = transport.onmessage;
1925
- transport.onmessage = (message, extra) => {
1926
- session.lastSeen = Date.now();
1927
- original?.(message, extra);
1928
- };
1929
- }).catch(() => this.handleDisconnect(sessionId));
2228
+ transport.start().catch(() => this.handleDisconnect(sessionId));
1930
2229
  }
1931
2230
  async handleDisconnect(sessionId) {
1932
2231
  const session = this.sessions.get(sessionId);
@@ -1938,7 +2237,9 @@ class DaemonServer {
1938
2237
  await session.server.close();
1939
2238
  await session.transport.close();
1940
2239
  } catch {}
1941
- this.runtime.getStatsCollector().decrementActiveConnections();
2240
+ if (session.authenticated) {
2241
+ this.runtime.getStatsCollector().decrementActiveConnections();
2242
+ }
1942
2243
  if (this.ownerSessionId === sessionId) {
1943
2244
  this.ownerSessionId = null;
1944
2245
  this.assignOwnerIfNeeded();
@@ -1951,7 +2252,7 @@ class DaemonServer {
1951
2252
  if (this.ownerSessionId) {
1952
2253
  return;
1953
2254
  }
1954
- const next = Array.from(this.sessions.values()).sort((a, b) => a.connectedAt - b.connectedAt)[0];
2255
+ const next = Array.from(this.sessions.values()).filter((session) => session.authenticated).sort((a, b) => a.connectedAt - b.connectedAt)[0];
1955
2256
  if (next) {
1956
2257
  this.ownerSessionId = next.id;
1957
2258
  this.broadcastOwnerChange();
@@ -1962,6 +2263,9 @@ class DaemonServer {
1962
2263
  return;
1963
2264
  }
1964
2265
  for (const session of this.sessions.values()) {
2266
+ if (!session.authenticated) {
2267
+ continue;
2268
+ }
1965
2269
  session.transport.sendControl({
1966
2270
  type: "ownerChanged",
1967
2271
  ownerSessionId: this.ownerSessionId
@@ -2015,7 +2319,7 @@ class DaemonServer {
2015
2319
  return this.ownerSessionId;
2016
2320
  }
2017
2321
  getClientInfo() {
2018
- return Array.from(this.sessions.values()).map((session) => {
2322
+ return Array.from(this.sessions.values()).filter((session) => session.authenticated).map((session) => {
2019
2323
  const info = {
2020
2324
  sessionId: session.id,
2021
2325
  connectedAt: session.connectedAt,
@@ -2343,6 +2647,16 @@ function mapToSseServer(server) {
2343
2647
  headers: normalizeEnvVars(server.headers ?? {})
2344
2648
  }
2345
2649
  };
2650
+ if (server.auth !== undefined) {
2651
+ if (server.auth && typeof server.auth === "object") {
2652
+ config.sse.auth = {
2653
+ callbackPort: server.auth.callbackPort ?? 8089,
2654
+ clientName: server.auth.clientName ?? "MCP\xB2"
2655
+ };
2656
+ } else {
2657
+ config.sse.auth = server.auth;
2658
+ }
2659
+ }
2346
2660
  return {
2347
2661
  name: server.name,
2348
2662
  config
@@ -3441,7 +3755,7 @@ function promptUser(question) {
3441
3755
  }
3442
3756
 
3443
3757
  // src/install/runner.ts
3444
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
3758
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
3445
3759
  import { dirname as dirname3 } from "path";
3446
3760
  import { createInterface } from "readline";
3447
3761
  import { parse as parseToml3, stringify as stringifyToml2 } from "smol-toml";
@@ -3761,7 +4075,7 @@ function performInstallation(options) {
3761
4075
  if (existsSync7(path)) {
3762
4076
  configExists = true;
3763
4077
  try {
3764
- const content = readFileSync3(path, "utf-8");
4078
+ const content = readFileSync4(path, "utf-8");
3765
4079
  existingConfig = isToml ? parseToml3(content) : JSON.parse(content);
3766
4080
  } catch (error) {
3767
4081
  return {
@@ -4180,8 +4494,34 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4180
4494
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4181
4495
 
4182
4496
  // src/oauth/provider.ts
4183
- var DEFAULT_CALLBACK_PORT = 8089;
4184
- var DEFAULT_CLIENT_NAME = "MCP\xB2";
4497
+ var DEFAULT_OAUTH_CALLBACK_PORT = 8089;
4498
+ var DEFAULT_OAUTH_CLIENT_NAME = "MCP\xB2";
4499
+ function isValidCallbackPort(value) {
4500
+ return Number.isInteger(value) && value >= 1 && value <= 65535;
4501
+ }
4502
+ function isValidClientName(value) {
4503
+ return value.trim().length > 0;
4504
+ }
4505
+ function resolveOAuthProviderOptions(authConfig) {
4506
+ if (!authConfig || typeof authConfig !== "object") {
4507
+ return {
4508
+ callbackPort: DEFAULT_OAUTH_CALLBACK_PORT,
4509
+ clientName: DEFAULT_OAUTH_CLIENT_NAME
4510
+ };
4511
+ }
4512
+ const callbackPort = authConfig.callbackPort ?? DEFAULT_OAUTH_CALLBACK_PORT;
4513
+ if (!isValidCallbackPort(callbackPort)) {
4514
+ throw new RangeError(`Invalid OAuth callbackPort: ${callbackPort}`);
4515
+ }
4516
+ const clientName = authConfig.clientName ?? DEFAULT_OAUTH_CLIENT_NAME;
4517
+ if (typeof clientName !== "string" || !isValidClientName(clientName)) {
4518
+ throw new TypeError(`Invalid OAuth clientName: ${String(clientName)}`);
4519
+ }
4520
+ return {
4521
+ callbackPort,
4522
+ clientName
4523
+ };
4524
+ }
4185
4525
 
4186
4526
  class McpOAuthProvider {
4187
4527
  upstreamName;
@@ -4193,8 +4533,8 @@ class McpOAuthProvider {
4193
4533
  constructor(upstreamName, storage, options = {}) {
4194
4534
  this.upstreamName = upstreamName;
4195
4535
  this.storage = storage;
4196
- this.callbackPort = options.callbackPort ?? DEFAULT_CALLBACK_PORT;
4197
- this._clientName = options.clientName ?? DEFAULT_CLIENT_NAME;
4536
+ this.callbackPort = options.callbackPort ?? DEFAULT_OAUTH_CALLBACK_PORT;
4537
+ this._clientName = options.clientName ?? DEFAULT_OAUTH_CLIENT_NAME;
4198
4538
  this._nonInteractive = options.nonInteractive ?? false;
4199
4539
  }
4200
4540
  get redirectUrl() {
@@ -4322,7 +4662,7 @@ import {
4322
4662
  chmodSync,
4323
4663
  existsSync as existsSync8,
4324
4664
  mkdirSync as mkdirSync5,
4325
- readFileSync as readFileSync4,
4665
+ readFileSync as readFileSync5,
4326
4666
  unlinkSync as unlinkSync4,
4327
4667
  writeFileSync as writeFileSync4
4328
4668
  } from "fs";
@@ -4360,7 +4700,7 @@ class TokenStorage {
4360
4700
  return;
4361
4701
  }
4362
4702
  try {
4363
- const content = readFileSync4(filePath, "utf-8");
4703
+ const content = readFileSync5(filePath, "utf-8");
4364
4704
  return JSON.parse(content);
4365
4705
  } catch {
4366
4706
  return;
@@ -4439,6 +4779,12 @@ class TokenStorage {
4439
4779
  }
4440
4780
 
4441
4781
  // src/oauth/preflight.ts
4782
+ function getPreflightClientMetadata() {
4783
+ return {
4784
+ name: "mcp-squared-preflight",
4785
+ version: VERSION
4786
+ };
4787
+ }
4442
4788
  async function performPreflightAuth(config) {
4443
4789
  const result = {
4444
4790
  authenticated: [],
@@ -4461,23 +4807,21 @@ async function performPreflightAuth(config) {
4461
4807
  return result;
4462
4808
  }
4463
4809
  for (const { name, config: sseConfig } of sseUpstreams) {
4464
- const authConfig = typeof sseConfig.sse.auth === "object" ? sseConfig.sse.auth : undefined;
4465
- const callbackPort = authConfig?.callbackPort ?? 8089;
4466
- const clientName = authConfig?.clientName ?? "MCP\xB2";
4467
- const authProvider = new McpOAuthProvider(name, tokenStorage, {
4468
- callbackPort,
4469
- clientName
4470
- });
4471
- const existingTokens = authProvider.tokens();
4472
- if (existingTokens && !authProvider.isTokenExpired()) {
4473
- result.alreadyValid.push(name);
4474
- continue;
4475
- }
4476
- console.error(`
4477
- [preflight] OAuth required for '${name}'`);
4478
- console.error(`[preflight] Server URL: ${sseConfig.sse.url}`);
4479
4810
  try {
4480
- await performInteractiveAuth(name, sseConfig, authProvider);
4811
+ const { callbackPort, clientName } = resolveOAuthProviderOptions(sseConfig.sse.auth);
4812
+ const authProvider = new McpOAuthProvider(name, tokenStorage, {
4813
+ callbackPort,
4814
+ clientName
4815
+ });
4816
+ const existingTokens = authProvider.tokens();
4817
+ if (existingTokens && !authProvider.isTokenExpired()) {
4818
+ result.alreadyValid.push(name);
4819
+ continue;
4820
+ }
4821
+ console.error(`
4822
+ [preflight] OAuth required for '${name}'`);
4823
+ console.error(`[preflight] Server URL: ${sseConfig.sse.url}`);
4824
+ await performInteractiveAuth(name, sseConfig, authProvider, callbackPort);
4481
4825
  result.authenticated.push(name);
4482
4826
  console.error(`[preflight] \u2713 Authentication successful for '${name}'`);
4483
4827
  } catch (err) {
@@ -4488,9 +4832,7 @@ async function performPreflightAuth(config) {
4488
4832
  }
4489
4833
  return result;
4490
4834
  }
4491
- async function performInteractiveAuth(name, sseConfig, authProvider) {
4492
- const authConfig = typeof sseConfig.sse.auth === "object" ? sseConfig.sse.auth : undefined;
4493
- const callbackPort = authConfig?.callbackPort ?? 8089;
4835
+ async function performInteractiveAuth(name, sseConfig, authProvider, callbackPort) {
4494
4836
  const callbackServer = new OAuthCallbackServer({
4495
4837
  port: callbackPort,
4496
4838
  path: "/callback",
@@ -4503,10 +4845,7 @@ async function performInteractiveAuth(name, sseConfig, authProvider) {
4503
4845
  headers: { ...sseConfig.sse.headers }
4504
4846
  }
4505
4847
  });
4506
- const client = new Client({
4507
- name: "mcp-squared-preflight",
4508
- version: "0.1.0"
4509
- });
4848
+ const client = new Client(getPreflightClientMetadata());
4510
4849
  try {
4511
4850
  console.error(`[preflight:${name}] Connecting to server (will trigger OAuth)...`);
4512
4851
  await client.connect(transport);
@@ -4905,7 +5244,7 @@ class Guard {
4905
5244
  }
4906
5245
  }
4907
5246
  // agent_safety_kit/policy/load.ts
4908
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
5247
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
4909
5248
  import { resolve as resolve3 } from "path";
4910
5249
  import { parse as parseYaml } from "yaml";
4911
5250
 
@@ -4968,7 +5307,7 @@ function load_policy(options = {}) {
4968
5307
  if (!existsSync9(sourcePath)) {
4969
5308
  throw new Error(`Agent safety policy file not found at ${sourcePath}`);
4970
5309
  }
4971
- const raw = readFileSync5(sourcePath, "utf8");
5310
+ const raw = readFileSync6(sourcePath, "utf8");
4972
5311
  const parsed = parseYaml(raw);
4973
5312
  const policy = AgentPolicySchema.parse(parsed);
4974
5313
  const playbookName = options.playbook ?? envConfig.playbook;
@@ -5005,7 +5344,7 @@ class NullSink {
5005
5344
  }
5006
5345
 
5007
5346
  // agent_safety_kit/observability/sinks/otel.ts
5008
- import { createRequire } from "module";
5347
+ import { createRequire as createRequire2 } from "module";
5009
5348
 
5010
5349
  // agent_safety_kit/observability/sinks/base.ts
5011
5350
  function compactAttributes(attributes) {
@@ -5022,7 +5361,7 @@ function compactAttributes(attributes) {
5022
5361
  }
5023
5362
 
5024
5363
  // agent_safety_kit/observability/sinks/otel.ts
5025
- var require2 = createRequire(import.meta.url);
5364
+ var require2 = createRequire2(import.meta.url);
5026
5365
  function loadOpenTelemetryApi() {
5027
5366
  try {
5028
5367
  return require2("@opentelemetry/api");
@@ -5859,7 +6198,8 @@ class Retriever {
5859
6198
  return {
5860
6199
  tools,
5861
6200
  query,
5862
- totalMatches: allTools.length
6201
+ totalMatches: allTools.length,
6202
+ searchMode: mode
5863
6203
  };
5864
6204
  }
5865
6205
  switch (mode) {
@@ -5881,12 +6221,15 @@ class Retriever {
5881
6221
  serverKey: r.serverKey
5882
6222
  })),
5883
6223
  query,
5884
- totalMatches
6224
+ totalMatches,
6225
+ searchMode: "fast"
5885
6226
  };
5886
6227
  }
5887
6228
  async searchSemantic(query, limit) {
5888
6229
  if (!this.embeddingGenerator || this.indexStore.getEmbeddingCount() === 0) {
5889
- return this.searchFast(query, limit);
6230
+ console.error("[mcp\xB2] Semantic search requested but embeddings not available \u2014 falling back to fast (FTS5) mode.");
6231
+ const result2 = this.searchFast(query, limit);
6232
+ return { ...result2, searchMode: "fast" };
5890
6233
  }
5891
6234
  const result = await this.embeddingGenerator.embed(query, true);
5892
6235
  const queryEmbedding = result.embedding;
@@ -5898,17 +6241,20 @@ class Retriever {
5898
6241
  serverKey: r.serverKey
5899
6242
  })),
5900
6243
  query,
5901
- totalMatches: results.length
6244
+ totalMatches: results.length,
6245
+ searchMode: "semantic"
5902
6246
  };
5903
6247
  }
5904
6248
  async searchHybrid(query, limit) {
5905
6249
  if (!this.embeddingGenerator || this.indexStore.getEmbeddingCount() === 0) {
5906
- return this.searchFast(query, limit);
6250
+ console.error("[mcp\xB2] Hybrid search requested but embeddings not available \u2014 falling back to fast (FTS5) mode.");
6251
+ const result2 = this.searchFast(query, limit);
6252
+ return { ...result2, searchMode: "fast" };
5907
6253
  }
5908
6254
  const candidateLimit = Math.min(limit * 3, 100);
5909
6255
  const ftsResults = this.indexStore.search(query, candidateLimit);
5910
6256
  if (ftsResults.length === 0) {
5911
- return { tools: [], query, totalMatches: 0 };
6257
+ return { tools: [], query, totalMatches: 0, searchMode: "hybrid" };
5912
6258
  }
5913
6259
  const result = await this.embeddingGenerator.embed(query, true);
5914
6260
  const queryEmbedding = result.embedding;
@@ -5943,7 +6289,8 @@ class Retriever {
5943
6289
  serverKey: r.serverKey
5944
6290
  })),
5945
6291
  query,
5946
- totalMatches: ftsResults.length
6292
+ totalMatches: ftsResults.length,
6293
+ searchMode: "hybrid"
5947
6294
  };
5948
6295
  }
5949
6296
  getTool(name, serverKey) {
@@ -6125,6 +6472,12 @@ function evaluatePolicy(context, config) {
6125
6472
  reason: `Tool "${toolName}" on server "${serverKey}" is blocked by security policy`
6126
6473
  };
6127
6474
  }
6475
+ if (matchesAnyPattern(allow, serverKey, toolName)) {
6476
+ return {
6477
+ decision: "allow",
6478
+ reason: `Tool "${toolName}" is allowed by security policy`
6479
+ };
6480
+ }
6128
6481
  if (matchesAnyPattern(confirm, serverKey, toolName)) {
6129
6482
  if (confirmationToken && validateConfirmationToken(confirmationToken, serverKey, toolName)) {
6130
6483
  return {
@@ -6139,15 +6492,9 @@ function evaluatePolicy(context, config) {
6139
6492
  confirmationToken: token
6140
6493
  };
6141
6494
  }
6142
- if (matchesAnyPattern(allow, serverKey, toolName)) {
6143
- return {
6144
- decision: "allow",
6145
- reason: `Tool "${toolName}" is allowed by security policy`
6146
- };
6147
- }
6148
6495
  return {
6149
6496
  decision: "block",
6150
- reason: `Tool "${toolName}" on server "${serverKey}" is not in the allow list`
6497
+ reason: `Tool "${toolName}" on server "${serverKey}" is not in the allow or confirm list`
6151
6498
  };
6152
6499
  }
6153
6500
  function compilePolicy(config) {
@@ -6164,12 +6511,12 @@ function getToolVisibilityFromPatterns(serverKey, toolName, block, confirm, allo
6164
6511
  if (matchesAnyPattern(block, serverKey, toolName)) {
6165
6512
  return { visible: false, requiresConfirmation: false };
6166
6513
  }
6167
- if (matchesAnyPattern(confirm, serverKey, toolName)) {
6168
- return { visible: true, requiresConfirmation: true };
6169
- }
6170
6514
  if (matchesAnyPattern(allow, serverKey, toolName)) {
6171
6515
  return { visible: true, requiresConfirmation: false };
6172
6516
  }
6517
+ if (matchesAnyPattern(confirm, serverKey, toolName)) {
6518
+ return { visible: true, requiresConfirmation: true };
6519
+ }
6173
6520
  return { visible: false, requiresConfirmation: false };
6174
6521
  }
6175
6522
  // src/security/sanitize.ts
@@ -6355,7 +6702,8 @@ class Cataloger {
6355
6702
  client: null,
6356
6703
  transport: null,
6357
6704
  authProvider: null,
6358
- authPending: false
6705
+ authPending: false,
6706
+ authStateVersion: this.getAuthStateVersion(key)
6359
6707
  };
6360
6708
  this.connections.set(key, connection);
6361
6709
  try {
@@ -6557,7 +6905,11 @@ class Cataloger {
6557
6905
  }
6558
6906
  async refreshTools(key) {
6559
6907
  const connection = this.connections.get(key);
6560
- if (!connection?.client || connection.status !== "connected") {
6908
+ if (!connection) {
6909
+ return;
6910
+ }
6911
+ if (!connection.client || connection.status !== "connected") {
6912
+ await this.reconnectIfAuthStateUpdated(connection);
6561
6913
  return;
6562
6914
  }
6563
6915
  try {
@@ -6568,7 +6920,17 @@ class Cataloger {
6568
6920
  inputSchema: tool.inputSchema,
6569
6921
  serverKey: key
6570
6922
  }));
6923
+ connection.error = undefined;
6924
+ connection.authPending = false;
6571
6925
  } catch (err) {
6926
+ if (err instanceof UnauthorizedError2 && connection.authProvider) {
6927
+ if (connection.authProvider.isNonInteractive()) {
6928
+ connection.authPending = true;
6929
+ connection.status = "error";
6930
+ connection.error = `OAuth authorization required. Run: mcp-squared auth ${key}`;
6931
+ return;
6932
+ }
6933
+ }
6572
6934
  connection.status = "error";
6573
6935
  connection.error = err instanceof Error ? err.message : String(err);
6574
6936
  }
@@ -6580,6 +6942,21 @@ class Cataloger {
6580
6942
  }
6581
6943
  await Promise.allSettled(refreshPromises);
6582
6944
  }
6945
+ async reconnectIfAuthStateUpdated(connection) {
6946
+ if (!connection.authPending || !connection.authProvider) {
6947
+ return;
6948
+ }
6949
+ const authStateVersion = this.getAuthStateVersion(connection.key);
6950
+ if (authStateVersion <= connection.authStateVersion) {
6951
+ return;
6952
+ }
6953
+ connection.authStateVersion = authStateVersion;
6954
+ await this.connect(connection.key, connection.config);
6955
+ }
6956
+ getAuthStateVersion(key) {
6957
+ const tokenStorage = new TokenStorage;
6958
+ return tokenStorage.load(key)?.updatedAt ?? 0;
6959
+ }
6583
6960
  createStdioTransport(config) {
6584
6961
  const resolvedEnv = resolveEnvVars(config.env);
6585
6962
  const envWithDefaults = { ...process.env, ...resolvedEnv };
@@ -6700,10 +7077,15 @@ function createHttpTransport(config, log, verbose, authProvider) {
6700
7077
  const transport = new StreamableHTTPClientTransport3(new URL(config.sse.url), transportOptions);
6701
7078
  return transport;
6702
7079
  }
6703
- async function handleOAuthCallback(transport, provider, log) {
6704
- const callbackServer = new OAuthCallbackServer({
6705
- port: 8089,
6706
- path: "/callback",
7080
+ async function handleOAuthCallback(transport, provider, log, callbackServerFactory = (options) => new OAuthCallbackServer(options)) {
7081
+ const callbackUrl = new URL(provider.redirectUrl);
7082
+ const callbackPort = Number.parseInt(callbackUrl.port, 10);
7083
+ if (Number.isNaN(callbackPort) || callbackPort <= 0) {
7084
+ throw new Error(`Invalid OAuth callback URL: ${provider.redirectUrl}`);
7085
+ }
7086
+ const callbackServer = callbackServerFactory({
7087
+ port: callbackPort,
7088
+ path: callbackUrl.pathname || "/callback",
6707
7089
  timeoutMs: 300000
6708
7090
  });
6709
7091
  log("Waiting for browser authorization...");
@@ -6760,10 +7142,11 @@ async function testUpstreamConnection(name, config, options = {}) {
6760
7142
  const tokenStorage = new TokenStorage;
6761
7143
  const hasStoredTokens = tokenStorage.load(name)?.tokens !== undefined;
6762
7144
  if (sseConfig.sse.auth || hasStoredTokens) {
6763
- const authOptions = typeof sseConfig.sse.auth === "object" ? sseConfig.sse.auth : {};
7145
+ const authOptions = resolveOAuthProviderOptions(sseConfig.sse.auth);
6764
7146
  authProvider = new McpOAuthProvider(name, tokenStorage, authOptions);
6765
7147
  }
6766
- httpTransport = createHttpTransport(sseConfig, log, verbose, authProvider);
7148
+ const httpTransportFactory = options.httpTransportFactory ?? createHttpTransport;
7149
+ httpTransport = httpTransportFactory(sseConfig, log, verbose, authProvider);
6767
7150
  transport = httpTransport;
6768
7151
  } else {
6769
7152
  const unknownConfig = config;
@@ -6790,7 +7173,7 @@ async function testUpstreamConnection(name, config, options = {}) {
6790
7173
  if (err instanceof UnauthorizedError3 && authProvider && httpTransport) {
6791
7174
  if (authProvider.isInteractive()) {
6792
7175
  log("OAuth authorization required, opening browser...");
6793
- await handleOAuthCallback(httpTransport, authProvider, log);
7176
+ await handleOAuthCallback(httpTransport, authProvider, log, options.oauthCallbackServerFactory);
6794
7177
  log("Retrying connection after OAuth...");
6795
7178
  await Promise.race([client.connect(transport), timeoutPromise]);
6796
7179
  } else {
@@ -7372,6 +7755,12 @@ class McpSquaredServer {
7372
7755
  });
7373
7756
  this.indexRefreshManager.on("refresh:complete", () => {
7374
7757
  this.statsCollector.updateIndexRefreshTime(Date.now());
7758
+ if (this.config.operations.embeddings.enabled) {
7759
+ this.retriever.generateToolEmbeddings().catch((err) => {
7760
+ const message = err instanceof Error ? err.message : String(err);
7761
+ console.error(`[mcp\xB2] Background embedding generation failed \u2014 ${message}`);
7762
+ });
7763
+ }
7375
7764
  });
7376
7765
  this.registerMetaTools(this.mcpServer);
7377
7766
  }
@@ -7446,6 +7835,8 @@ class McpSquaredServer {
7446
7835
  query: result.query,
7447
7836
  totalMatches: filteredTools.length,
7448
7837
  detailLevel,
7838
+ searchMode: result.searchMode,
7839
+ embeddingsAvailable: this.retriever.hasEmbeddings(),
7449
7840
  tools,
7450
7841
  ...suggestedTools && { suggestedTools }
7451
7842
  })
@@ -7550,7 +7941,7 @@ class McpSquaredServer {
7550
7941
  serverKey = tool.serverKey;
7551
7942
  const policyResult = evaluatePolicy({
7552
7943
  serverKey: tool.serverKey,
7553
- toolName: args.tool_name,
7944
+ toolName: tool.name,
7554
7945
  confirmationToken: args.confirmation_token
7555
7946
  }, this.config);
7556
7947
  if (policyResult.decision === "block") {
@@ -7792,6 +8183,21 @@ class McpSquaredServer {
7792
8183
  });
7793
8184
  await Promise.all(connectionPromises);
7794
8185
  this.syncIndex();
8186
+ if (this.config.operations.embeddings.enabled) {
8187
+ try {
8188
+ await this.retriever.initializeEmbeddings();
8189
+ const embeddingCount = await this.retriever.generateToolEmbeddings();
8190
+ const toolCount = this.retriever.getIndexedToolCount();
8191
+ if (this.retriever.hasEmbeddings()) {
8192
+ console.error(`[mcp\xB2] Embeddings: initialized (${embeddingCount}/${toolCount} tools embedded). Search modes: semantic, hybrid available.`);
8193
+ } else {
8194
+ console.error(`[mcp\xB2] Embeddings: enabled but runtime unavailable (onnxruntime not found). Falling back to fast (FTS5) search.`);
8195
+ }
8196
+ } catch (err) {
8197
+ const message = err instanceof Error ? err.message : String(err);
8198
+ console.error(`[mcp\xB2] Embeddings: initialization failed \u2014 ${message}. Falling back to fast (FTS5) search.`);
8199
+ }
8200
+ }
7795
8201
  this.statsCollector.updateIndexRefreshTime(Date.now());
7796
8202
  this.indexRefreshManager.start();
7797
8203
  await this.monitorServer.start();
@@ -7820,9 +8226,24 @@ class McpSquaredServer {
7820
8226
  }
7821
8227
 
7822
8228
  // src/index.ts
7823
- var VERSION = "0.1.0";
8229
+ function logSecurityProfile(config) {
8230
+ const { allow, confirm } = config.security.tools;
8231
+ const isHardened = confirm.includes("*:*") && allow.length === 0;
8232
+ if (isHardened) {
8233
+ console.error("[mcp\xB2] Security: confirm-all mode (default). Tools require confirmation before execution. To use permissive mode: mcp-squared init --security=permissive");
8234
+ }
8235
+ }
8236
+ function logSearchModeProfile(config) {
8237
+ const { defaultMode } = config.operations.findTools;
8238
+ const { enabled: embeddingsEnabled } = config.operations.embeddings;
8239
+ if ((defaultMode === "semantic" || defaultMode === "hybrid") && !embeddingsEnabled) {
8240
+ console.error(`[mcp\xB2] Search: defaultMode is "${defaultMode}" but embeddings are disabled. Searches will fall back to fast (FTS5). Enable with: [operations.embeddings] enabled = true`);
8241
+ }
8242
+ }
7824
8243
  async function startServer() {
7825
8244
  const { config, path: configPath } = await loadConfig();
8245
+ logSecurityProfile(config);
8246
+ logSearchModeProfile(config);
7826
8247
  await listActiveInstanceEntries({ prune: true });
7827
8248
  const preflightResult = await performPreflightAuth(config);
7828
8249
  if (preflightResult.authenticated.length > 0) {
@@ -8043,9 +8464,15 @@ async function runAuth(targetName) {
8043
8464
  Authenticating with '${targetName}'...`);
8044
8465
  console.log(`Server URL: ${sseConfig.sse.url}`);
8045
8466
  const tokenStorage = new TokenStorage;
8046
- const authConfig = typeof sseConfig.sse.auth === "object" ? sseConfig.sse.auth : undefined;
8047
- const callbackPort = authConfig?.callbackPort ?? 8089;
8048
- const clientName = authConfig?.clientName ?? "MCP\xB2";
8467
+ let callbackPort;
8468
+ let clientName;
8469
+ try {
8470
+ ({ callbackPort, clientName } = resolveOAuthProviderOptions(sseConfig.sse.auth));
8471
+ } catch (err) {
8472
+ const message = err instanceof Error ? err.message : String(err);
8473
+ console.error(`Error: Invalid OAuth configuration for '${targetName}': ${message}`);
8474
+ process.exit(1);
8475
+ }
8049
8476
  const authProvider = new McpOAuthProvider(targetName, tokenStorage, {
8050
8477
  callbackPort,
8051
8478
  clientName
@@ -8303,8 +8730,21 @@ function augmentProcessInfo(entries) {
8303
8730
  function resolveLauncherHint() {
8304
8731
  return process.env["MCP_SQUARED_LAUNCHER"] ?? process.env["MCP_CLIENT_NAME"] ?? process.env["MCP_SQUARED_AGENT"] ?? undefined;
8305
8732
  }
8733
+ function resolveDaemonSharedSecret(cliValue) {
8734
+ const value = cliValue ?? process.env["MCP_SQUARED_DAEMON_SECRET"];
8735
+ if (!value) {
8736
+ return;
8737
+ }
8738
+ const trimmed = value.trim();
8739
+ if (trimmed.length === 0) {
8740
+ return;
8741
+ }
8742
+ return trimmed;
8743
+ }
8306
8744
  async function runDaemon(options) {
8307
8745
  const { config, path: configPath } = await loadConfig();
8746
+ logSecurityProfile(config);
8747
+ logSearchModeProfile(config);
8308
8748
  const configHash = computeConfigHash(config);
8309
8749
  const monitorSocketPath = getSocketFilePath(configHash);
8310
8750
  const runtime = new McpSquaredServer({
@@ -8321,6 +8761,10 @@ async function runDaemon(options) {
8321
8761
  if (options.socketPath) {
8322
8762
  daemonOptions.socketPath = options.socketPath;
8323
8763
  }
8764
+ const sharedSecret = resolveDaemonSharedSecret(options.sharedSecret);
8765
+ if (sharedSecret) {
8766
+ daemonOptions.sharedSecret = sharedSecret;
8767
+ }
8324
8768
  const daemon = new DaemonServer(daemonOptions);
8325
8769
  ensureInstanceRegistryDir();
8326
8770
  ensureSocketDir();
@@ -8441,6 +8885,10 @@ async function runProxyCommand(options) {
8441
8885
  if (options.socketPath) {
8442
8886
  proxyOptions.endpoint = options.socketPath;
8443
8887
  }
8888
+ const sharedSecret = resolveDaemonSharedSecret(options.sharedSecret);
8889
+ if (sharedSecret) {
8890
+ proxyOptions.sharedSecret = sharedSecret;
8891
+ }
8444
8892
  try {
8445
8893
  proxyHandle = await runProxy(proxyOptions);
8446
8894
  } catch (error) {
@@ -8490,6 +8938,11 @@ async function main(argv = process.argv.slice(2)) {
8490
8938
  case "install":
8491
8939
  await runInstall(args.install);
8492
8940
  break;
8941
+ case "init": {
8942
+ const { runInit: runInit2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
8943
+ await runInit2(args.init);
8944
+ break;
8945
+ }
8493
8946
  case "monitor":
8494
8947
  await runMonitor(args.monitor);
8495
8948
  break;
@@ -8519,5 +8972,7 @@ if (import.meta.main) {
8519
8972
  }
8520
8973
  export {
8521
8974
  main,
8975
+ logSecurityProfile,
8976
+ logSearchModeProfile,
8522
8977
  VERSION
8523
8978
  };