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/README.md +11 -2
- package/dist/index.js +669 -214
- package/package.json +9 -6
- package/dist/highlights-eq9cgrbb.scm +0 -604
- package/dist/highlights-ghv9g403.scm +0 -205
- package/dist/highlights-hk7bwhj4.scm +0 -284
- package/dist/highlights-r812a2qc.scm +0 -150
- package/dist/highlights-x6tmsnaa.scm +0 -115
- package/dist/injections-73j83es3.scm +0 -27
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
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: (
|
|
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: [
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
4184
|
-
var
|
|
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 ??
|
|
4197
|
-
this._clientName = options.clientName ??
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
6705
|
-
|
|
6706
|
-
|
|
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 =
|
|
7145
|
+
const authOptions = resolveOAuthProviderOptions(sseConfig.sse.auth);
|
|
6764
7146
|
authProvider = new McpOAuthProvider(name, tokenStorage, authOptions);
|
|
6765
7147
|
}
|
|
6766
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
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
|
};
|