mcp-squared 0.3.3 → 0.4.0
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 +29 -0
- package/dist/index.js +252 -33
- package/dist/tui/config.js +259 -132
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -72,6 +72,9 @@ bun run build
|
|
|
72
72
|
# Run tests
|
|
73
73
|
bun test
|
|
74
74
|
|
|
75
|
+
# Evaluate tool-routing behavior (discovery-first quality check)
|
|
76
|
+
bun run eval:routing
|
|
77
|
+
|
|
75
78
|
# Dependency audit
|
|
76
79
|
bun run audit
|
|
77
80
|
```
|
|
@@ -103,6 +106,7 @@ mcp-squared config # Launch configuration TUI
|
|
|
103
106
|
mcp-squared test [upstream] # Test upstream server connections
|
|
104
107
|
mcp-squared auth <upstream> # OAuth auth for SSE/HTTP upstreams
|
|
105
108
|
mcp-squared import # Import MCP configs from other tools
|
|
109
|
+
mcp-squared migrate # Apply one-time config migrations
|
|
106
110
|
mcp-squared install # Install MCP² into other MCP clients
|
|
107
111
|
mcp-squared monitor # Launch server monitor TUI
|
|
108
112
|
mcp-squared --help # Full command reference
|
|
@@ -159,6 +163,23 @@ auth = true
|
|
|
159
163
|
|
|
160
164
|
Security policies (allow/block/confirm) live under `security.tools`. Confirmation flows return a short-lived token that must be provided to `execute` to proceed. OAuth tokens for SSE upstreams are stored under `~/.config/mcp-squared/tokens/<upstream>.json`.
|
|
161
165
|
|
|
166
|
+
`mcp-squared init` now seeds code-search routing preferences so `find_tools` prioritizes common code indexers by default:
|
|
167
|
+
|
|
168
|
+
```toml
|
|
169
|
+
[operations.findTools.preferredNamespacesByIntent]
|
|
170
|
+
codeSearch = ["auggie", "ctxdb"]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Tune this list (or set it to `[]`) if your environment uses different namespaces.
|
|
174
|
+
|
|
175
|
+
For existing configs created before this default, run:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
mcp-squared migrate
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Use `mcp-squared migrate --dry-run` to preview without writing.
|
|
182
|
+
|
|
162
183
|
## Tool API (Meta-Tools)
|
|
163
184
|
|
|
164
185
|
MCP² exposes these tools to MCP clients:
|
|
@@ -168,6 +189,14 @@ MCP² exposes these tools to MCP clients:
|
|
|
168
189
|
- `list_namespaces` - List upstream namespaces (optionally with tool names)
|
|
169
190
|
- `clear_selection_cache` - Reset co-occurrence based suggestions
|
|
170
191
|
|
|
192
|
+
Recommended workflow for LLM clients:
|
|
193
|
+
1. Call `find_tools` first to discover candidate tools for the task.
|
|
194
|
+
2. Call `describe_tools` for selected candidates to confirm exact argument schemas.
|
|
195
|
+
3. Call `execute` with a qualified tool name (`namespace:tool_name`).
|
|
196
|
+
4. Use `list_namespaces` if tool names are ambiguous or you need namespace context.
|
|
197
|
+
|
|
198
|
+
For codebase-search tasks, `find_tools` applies intent-aware ranking and may return namespace guidance (for example preferring configured code-search namespaces like `auggie`).
|
|
199
|
+
|
|
171
200
|
## Search Modes
|
|
172
201
|
|
|
173
202
|
`find_tools` supports three search modes:
|
package/dist/index.js
CHANGED
|
@@ -354,6 +354,11 @@ maxLimit = 50
|
|
|
354
354
|
defaultMode = "fast"
|
|
355
355
|
defaultDetailLevel = "L1"
|
|
356
356
|
|
|
357
|
+
[operations.findTools.preferredNamespacesByIntent]
|
|
358
|
+
# Default-on code-search namespace preference.
|
|
359
|
+
# Adjust or clear if your stack uses different code indexers.
|
|
360
|
+
codeSearch = ["auggie", "ctxdb"]
|
|
361
|
+
|
|
357
362
|
[operations.embeddings]
|
|
358
363
|
# Enable to use semantic or hybrid search modes.
|
|
359
364
|
# Requires onnxruntime shared library on the system.
|
|
@@ -455,6 +460,9 @@ function parseArgs(args) {
|
|
|
455
460
|
project: false,
|
|
456
461
|
force: false
|
|
457
462
|
},
|
|
463
|
+
migrate: {
|
|
464
|
+
dryRun: false
|
|
465
|
+
},
|
|
458
466
|
install: {
|
|
459
467
|
interactive: true,
|
|
460
468
|
dryRun: false,
|
|
@@ -511,6 +519,9 @@ function parseArgs(args) {
|
|
|
511
519
|
case "init":
|
|
512
520
|
result.mode = "init";
|
|
513
521
|
break;
|
|
522
|
+
case "migrate":
|
|
523
|
+
result.mode = "migrate";
|
|
524
|
+
break;
|
|
514
525
|
case "monitor":
|
|
515
526
|
result.mode = "monitor";
|
|
516
527
|
break;
|
|
@@ -561,6 +572,7 @@ function parseArgs(args) {
|
|
|
561
572
|
case "--dry-run":
|
|
562
573
|
result.import.dryRun = true;
|
|
563
574
|
result.install.dryRun = true;
|
|
575
|
+
result.migrate.dryRun = true;
|
|
564
576
|
break;
|
|
565
577
|
case "--no-interactive":
|
|
566
578
|
result.import.interactive = false;
|
|
@@ -685,6 +697,7 @@ Usage:
|
|
|
685
697
|
mcp-squared auth <upstream> Authenticate with an OAuth-protected upstream
|
|
686
698
|
mcp-squared import [options] Import MCP configs from other tools
|
|
687
699
|
mcp-squared init [options] Generate a starter config file with security profile
|
|
700
|
+
mcp-squared migrate [options] Apply config migrations to existing config
|
|
688
701
|
mcp-squared install [options] Install MCP\xB2 into other MCP clients
|
|
689
702
|
mcp-squared monitor [options] Launch server monitor TUI
|
|
690
703
|
mcp-squared daemon [options] Start shared MCP\xB2 daemon
|
|
@@ -698,6 +711,7 @@ Commands:
|
|
|
698
711
|
auth <name> Authenticate with an OAuth-protected upstream
|
|
699
712
|
import Import MCP server configs from other tools
|
|
700
713
|
init Generate a starter config with security profile
|
|
714
|
+
migrate Apply one-time config migrations to existing config
|
|
701
715
|
install Install MCP\xB2 as a server in other MCP clients
|
|
702
716
|
monitor Launch server monitor TUI
|
|
703
717
|
daemon Start shared daemon for multiple clients
|
|
@@ -724,6 +738,9 @@ Init Options:
|
|
|
724
738
|
--project Write to project-local mcp-squared.toml (default: user-level)
|
|
725
739
|
--force Overwrite existing config without prompting
|
|
726
740
|
|
|
741
|
+
Migrate Options:
|
|
742
|
+
--dry-run Preview migration changes without writing
|
|
743
|
+
|
|
727
744
|
Install Options:
|
|
728
745
|
--tool=<tool> Target tool (skip selection prompt)
|
|
729
746
|
--scope=<scope> Scope: user or project
|
|
@@ -760,6 +777,8 @@ Examples:
|
|
|
760
777
|
mcp-squared init Generate hardened config (confirm-all by default)
|
|
761
778
|
mcp-squared init --security=permissive Generate permissive config (allow-all)
|
|
762
779
|
mcp-squared init --project Generate project-local config
|
|
780
|
+
mcp-squared migrate Apply config migrations to current config file
|
|
781
|
+
mcp-squared migrate --dry-run Preview config migrations without writing
|
|
763
782
|
mcp-squared import --list List all discovered MCP configs
|
|
764
783
|
mcp-squared import --dry-run Preview import changes
|
|
765
784
|
mcp-squared import Import with interactive conflict resolution
|
|
@@ -1076,11 +1095,17 @@ var SecuritySchema = z.object({
|
|
|
1076
1095
|
});
|
|
1077
1096
|
var SearchModeSchema = z.enum(["fast", "semantic", "hybrid"]);
|
|
1078
1097
|
var DetailLevelSchema = z.enum(["L0", "L1", "L2"]);
|
|
1098
|
+
var PreferredNamespacesByIntentSchema = z.object({
|
|
1099
|
+
codeSearch: z.array(z.string().min(1)).default([])
|
|
1100
|
+
});
|
|
1079
1101
|
var FindToolsSchema = z.object({
|
|
1080
1102
|
defaultLimit: z.number().int().min(1).default(5),
|
|
1081
1103
|
maxLimit: z.number().int().min(1).max(200).default(50),
|
|
1082
1104
|
defaultMode: SearchModeSchema.default("fast"),
|
|
1083
|
-
defaultDetailLevel: DetailLevelSchema.default("L1")
|
|
1105
|
+
defaultDetailLevel: DetailLevelSchema.default("L1"),
|
|
1106
|
+
preferredNamespacesByIntent: PreferredNamespacesByIntentSchema.default({
|
|
1107
|
+
codeSearch: []
|
|
1108
|
+
})
|
|
1084
1109
|
});
|
|
1085
1110
|
var IndexSchema = z.object({
|
|
1086
1111
|
refreshIntervalMs: z.number().int().min(1000).default(30000)
|
|
@@ -1101,7 +1126,8 @@ var OperationsSchema = z.object({
|
|
|
1101
1126
|
defaultLimit: 5,
|
|
1102
1127
|
maxLimit: 50,
|
|
1103
1128
|
defaultMode: "fast",
|
|
1104
|
-
defaultDetailLevel: "L1"
|
|
1129
|
+
defaultDetailLevel: "L1",
|
|
1130
|
+
preferredNamespacesByIntent: { codeSearch: [] }
|
|
1105
1131
|
}),
|
|
1106
1132
|
index: IndexSchema.default({ refreshIntervalMs: 30000 }),
|
|
1107
1133
|
logging: LoggingSchema.default({ level: "info" }),
|
|
@@ -1116,7 +1142,8 @@ var OperationsSchema = z.object({
|
|
|
1116
1142
|
defaultLimit: 5,
|
|
1117
1143
|
maxLimit: 50,
|
|
1118
1144
|
defaultMode: "fast",
|
|
1119
|
-
defaultDetailLevel: "L1"
|
|
1145
|
+
defaultDetailLevel: "L1",
|
|
1146
|
+
preferredNamespacesByIntent: { codeSearch: [] }
|
|
1120
1147
|
},
|
|
1121
1148
|
index: { refreshIntervalMs: 30000 },
|
|
1122
1149
|
logging: { level: "info" },
|
|
@@ -1872,18 +1899,32 @@ function readManifestFile(manifestUrl) {
|
|
|
1872
1899
|
}
|
|
1873
1900
|
function readBundledManifestFile() {
|
|
1874
1901
|
const require2 = createRequire(import.meta.url);
|
|
1875
|
-
|
|
1902
|
+
const candidates = ["../package.json", "../../package.json"];
|
|
1903
|
+
for (const candidate of candidates) {
|
|
1904
|
+
try {
|
|
1905
|
+
return require2(candidate);
|
|
1906
|
+
} catch {}
|
|
1907
|
+
}
|
|
1908
|
+
throw new Error("Unable to resolve bundled package.json");
|
|
1909
|
+
}
|
|
1910
|
+
function getDefaultManifestUrls() {
|
|
1911
|
+
return [
|
|
1912
|
+
new URL("../package.json", import.meta.url),
|
|
1913
|
+
new URL("../../package.json", import.meta.url)
|
|
1914
|
+
];
|
|
1876
1915
|
}
|
|
1877
1916
|
function resolveVersion(options = {}) {
|
|
1878
1917
|
const readManifest = options.readManifest ?? readManifestFile;
|
|
1879
|
-
const
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1918
|
+
const manifestUrls = options.manifestUrls ? [...options.manifestUrls] : options.manifestUrl ? [options.manifestUrl] : getDefaultManifestUrls();
|
|
1919
|
+
for (const manifestUrl of manifestUrls) {
|
|
1920
|
+
try {
|
|
1921
|
+
const manifest = readManifest(manifestUrl);
|
|
1922
|
+
const manifestVersion = normalizeVersion(manifest.version);
|
|
1923
|
+
if (manifestVersion) {
|
|
1924
|
+
return manifestVersion;
|
|
1925
|
+
}
|
|
1926
|
+
} catch {}
|
|
1927
|
+
}
|
|
1887
1928
|
const envVersion = normalizeVersion((options.env ?? process.env)["npm_package_version"]);
|
|
1888
1929
|
if (envVersion) {
|
|
1889
1930
|
return envVersion;
|
|
@@ -4263,6 +4304,81 @@ Installation cancelled.`);
|
|
|
4263
4304
|
process.exit(0);
|
|
4264
4305
|
}
|
|
4265
4306
|
|
|
4307
|
+
// src/migrate/runner.ts
|
|
4308
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
4309
|
+
import { parse as parseToml4 } from "smol-toml";
|
|
4310
|
+
var DEFAULT_CODE_SEARCH_NAMESPACES = ["auggie", "ctxdb"];
|
|
4311
|
+
function isRecord(value) {
|
|
4312
|
+
return typeof value === "object" && value !== null;
|
|
4313
|
+
}
|
|
4314
|
+
function hasOwn(obj, key) {
|
|
4315
|
+
return Object.hasOwn(obj, key);
|
|
4316
|
+
}
|
|
4317
|
+
function isCodeSearchExplicitlyConfigured(rawConfig) {
|
|
4318
|
+
if (!isRecord(rawConfig)) {
|
|
4319
|
+
return false;
|
|
4320
|
+
}
|
|
4321
|
+
const operations = rawConfig["operations"];
|
|
4322
|
+
if (!isRecord(operations)) {
|
|
4323
|
+
return false;
|
|
4324
|
+
}
|
|
4325
|
+
const findTools = operations["findTools"];
|
|
4326
|
+
if (!isRecord(findTools)) {
|
|
4327
|
+
return false;
|
|
4328
|
+
}
|
|
4329
|
+
const preferredNamespacesByIntent = findTools["preferredNamespacesByIntent"];
|
|
4330
|
+
if (!isRecord(preferredNamespacesByIntent)) {
|
|
4331
|
+
return false;
|
|
4332
|
+
}
|
|
4333
|
+
return hasOwn(preferredNamespacesByIntent, "codeSearch");
|
|
4334
|
+
}
|
|
4335
|
+
function applyCodeSearchPreferenceMigration(config, options) {
|
|
4336
|
+
if (options.codeSearchExplicitlyConfigured) {
|
|
4337
|
+
return { config, changed: false };
|
|
4338
|
+
}
|
|
4339
|
+
return {
|
|
4340
|
+
changed: true,
|
|
4341
|
+
config: {
|
|
4342
|
+
...config,
|
|
4343
|
+
operations: {
|
|
4344
|
+
...config.operations,
|
|
4345
|
+
findTools: {
|
|
4346
|
+
...config.operations.findTools,
|
|
4347
|
+
preferredNamespacesByIntent: {
|
|
4348
|
+
...config.operations.findTools.preferredNamespacesByIntent,
|
|
4349
|
+
codeSearch: [...DEFAULT_CODE_SEARCH_NAMESPACES]
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
};
|
|
4355
|
+
}
|
|
4356
|
+
async function runMigrate(args) {
|
|
4357
|
+
const discovered = discoverConfigPath();
|
|
4358
|
+
if (!discovered) {
|
|
4359
|
+
console.error("No configuration file found. Run 'mcp-squared init' to create one first.");
|
|
4360
|
+
process.exit(1);
|
|
4361
|
+
}
|
|
4362
|
+
const loaded = await loadConfigFromPath(discovered.path, discovered.source);
|
|
4363
|
+
const rawConfig = parseToml4(readFileSync5(discovered.path, "utf-8"));
|
|
4364
|
+
const codeSearchExplicitlyConfigured = isCodeSearchExplicitlyConfigured(rawConfig);
|
|
4365
|
+
const { config: migratedConfig, changed } = applyCodeSearchPreferenceMigration(loaded.config, {
|
|
4366
|
+
codeSearchExplicitlyConfigured
|
|
4367
|
+
});
|
|
4368
|
+
if (!changed) {
|
|
4369
|
+
console.log(`No migration needed: code-search preferences already configured in ${discovered.path}`);
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
if (args.dryRun) {
|
|
4373
|
+
console.log(`[dry-run] Would update ${discovered.path}`);
|
|
4374
|
+
console.log('[dry-run] Set operations.findTools.preferredNamespacesByIntent.codeSearch = ["auggie", "ctxdb"]');
|
|
4375
|
+
return;
|
|
4376
|
+
}
|
|
4377
|
+
await saveConfig(discovered.path, migratedConfig);
|
|
4378
|
+
console.log(`Updated ${discovered.path}`);
|
|
4379
|
+
console.log('Set operations.findTools.preferredNamespacesByIntent.codeSearch = ["auggie", "ctxdb"]');
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4266
4382
|
// src/oauth/browser.ts
|
|
4267
4383
|
import { spawn as spawn2 } from "child_process";
|
|
4268
4384
|
async function openBrowser(url) {
|
|
@@ -4650,7 +4766,7 @@ import {
|
|
|
4650
4766
|
chmodSync,
|
|
4651
4767
|
existsSync as existsSync8,
|
|
4652
4768
|
mkdirSync as mkdirSync5,
|
|
4653
|
-
readFileSync as
|
|
4769
|
+
readFileSync as readFileSync6,
|
|
4654
4770
|
unlinkSync as unlinkSync4,
|
|
4655
4771
|
writeFileSync as writeFileSync4
|
|
4656
4772
|
} from "fs";
|
|
@@ -4688,7 +4804,7 @@ class TokenStorage {
|
|
|
4688
4804
|
return;
|
|
4689
4805
|
}
|
|
4690
4806
|
try {
|
|
4691
|
-
const content =
|
|
4807
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
4692
4808
|
return JSON.parse(content);
|
|
4693
4809
|
} catch {
|
|
4694
4810
|
return;
|
|
@@ -4876,6 +4992,15 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
4876
4992
|
import { StdioServerTransport as StdioServerTransport2 } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4877
4993
|
import { z as z3 } from "zod";
|
|
4878
4994
|
|
|
4995
|
+
// agent_safety_kit/guard/errors.ts
|
|
4996
|
+
class PolicyDenied extends Error {
|
|
4997
|
+
decision;
|
|
4998
|
+
constructor(message, decision) {
|
|
4999
|
+
super(message);
|
|
5000
|
+
this.name = "PolicyDenied";
|
|
5001
|
+
this.decision = decision;
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
4879
5004
|
// agent_safety_kit/policy/matchers.ts
|
|
4880
5005
|
var GLOB_SPECIALS = /[.+^${}()|[\]\\]/g;
|
|
4881
5006
|
var PATH_KEY_RE = /(path|file|dir|cwd|workspace|root)/i;
|
|
@@ -5013,16 +5138,6 @@ function valuesConstrainedByGlob(values, allowlist) {
|
|
|
5013
5138
|
return values.every((value) => matchesAnyGlob(allowlist, value));
|
|
5014
5139
|
}
|
|
5015
5140
|
|
|
5016
|
-
// agent_safety_kit/guard/errors.ts
|
|
5017
|
-
class PolicyDenied extends Error {
|
|
5018
|
-
decision;
|
|
5019
|
-
constructor(message, decision) {
|
|
5020
|
-
super(message);
|
|
5021
|
-
this.name = "PolicyDenied";
|
|
5022
|
-
this.decision = decision;
|
|
5023
|
-
}
|
|
5024
|
-
}
|
|
5025
|
-
|
|
5026
5141
|
// agent_safety_kit/guard/ratelimit.ts
|
|
5027
5142
|
class SlidingWindowRateLimiter {
|
|
5028
5143
|
requests = new Map;
|
|
@@ -5232,7 +5347,7 @@ class Guard {
|
|
|
5232
5347
|
}
|
|
5233
5348
|
}
|
|
5234
5349
|
// agent_safety_kit/policy/load.ts
|
|
5235
|
-
import { existsSync as existsSync9, readFileSync as
|
|
5350
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
5236
5351
|
import { resolve as resolve3 } from "path";
|
|
5237
5352
|
import { parse as parseYaml } from "yaml";
|
|
5238
5353
|
|
|
@@ -5295,7 +5410,7 @@ function load_policy(options = {}) {
|
|
|
5295
5410
|
if (!existsSync9(sourcePath)) {
|
|
5296
5411
|
throw new Error(`Agent safety policy file not found at ${sourcePath}`);
|
|
5297
5412
|
}
|
|
5298
|
-
const raw =
|
|
5413
|
+
const raw = readFileSync7(sourcePath, "utf8");
|
|
5299
5414
|
const parsed = parseYaml(raw);
|
|
5300
5415
|
const policy = AgentPolicySchema.parse(parsed);
|
|
5301
5416
|
const playbookName = options.playbook ?? envConfig.playbook;
|
|
@@ -5315,6 +5430,7 @@ function load_policy(options = {}) {
|
|
|
5315
5430
|
rules: playbook.rules
|
|
5316
5431
|
};
|
|
5317
5432
|
}
|
|
5433
|
+
|
|
5318
5434
|
// agent_safety_kit/observability/sinks/null.ts
|
|
5319
5435
|
class NullSpan {
|
|
5320
5436
|
setAttributes(_attributes) {}
|
|
@@ -7753,11 +7869,80 @@ class McpSquaredServer {
|
|
|
7753
7869
|
this.registerMetaTools(this.mcpServer);
|
|
7754
7870
|
}
|
|
7755
7871
|
createMcpServer(name, version) {
|
|
7756
|
-
return new McpServer({
|
|
7872
|
+
return new McpServer({
|
|
7873
|
+
name,
|
|
7874
|
+
version,
|
|
7875
|
+
title: "MCP\xB2 Meta Router",
|
|
7876
|
+
description: "Discover and execute tools across upstream MCP servers through a compact meta-tool interface."
|
|
7877
|
+
}, {
|
|
7757
7878
|
capabilities: {
|
|
7758
7879
|
tools: {}
|
|
7880
|
+
},
|
|
7881
|
+
instructions: this.buildServerInstructions()
|
|
7882
|
+
});
|
|
7883
|
+
}
|
|
7884
|
+
buildServerInstructions() {
|
|
7885
|
+
const codeSearchNamespaces = this.getCodeSearchNamespaces();
|
|
7886
|
+
const codeSearchHint = codeSearchNamespaces.length > 0 ? ` Prefer these configured code-search namespaces when relevant: ${codeSearchNamespaces.join(", ")}.` : "";
|
|
7887
|
+
return [
|
|
7888
|
+
"Use discovery-first workflow: call `find_tools` before defaulting to local shell discovery (for example `grep`/`rg`).",
|
|
7889
|
+
`For codebase lookup tasks, start with \`find_tools\` queries such as "code search", "find symbol", or "references".${codeSearchHint}`,
|
|
7890
|
+
"After choosing a candidate, call `describe_tools` to confirm schema, then call `execute` with a qualified tool name (`namespace:tool_name`).",
|
|
7891
|
+
"If capabilities are unclear or a name is ambiguous, call `list_namespaces`."
|
|
7892
|
+
].join(" ");
|
|
7893
|
+
}
|
|
7894
|
+
getCodeSearchNamespaces() {
|
|
7895
|
+
const configured = this.config.operations.findTools.preferredNamespacesByIntent.codeSearch;
|
|
7896
|
+
const upstreamKeys = Object.keys(this.config.upstreams);
|
|
7897
|
+
if (configured.length > 0) {
|
|
7898
|
+
const deduped = [...new Set(configured)];
|
|
7899
|
+
const present = deduped.filter((ns) => upstreamKeys.includes(ns));
|
|
7900
|
+
return present.length > 0 ? present : deduped;
|
|
7901
|
+
}
|
|
7902
|
+
return upstreamKeys.filter((key) => /(auggie|augment|code|source|repo|search)/i.test(key));
|
|
7903
|
+
}
|
|
7904
|
+
isCodeSearchQuery(query) {
|
|
7905
|
+
return /\b(codebase|source code|repository|repo)\b/i.test(query) || /\b(code search|search code|find symbol|symbol lookup|definition|references?|usages?)\b/i.test(query);
|
|
7906
|
+
}
|
|
7907
|
+
rankToolsForQuery(tools, query) {
|
|
7908
|
+
if (!this.isCodeSearchQuery(query) || tools.length < 2) {
|
|
7909
|
+
return tools;
|
|
7910
|
+
}
|
|
7911
|
+
const preferredNamespaces = new Set(this.getCodeSearchNamespaces());
|
|
7912
|
+
const scored = tools.map((tool, index) => {
|
|
7913
|
+
const haystack = `${tool.serverKey} ${tool.name} ${tool.description ?? ""}`.toLowerCase();
|
|
7914
|
+
let score = 0;
|
|
7915
|
+
if (preferredNamespaces.has(tool.serverKey))
|
|
7916
|
+
score += 100;
|
|
7917
|
+
if (/(auggie|augment)/i.test(tool.serverKey))
|
|
7918
|
+
score += 40;
|
|
7919
|
+
if (/\b(search|find|query|lookup)\b/.test(haystack))
|
|
7920
|
+
score += 15;
|
|
7921
|
+
if (/\b(code|symbol|definition|reference|repo|repository|source)\b/.test(haystack)) {
|
|
7922
|
+
score += 20;
|
|
7923
|
+
}
|
|
7924
|
+
return { tool, index, score };
|
|
7925
|
+
});
|
|
7926
|
+
scored.sort((a, b) => {
|
|
7927
|
+
if (a.score === b.score) {
|
|
7928
|
+
return a.index - b.index;
|
|
7759
7929
|
}
|
|
7930
|
+
return b.score - a.score;
|
|
7760
7931
|
});
|
|
7932
|
+
return scored.map((entry) => entry.tool);
|
|
7933
|
+
}
|
|
7934
|
+
buildFindToolsGuidance(query) {
|
|
7935
|
+
const guidance = {
|
|
7936
|
+
nextStep: "Use describe_tools for selected candidates, then execute with a qualified name."
|
|
7937
|
+
};
|
|
7938
|
+
if (this.isCodeSearchQuery(query)) {
|
|
7939
|
+
const preferredNamespaces = this.getCodeSearchNamespaces();
|
|
7940
|
+
if (preferredNamespaces.length > 0) {
|
|
7941
|
+
guidance.preferredNamespaces = preferredNamespaces;
|
|
7942
|
+
guidance.note = "For codebase exploration, prefer these namespaces before local shell grep/rg.";
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7945
|
+
return guidance;
|
|
7761
7946
|
}
|
|
7762
7947
|
createSessionServer() {
|
|
7763
7948
|
const server = this.createMcpServer(this.serverName, this.serverVersion);
|
|
@@ -7783,9 +7968,14 @@ class McpSquaredServer {
|
|
|
7783
7968
|
}
|
|
7784
7969
|
registerMetaTools(server) {
|
|
7785
7970
|
server.registerTool("find_tools", {
|
|
7786
|
-
|
|
7971
|
+
title: "Discover Upstream Tools",
|
|
7972
|
+
description: "Call this first for capability discovery. Search available tools across all connected upstream MCP servers and return ranked tool summaries matching the query.",
|
|
7973
|
+
annotations: {
|
|
7974
|
+
readOnlyHint: true,
|
|
7975
|
+
openWorldHint: false
|
|
7976
|
+
},
|
|
7787
7977
|
inputSchema: {
|
|
7788
|
-
query: z3.string().describe(
|
|
7978
|
+
query: z3.string().describe('Natural language query describing the task (for example: "code search", "find symbol", "create issue")'),
|
|
7789
7979
|
limit: z3.number().int().min(1).max(this.maxLimit).default(this.config.operations.findTools.defaultLimit).describe("Maximum number of results to return"),
|
|
7790
7980
|
mode: SearchModeSchema.optional().describe('Search mode: "fast" (FTS5), "semantic" (embeddings), or "hybrid" (FTS5 + rerank)'),
|
|
7791
7981
|
detail_level: DetailLevelSchema.optional().describe('Level of detail: "L0" (name only), "L1" (summary with description, default), "L2" (full schema)')
|
|
@@ -7800,8 +7990,10 @@ class McpSquaredServer {
|
|
|
7800
7990
|
mode: args.mode
|
|
7801
7991
|
});
|
|
7802
7992
|
const filteredTools = this.filterToolsByPolicy(result.tools);
|
|
7993
|
+
const rankedTools = this.rankToolsForQuery(filteredTools, args.query);
|
|
7803
7994
|
const detailLevel = args.detail_level ?? this.config.operations.findTools.defaultDetailLevel;
|
|
7804
|
-
const tools = this.formatToolsForDetailLevel(
|
|
7995
|
+
const tools = this.formatToolsForDetailLevel(rankedTools, detailLevel);
|
|
7996
|
+
const guidance = this.buildFindToolsGuidance(args.query);
|
|
7805
7997
|
const selectionCacheConfig = this.config.operations.selectionCache;
|
|
7806
7998
|
let suggestedTools;
|
|
7807
7999
|
if (selectionCacheConfig.enabled && selectionCacheConfig.maxBundleSuggestions > 0) {
|
|
@@ -7826,6 +8018,7 @@ class McpSquaredServer {
|
|
|
7826
8018
|
searchMode: result.searchMode,
|
|
7827
8019
|
embeddingsAvailable: this.retriever.hasEmbeddings(),
|
|
7828
8020
|
tools,
|
|
8021
|
+
guidance,
|
|
7829
8022
|
...suggestedTools && { suggestedTools }
|
|
7830
8023
|
})
|
|
7831
8024
|
}
|
|
@@ -7837,7 +8030,12 @@ class McpSquaredServer {
|
|
|
7837
8030
|
}
|
|
7838
8031
|
}));
|
|
7839
8032
|
server.registerTool("describe_tools", {
|
|
7840
|
-
|
|
8033
|
+
title: "Inspect Tool Schemas",
|
|
8034
|
+
description: "After find_tools, fetch full JSON schemas for selected tools to validate required arguments before execution.",
|
|
8035
|
+
annotations: {
|
|
8036
|
+
readOnlyHint: true,
|
|
8037
|
+
openWorldHint: false
|
|
8038
|
+
},
|
|
7841
8039
|
inputSchema: {
|
|
7842
8040
|
tool_names: z3.array(z3.string()).min(1).max(20).describe("List of tool names to get schemas for")
|
|
7843
8041
|
}
|
|
@@ -7883,7 +8081,13 @@ class McpSquaredServer {
|
|
|
7883
8081
|
}
|
|
7884
8082
|
}));
|
|
7885
8083
|
server.registerTool("execute", {
|
|
7886
|
-
|
|
8084
|
+
title: "Execute Upstream Tool",
|
|
8085
|
+
description: "Execute an upstream tool after find_tools/describe_tools selection. The tool must exist and the arguments must match its schema.",
|
|
8086
|
+
annotations: {
|
|
8087
|
+
readOnlyHint: false,
|
|
8088
|
+
destructiveHint: false,
|
|
8089
|
+
openWorldHint: true
|
|
8090
|
+
},
|
|
7887
8091
|
inputSchema: {
|
|
7888
8092
|
tool_name: z3.string().describe("Name of the tool to execute"),
|
|
7889
8093
|
arguments: z3.record(z3.string(), z3.unknown()).default({}).describe("Arguments to pass to the tool"),
|
|
@@ -8014,7 +8218,14 @@ class McpSquaredServer {
|
|
|
8014
8218
|
}
|
|
8015
8219
|
}));
|
|
8016
8220
|
server.registerTool("clear_selection_cache", {
|
|
8221
|
+
title: "Reset Selection Cache",
|
|
8017
8222
|
description: "Clears all learned tool co-occurrence patterns. Use this to reset the selection cache if suggestions become stale or irrelevant.",
|
|
8223
|
+
annotations: {
|
|
8224
|
+
readOnlyHint: false,
|
|
8225
|
+
destructiveHint: true,
|
|
8226
|
+
idempotentHint: true,
|
|
8227
|
+
openWorldHint: false
|
|
8228
|
+
},
|
|
8018
8229
|
inputSchema: {}
|
|
8019
8230
|
}, async () => this.runTaskSpan("clear_selection_cache", async () => {
|
|
8020
8231
|
const requestId = this.statsCollector.startRequest();
|
|
@@ -8042,7 +8253,12 @@ class McpSquaredServer {
|
|
|
8042
8253
|
}
|
|
8043
8254
|
}));
|
|
8044
8255
|
server.registerTool("list_namespaces", {
|
|
8045
|
-
|
|
8256
|
+
title: "List Upstream Namespaces",
|
|
8257
|
+
description: "List available namespaces (upstream MCP servers) and optional tool names. Use this to discover routing options and disambiguate qualified names (namespace:tool_name).",
|
|
8258
|
+
annotations: {
|
|
8259
|
+
readOnlyHint: true,
|
|
8260
|
+
openWorldHint: false
|
|
8261
|
+
},
|
|
8046
8262
|
inputSchema: {
|
|
8047
8263
|
include_tools: z3.boolean().default(false).describe("If true, includes the list of tool names available in each namespace")
|
|
8048
8264
|
}
|
|
@@ -8931,6 +9147,9 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
8931
9147
|
await runInit2(args.init);
|
|
8932
9148
|
break;
|
|
8933
9149
|
}
|
|
9150
|
+
case "migrate":
|
|
9151
|
+
await runMigrate(args.migrate);
|
|
9152
|
+
break;
|
|
8934
9153
|
case "monitor":
|
|
8935
9154
|
await runMonitor(args.monitor);
|
|
8936
9155
|
break;
|
package/dist/tui/config.js
CHANGED
|
@@ -434,11 +434,17 @@ var SecuritySchema = z.object({
|
|
|
434
434
|
});
|
|
435
435
|
var SearchModeSchema = z.enum(["fast", "semantic", "hybrid"]);
|
|
436
436
|
var DetailLevelSchema = z.enum(["L0", "L1", "L2"]);
|
|
437
|
+
var PreferredNamespacesByIntentSchema = z.object({
|
|
438
|
+
codeSearch: z.array(z.string().min(1)).default([])
|
|
439
|
+
});
|
|
437
440
|
var FindToolsSchema = z.object({
|
|
438
441
|
defaultLimit: z.number().int().min(1).default(5),
|
|
439
442
|
maxLimit: z.number().int().min(1).max(200).default(50),
|
|
440
443
|
defaultMode: SearchModeSchema.default("fast"),
|
|
441
|
-
defaultDetailLevel: DetailLevelSchema.default("L1")
|
|
444
|
+
defaultDetailLevel: DetailLevelSchema.default("L1"),
|
|
445
|
+
preferredNamespacesByIntent: PreferredNamespacesByIntentSchema.default({
|
|
446
|
+
codeSearch: []
|
|
447
|
+
})
|
|
442
448
|
});
|
|
443
449
|
var IndexSchema = z.object({
|
|
444
450
|
refreshIntervalMs: z.number().int().min(1000).default(30000)
|
|
@@ -459,7 +465,8 @@ var OperationsSchema = z.object({
|
|
|
459
465
|
defaultLimit: 5,
|
|
460
466
|
maxLimit: 50,
|
|
461
467
|
defaultMode: "fast",
|
|
462
|
-
defaultDetailLevel: "L1"
|
|
468
|
+
defaultDetailLevel: "L1",
|
|
469
|
+
preferredNamespacesByIntent: { codeSearch: [] }
|
|
463
470
|
}),
|
|
464
471
|
index: IndexSchema.default({ refreshIntervalMs: 30000 }),
|
|
465
472
|
logging: LoggingSchema.default({ level: "info" }),
|
|
@@ -474,7 +481,8 @@ var OperationsSchema = z.object({
|
|
|
474
481
|
defaultLimit: 5,
|
|
475
482
|
maxLimit: 50,
|
|
476
483
|
defaultMode: "fast",
|
|
477
|
-
defaultDetailLevel: "L1"
|
|
484
|
+
defaultDetailLevel: "L1",
|
|
485
|
+
preferredNamespacesByIntent: { codeSearch: [] }
|
|
478
486
|
},
|
|
479
487
|
index: { refreshIntervalMs: 30000 },
|
|
480
488
|
logging: { level: "info" },
|
|
@@ -1006,18 +1014,32 @@ function readManifestFile(manifestUrl) {
|
|
|
1006
1014
|
}
|
|
1007
1015
|
function readBundledManifestFile() {
|
|
1008
1016
|
const require2 = createRequire(import.meta.url);
|
|
1009
|
-
|
|
1017
|
+
const candidates = ["../package.json", "../../package.json"];
|
|
1018
|
+
for (const candidate of candidates) {
|
|
1019
|
+
try {
|
|
1020
|
+
return require2(candidate);
|
|
1021
|
+
} catch {}
|
|
1022
|
+
}
|
|
1023
|
+
throw new Error("Unable to resolve bundled package.json");
|
|
1024
|
+
}
|
|
1025
|
+
function getDefaultManifestUrls() {
|
|
1026
|
+
return [
|
|
1027
|
+
new URL("../package.json", import.meta.url),
|
|
1028
|
+
new URL("../../package.json", import.meta.url)
|
|
1029
|
+
];
|
|
1010
1030
|
}
|
|
1011
1031
|
function resolveVersion(options = {}) {
|
|
1012
1032
|
const readManifest = options.readManifest ?? readManifestFile;
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1033
|
+
const manifestUrls = options.manifestUrls ? [...options.manifestUrls] : options.manifestUrl ? [options.manifestUrl] : getDefaultManifestUrls();
|
|
1034
|
+
for (const manifestUrl of manifestUrls) {
|
|
1035
|
+
try {
|
|
1036
|
+
const manifest = readManifest(manifestUrl);
|
|
1037
|
+
const manifestVersion = normalizeVersion(manifest.version);
|
|
1038
|
+
if (manifestVersion) {
|
|
1039
|
+
return manifestVersion;
|
|
1040
|
+
}
|
|
1041
|
+
} catch {}
|
|
1042
|
+
}
|
|
1021
1043
|
const envVersion = normalizeVersion((options.env ?? process.env)["npm_package_version"]);
|
|
1022
1044
|
if (envVersion) {
|
|
1023
1045
|
return envVersion;
|
|
@@ -2241,6 +2263,132 @@ async function testUpstreamConnection(name, config, options = {}) {
|
|
|
2241
2263
|
log("Done");
|
|
2242
2264
|
}
|
|
2243
2265
|
}
|
|
2266
|
+
// src/tui/upstream-edit.ts
|
|
2267
|
+
function hasNameConflict(upstreams, nextName, existingName) {
|
|
2268
|
+
return Boolean(existingName && nextName !== existingName && typeof upstreams[nextName] !== "undefined");
|
|
2269
|
+
}
|
|
2270
|
+
function parseKeyValuePairsInput(input) {
|
|
2271
|
+
const result = {};
|
|
2272
|
+
if (!input.trim())
|
|
2273
|
+
return result;
|
|
2274
|
+
const pairs = input.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2275
|
+
for (const pair of pairs) {
|
|
2276
|
+
const eqIndex = pair.indexOf("=");
|
|
2277
|
+
if (eqIndex > 0) {
|
|
2278
|
+
const key = pair.substring(0, eqIndex).trim();
|
|
2279
|
+
const value = pair.substring(eqIndex + 1).trim();
|
|
2280
|
+
if (key) {
|
|
2281
|
+
result[key] = value;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
return result;
|
|
2286
|
+
}
|
|
2287
|
+
function stringifyKeyValuePairsInput(pairs) {
|
|
2288
|
+
const entries = Object.entries(pairs || {});
|
|
2289
|
+
if (entries.length === 0) {
|
|
2290
|
+
return "";
|
|
2291
|
+
}
|
|
2292
|
+
return entries.map(([key, value]) => `${key}=${value}`).join(", ");
|
|
2293
|
+
}
|
|
2294
|
+
function saveStdioUpstreamFromForm(input) {
|
|
2295
|
+
const trimmedName = input.name.trim();
|
|
2296
|
+
const trimmedCommand = input.commandLine.trim();
|
|
2297
|
+
if (!trimmedName) {
|
|
2298
|
+
return { ok: false, reason: "name_required" };
|
|
2299
|
+
}
|
|
2300
|
+
if (!trimmedCommand) {
|
|
2301
|
+
return { ok: false, reason: "command_required" };
|
|
2302
|
+
}
|
|
2303
|
+
if (hasNameConflict(input.upstreams, trimmedName, input.existingName)) {
|
|
2304
|
+
return { ok: false, reason: "name_conflict" };
|
|
2305
|
+
}
|
|
2306
|
+
const parts = trimmedCommand.split(/\s+/);
|
|
2307
|
+
const command = parts[0] || "";
|
|
2308
|
+
const args = parts.slice(1);
|
|
2309
|
+
const env = parseKeyValuePairsInput(input.envInput);
|
|
2310
|
+
if (input.existingName && input.existingName !== trimmedName) {
|
|
2311
|
+
delete input.upstreams[input.existingName];
|
|
2312
|
+
}
|
|
2313
|
+
input.upstreams[trimmedName] = {
|
|
2314
|
+
transport: "stdio",
|
|
2315
|
+
enabled: input.existingUpstream?.enabled ?? true,
|
|
2316
|
+
env,
|
|
2317
|
+
stdio: { command, args }
|
|
2318
|
+
};
|
|
2319
|
+
return { ok: true, savedName: trimmedName };
|
|
2320
|
+
}
|
|
2321
|
+
function saveSseUpstreamFromForm(input) {
|
|
2322
|
+
const trimmedName = input.name.trim();
|
|
2323
|
+
const trimmedUrl = input.url.trim();
|
|
2324
|
+
if (!trimmedName) {
|
|
2325
|
+
return { ok: false, reason: "name_required" };
|
|
2326
|
+
}
|
|
2327
|
+
if (!trimmedUrl) {
|
|
2328
|
+
return { ok: false, reason: "url_required" };
|
|
2329
|
+
}
|
|
2330
|
+
try {
|
|
2331
|
+
new URL(trimmedUrl);
|
|
2332
|
+
} catch {
|
|
2333
|
+
return { ok: false, reason: "invalid_url" };
|
|
2334
|
+
}
|
|
2335
|
+
if (hasNameConflict(input.upstreams, trimmedName, input.existingName)) {
|
|
2336
|
+
return { ok: false, reason: "name_conflict" };
|
|
2337
|
+
}
|
|
2338
|
+
const headers = parseKeyValuePairsInput(input.headersInput);
|
|
2339
|
+
const env = parseKeyValuePairsInput(input.envInput);
|
|
2340
|
+
if (input.existingName && input.existingName !== trimmedName) {
|
|
2341
|
+
delete input.upstreams[input.existingName];
|
|
2342
|
+
}
|
|
2343
|
+
input.upstreams[trimmedName] = {
|
|
2344
|
+
transport: "sse",
|
|
2345
|
+
enabled: input.existingUpstream?.enabled ?? true,
|
|
2346
|
+
env,
|
|
2347
|
+
sse: {
|
|
2348
|
+
url: trimmedUrl,
|
|
2349
|
+
headers,
|
|
2350
|
+
auth: input.authEnabled ? true : undefined
|
|
2351
|
+
}
|
|
2352
|
+
};
|
|
2353
|
+
return { ok: true, savedName: trimmedName };
|
|
2354
|
+
}
|
|
2355
|
+
function deleteUpstreamByName(upstreams, name) {
|
|
2356
|
+
if (typeof upstreams[name] === "undefined") {
|
|
2357
|
+
return false;
|
|
2358
|
+
}
|
|
2359
|
+
delete upstreams[name];
|
|
2360
|
+
return true;
|
|
2361
|
+
}
|
|
2362
|
+
function getUpstreamEditMenuOptions(upstream) {
|
|
2363
|
+
return [
|
|
2364
|
+
{
|
|
2365
|
+
name: "Edit Configuration",
|
|
2366
|
+
description: "Update connection details and environment",
|
|
2367
|
+
value: "edit"
|
|
2368
|
+
},
|
|
2369
|
+
{
|
|
2370
|
+
name: "Test Connection",
|
|
2371
|
+
description: "Connect and list available tools",
|
|
2372
|
+
value: "test"
|
|
2373
|
+
},
|
|
2374
|
+
{
|
|
2375
|
+
name: upstream.enabled ? "Disable" : "Enable",
|
|
2376
|
+
description: upstream.enabled ? "Stop using this upstream" : "Start using this upstream",
|
|
2377
|
+
value: "toggle"
|
|
2378
|
+
},
|
|
2379
|
+
{
|
|
2380
|
+
name: "Delete",
|
|
2381
|
+
description: "Remove this upstream configuration",
|
|
2382
|
+
value: "delete"
|
|
2383
|
+
},
|
|
2384
|
+
{
|
|
2385
|
+
name: "\u2190 Back",
|
|
2386
|
+
description: "",
|
|
2387
|
+
value: "back"
|
|
2388
|
+
}
|
|
2389
|
+
];
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2244
2392
|
// src/tui/config.ts
|
|
2245
2393
|
var PROJECT_DESCRIPTION = "Mercury Control Plane";
|
|
2246
2394
|
async function runConfigTui() {
|
|
@@ -2572,12 +2720,13 @@ class ConfigTuiApp {
|
|
|
2572
2720
|
menu.focus();
|
|
2573
2721
|
this.addInstructions("\u2191\u2193 Navigate | Enter Select | Esc Back");
|
|
2574
2722
|
}
|
|
2575
|
-
showAddStdioScreen() {
|
|
2723
|
+
showAddStdioScreen(existingName, existingUpstream) {
|
|
2576
2724
|
this.state.currentScreen = "add-stdio";
|
|
2577
2725
|
this.clearScreen();
|
|
2578
2726
|
this.addHeader();
|
|
2579
2727
|
if (!this.container)
|
|
2580
2728
|
return;
|
|
2729
|
+
const isEditMode = Boolean(existingName && existingUpstream);
|
|
2581
2730
|
const formBox = new BoxRenderable(this.renderer, {
|
|
2582
2731
|
id: "add-stdio-box",
|
|
2583
2732
|
width: 60,
|
|
@@ -2585,7 +2734,7 @@ class ConfigTuiApp {
|
|
|
2585
2734
|
border: true,
|
|
2586
2735
|
borderStyle: "single",
|
|
2587
2736
|
borderColor: "#475569",
|
|
2588
|
-
title: "Add Stdio Upstream",
|
|
2737
|
+
title: isEditMode ? `Edit Stdio Upstream: ${existingName}` : "Add Stdio Upstream",
|
|
2589
2738
|
titleAlignment: "center",
|
|
2590
2739
|
backgroundColor: "#1e293b",
|
|
2591
2740
|
flexDirection: "column",
|
|
@@ -2612,6 +2761,7 @@ class ConfigTuiApp {
|
|
|
2612
2761
|
}
|
|
2613
2762
|
});
|
|
2614
2763
|
formBox.add(nameInput);
|
|
2764
|
+
nameInput.value = existingName ?? "";
|
|
2615
2765
|
const commandLabel = new TextRenderable(this.renderer, {
|
|
2616
2766
|
id: "command-label",
|
|
2617
2767
|
content: "Command (with arguments):",
|
|
@@ -2631,6 +2781,7 @@ class ConfigTuiApp {
|
|
|
2631
2781
|
}
|
|
2632
2782
|
});
|
|
2633
2783
|
formBox.add(commandInput);
|
|
2784
|
+
commandInput.value = existingUpstream ? [existingUpstream.stdio.command, ...existingUpstream.stdio.args].filter(Boolean).join(" ") : "";
|
|
2634
2785
|
const envLabel = new TextRenderable(this.renderer, {
|
|
2635
2786
|
id: "env-label",
|
|
2636
2787
|
content: "Environment variables (optional, comma-separated):",
|
|
@@ -2651,6 +2802,7 @@ class ConfigTuiApp {
|
|
|
2651
2802
|
}
|
|
2652
2803
|
});
|
|
2653
2804
|
formBox.add(envInput);
|
|
2805
|
+
envInput.value = stringifyKeyValuePairsInput(existingUpstream?.env);
|
|
2654
2806
|
const submitOptions = [
|
|
2655
2807
|
{ name: "[ Save Upstream ]", description: "", value: "save" },
|
|
2656
2808
|
{ name: "[ Cancel ]", description: "", value: "cancel" }
|
|
@@ -2675,44 +2827,28 @@ class ConfigTuiApp {
|
|
|
2675
2827
|
if (field)
|
|
2676
2828
|
field.focus();
|
|
2677
2829
|
};
|
|
2678
|
-
const parseEnvVars = (input) => {
|
|
2679
|
-
const env = {};
|
|
2680
|
-
if (!input.trim())
|
|
2681
|
-
return env;
|
|
2682
|
-
const pairs = input.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2683
|
-
for (const pair of pairs) {
|
|
2684
|
-
const eqIndex = pair.indexOf("=");
|
|
2685
|
-
if (eqIndex > 0) {
|
|
2686
|
-
const key = pair.substring(0, eqIndex).trim();
|
|
2687
|
-
const value = pair.substring(eqIndex + 1).trim();
|
|
2688
|
-
if (key) {
|
|
2689
|
-
env[key] = value;
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
}
|
|
2693
|
-
return env;
|
|
2694
|
-
};
|
|
2695
2830
|
const saveUpstream = () => {
|
|
2696
|
-
const
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2831
|
+
const result = saveStdioUpstreamFromForm({
|
|
2832
|
+
upstreams: this.state.config.upstreams,
|
|
2833
|
+
name: nameInput.value || "",
|
|
2834
|
+
commandLine: commandInput.value || "",
|
|
2835
|
+
envInput: envInput.value || "",
|
|
2836
|
+
existingName: isEditMode ? existingName : undefined,
|
|
2837
|
+
existingUpstream
|
|
2838
|
+
});
|
|
2839
|
+
if (!result.ok) {
|
|
2840
|
+
switch (result.reason) {
|
|
2841
|
+
case "name_required":
|
|
2842
|
+
case "name_conflict":
|
|
2843
|
+
nameInput.focus();
|
|
2844
|
+
return;
|
|
2845
|
+
case "command_required":
|
|
2846
|
+
commandInput.focus();
|
|
2847
|
+
return;
|
|
2848
|
+
default:
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2705
2851
|
}
|
|
2706
|
-
const envVars = parseEnvVars(envInput.value || "");
|
|
2707
|
-
const parts = trimmedCommand.split(/\s+/);
|
|
2708
|
-
const command = parts[0] || "";
|
|
2709
|
-
const args = parts.slice(1);
|
|
2710
|
-
this.state.config.upstreams[trimmedName] = {
|
|
2711
|
-
transport: "stdio",
|
|
2712
|
-
enabled: true,
|
|
2713
|
-
env: envVars,
|
|
2714
|
-
stdio: { command, args }
|
|
2715
|
-
};
|
|
2716
2852
|
this.state.isDirty = true;
|
|
2717
2853
|
cleanup();
|
|
2718
2854
|
this.showUpstreamsScreen();
|
|
@@ -2722,13 +2858,21 @@ class ConfigTuiApp {
|
|
|
2722
2858
|
saveUpstream();
|
|
2723
2859
|
} else {
|
|
2724
2860
|
cleanup();
|
|
2725
|
-
|
|
2861
|
+
if (isEditMode && existingName) {
|
|
2862
|
+
this.showEditUpstreamScreen(existingName);
|
|
2863
|
+
} else {
|
|
2864
|
+
this.showAddUpstreamScreen();
|
|
2865
|
+
}
|
|
2726
2866
|
}
|
|
2727
2867
|
});
|
|
2728
2868
|
const handleKeypress = (key) => {
|
|
2729
2869
|
if (key.name === "escape") {
|
|
2730
2870
|
cleanup();
|
|
2731
|
-
|
|
2871
|
+
if (isEditMode && existingName) {
|
|
2872
|
+
this.showEditUpstreamScreen(existingName);
|
|
2873
|
+
} else {
|
|
2874
|
+
this.showAddUpstreamScreen();
|
|
2875
|
+
}
|
|
2732
2876
|
return;
|
|
2733
2877
|
}
|
|
2734
2878
|
if (key.name === "tab" && !key.shift) {
|
|
@@ -2745,14 +2889,15 @@ class ConfigTuiApp {
|
|
|
2745
2889
|
};
|
|
2746
2890
|
this.renderer.keyInput.on("keypress", handleKeypress);
|
|
2747
2891
|
focusField(0);
|
|
2748
|
-
this.addInstructions("Tab: next field | Shift+Tab: prev | Esc:
|
|
2892
|
+
this.addInstructions("Tab: next field | Shift+Tab: prev | Esc: back");
|
|
2749
2893
|
}
|
|
2750
|
-
showAddSseScreen() {
|
|
2894
|
+
showAddSseScreen(existingName, existingUpstream) {
|
|
2751
2895
|
this.state.currentScreen = "add-sse";
|
|
2752
2896
|
this.clearScreen();
|
|
2753
2897
|
this.addHeader();
|
|
2754
2898
|
if (!this.container)
|
|
2755
2899
|
return;
|
|
2900
|
+
const isEditMode = Boolean(existingName && existingUpstream);
|
|
2756
2901
|
const formBox = new BoxRenderable(this.renderer, {
|
|
2757
2902
|
id: "add-sse-box",
|
|
2758
2903
|
width: 60,
|
|
@@ -2760,7 +2905,7 @@ class ConfigTuiApp {
|
|
|
2760
2905
|
border: true,
|
|
2761
2906
|
borderStyle: "single",
|
|
2762
2907
|
borderColor: "#475569",
|
|
2763
|
-
title: "Add HTTP/SSE Upstream",
|
|
2908
|
+
title: isEditMode ? `Edit HTTP/SSE Upstream: ${existingName}` : "Add HTTP/SSE Upstream",
|
|
2764
2909
|
titleAlignment: "center",
|
|
2765
2910
|
backgroundColor: "#1e293b",
|
|
2766
2911
|
flexDirection: "column",
|
|
@@ -2787,6 +2932,7 @@ class ConfigTuiApp {
|
|
|
2787
2932
|
}
|
|
2788
2933
|
});
|
|
2789
2934
|
formBox.add(nameInput);
|
|
2935
|
+
nameInput.value = existingName ?? "";
|
|
2790
2936
|
const urlLabel = new TextRenderable(this.renderer, {
|
|
2791
2937
|
id: "url-label",
|
|
2792
2938
|
content: "Server URL:",
|
|
@@ -2806,6 +2952,7 @@ class ConfigTuiApp {
|
|
|
2806
2952
|
}
|
|
2807
2953
|
});
|
|
2808
2954
|
formBox.add(urlInput);
|
|
2955
|
+
urlInput.value = existingUpstream?.sse.url ?? "";
|
|
2809
2956
|
const headersLabel = new TextRenderable(this.renderer, {
|
|
2810
2957
|
id: "headers-label",
|
|
2811
2958
|
content: "HTTP Headers (optional, comma-separated):",
|
|
@@ -2826,6 +2973,7 @@ class ConfigTuiApp {
|
|
|
2826
2973
|
}
|
|
2827
2974
|
});
|
|
2828
2975
|
formBox.add(headersInput);
|
|
2976
|
+
headersInput.value = stringifyKeyValuePairsInput(existingUpstream?.sse.headers);
|
|
2829
2977
|
const authLabel = new TextRenderable(this.renderer, {
|
|
2830
2978
|
id: "auth-label",
|
|
2831
2979
|
content: "OAuth Authentication:",
|
|
@@ -2833,7 +2981,14 @@ class ConfigTuiApp {
|
|
|
2833
2981
|
marginBottom: 0
|
|
2834
2982
|
});
|
|
2835
2983
|
formBox.add(authLabel);
|
|
2836
|
-
const authOptions = [
|
|
2984
|
+
const authOptions = existingUpstream?.sse.auth ? [
|
|
2985
|
+
{
|
|
2986
|
+
name: "Enabled (default port 8089)",
|
|
2987
|
+
description: "",
|
|
2988
|
+
value: "enabled"
|
|
2989
|
+
},
|
|
2990
|
+
{ name: "Disabled", description: "", value: "disabled" }
|
|
2991
|
+
] : [
|
|
2837
2992
|
{ name: "Disabled", description: "", value: "disabled" },
|
|
2838
2993
|
{
|
|
2839
2994
|
name: "Enabled (default port 8089)",
|
|
@@ -2874,6 +3029,7 @@ class ConfigTuiApp {
|
|
|
2874
3029
|
}
|
|
2875
3030
|
});
|
|
2876
3031
|
formBox.add(envInput);
|
|
3032
|
+
envInput.value = stringifyKeyValuePairsInput(existingUpstream?.env);
|
|
2877
3033
|
const submitOptions = [
|
|
2878
3034
|
{ name: "[ Save Upstream ]", description: "", value: "save" },
|
|
2879
3035
|
{ name: "[ Cancel ]", description: "", value: "cancel" }
|
|
@@ -2899,63 +3055,41 @@ class ConfigTuiApp {
|
|
|
2899
3055
|
submitSelect
|
|
2900
3056
|
];
|
|
2901
3057
|
let focusIndex = 0;
|
|
2902
|
-
let
|
|
3058
|
+
let selectedAuthValue = String(authOptions[0]?.value ?? "disabled");
|
|
2903
3059
|
const focusField = (index) => {
|
|
2904
3060
|
focusIndex = index;
|
|
2905
3061
|
const field = fields[index];
|
|
2906
3062
|
if (field)
|
|
2907
3063
|
field.focus();
|
|
2908
3064
|
};
|
|
2909
|
-
authSelect.on(SelectRenderableEvents.ITEM_SELECTED, (
|
|
2910
|
-
|
|
2911
|
-
});
|
|
2912
|
-
const parseKeyValuePairs = (input) => {
|
|
2913
|
-
const result = {};
|
|
2914
|
-
if (!input.trim())
|
|
2915
|
-
return result;
|
|
2916
|
-
const pairs = input.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2917
|
-
for (const pair of pairs) {
|
|
2918
|
-
const eqIndex = pair.indexOf("=");
|
|
2919
|
-
if (eqIndex > 0) {
|
|
2920
|
-
const key = pair.substring(0, eqIndex).trim();
|
|
2921
|
-
const value = pair.substring(eqIndex + 1).trim();
|
|
2922
|
-
if (key) {
|
|
2923
|
-
result[key] = value;
|
|
2924
|
-
}
|
|
2925
|
-
}
|
|
2926
|
-
}
|
|
2927
|
-
return result;
|
|
2928
|
-
};
|
|
3065
|
+
authSelect.on(SelectRenderableEvents.ITEM_SELECTED, (_index, opt) => {
|
|
3066
|
+
selectedAuthValue = String(opt.value);
|
|
3067
|
+
});
|
|
2929
3068
|
const saveUpstream = () => {
|
|
2930
|
-
const
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
}
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
env: envVars,
|
|
2953
|
-
sse: {
|
|
2954
|
-
url: trimmedUrl,
|
|
2955
|
-
headers,
|
|
2956
|
-
auth: authEnabled ? true : undefined
|
|
3069
|
+
const result = saveSseUpstreamFromForm({
|
|
3070
|
+
upstreams: this.state.config.upstreams,
|
|
3071
|
+
name: nameInput.value || "",
|
|
3072
|
+
url: urlInput.value || "",
|
|
3073
|
+
headersInput: headersInput.value || "",
|
|
3074
|
+
envInput: envInput.value || "",
|
|
3075
|
+
authEnabled: selectedAuthValue === "enabled",
|
|
3076
|
+
existingName: isEditMode ? existingName : undefined,
|
|
3077
|
+
existingUpstream
|
|
3078
|
+
});
|
|
3079
|
+
if (!result.ok) {
|
|
3080
|
+
switch (result.reason) {
|
|
3081
|
+
case "name_required":
|
|
3082
|
+
case "name_conflict":
|
|
3083
|
+
nameInput.focus();
|
|
3084
|
+
return;
|
|
3085
|
+
case "url_required":
|
|
3086
|
+
case "invalid_url":
|
|
3087
|
+
urlInput.focus();
|
|
3088
|
+
return;
|
|
3089
|
+
default:
|
|
3090
|
+
return;
|
|
2957
3091
|
}
|
|
2958
|
-
}
|
|
3092
|
+
}
|
|
2959
3093
|
this.state.isDirty = true;
|
|
2960
3094
|
cleanup();
|
|
2961
3095
|
this.showUpstreamsScreen();
|
|
@@ -2965,13 +3099,21 @@ class ConfigTuiApp {
|
|
|
2965
3099
|
saveUpstream();
|
|
2966
3100
|
} else {
|
|
2967
3101
|
cleanup();
|
|
2968
|
-
|
|
3102
|
+
if (isEditMode && existingName) {
|
|
3103
|
+
this.showEditUpstreamScreen(existingName);
|
|
3104
|
+
} else {
|
|
3105
|
+
this.showAddUpstreamScreen();
|
|
3106
|
+
}
|
|
2969
3107
|
}
|
|
2970
3108
|
});
|
|
2971
3109
|
const handleKeypress = (key) => {
|
|
2972
3110
|
if (key.name === "escape") {
|
|
2973
3111
|
cleanup();
|
|
2974
|
-
|
|
3112
|
+
if (isEditMode && existingName) {
|
|
3113
|
+
this.showEditUpstreamScreen(existingName);
|
|
3114
|
+
} else {
|
|
3115
|
+
this.showAddUpstreamScreen();
|
|
3116
|
+
}
|
|
2975
3117
|
return;
|
|
2976
3118
|
}
|
|
2977
3119
|
if (key.name === "tab" && !key.shift) {
|
|
@@ -2988,7 +3130,7 @@ class ConfigTuiApp {
|
|
|
2988
3130
|
};
|
|
2989
3131
|
this.renderer.keyInput.on("keypress", handleKeypress);
|
|
2990
3132
|
focusField(0);
|
|
2991
|
-
this.addInstructions("Tab: next field | Shift+Tab: prev | Esc:
|
|
3133
|
+
this.addInstructions("Tab: next field | Shift+Tab: prev | Esc: back");
|
|
2992
3134
|
}
|
|
2993
3135
|
showEditUpstreamScreen(name) {
|
|
2994
3136
|
this.state.currentScreen = "edit-upstream";
|
|
@@ -3091,28 +3233,7 @@ class ConfigTuiApp {
|
|
|
3091
3233
|
});
|
|
3092
3234
|
menuBox.add(noEnvText);
|
|
3093
3235
|
}
|
|
3094
|
-
const options =
|
|
3095
|
-
{
|
|
3096
|
-
name: "Test Connection",
|
|
3097
|
-
description: "Connect and list available tools",
|
|
3098
|
-
value: "test"
|
|
3099
|
-
},
|
|
3100
|
-
{
|
|
3101
|
-
name: upstream.enabled ? "Disable" : "Enable",
|
|
3102
|
-
description: upstream.enabled ? "Stop using this upstream" : "Start using this upstream",
|
|
3103
|
-
value: "toggle"
|
|
3104
|
-
},
|
|
3105
|
-
{
|
|
3106
|
-
name: "Delete",
|
|
3107
|
-
description: "Remove this upstream configuration",
|
|
3108
|
-
value: "delete"
|
|
3109
|
-
},
|
|
3110
|
-
{
|
|
3111
|
-
name: "\u2190 Back",
|
|
3112
|
-
description: "",
|
|
3113
|
-
value: "back"
|
|
3114
|
-
}
|
|
3115
|
-
];
|
|
3236
|
+
const options = getUpstreamEditMenuOptions(upstream);
|
|
3116
3237
|
const menu = new SelectRenderable(this.renderer, {
|
|
3117
3238
|
id: "edit-menu",
|
|
3118
3239
|
width: "100%",
|
|
@@ -3129,6 +3250,13 @@ class ConfigTuiApp {
|
|
|
3129
3250
|
menuBox.add(menu);
|
|
3130
3251
|
menu.on(SelectRenderableEvents.ITEM_SELECTED, (_index, option) => {
|
|
3131
3252
|
switch (option.value) {
|
|
3253
|
+
case "edit":
|
|
3254
|
+
if (upstream.transport === "stdio") {
|
|
3255
|
+
this.showAddStdioScreen(name, upstream);
|
|
3256
|
+
} else {
|
|
3257
|
+
this.showAddSseScreen(name, upstream);
|
|
3258
|
+
}
|
|
3259
|
+
break;
|
|
3132
3260
|
case "test":
|
|
3133
3261
|
this.showTestScreen(name, upstream);
|
|
3134
3262
|
break;
|
|
@@ -3138,8 +3266,7 @@ class ConfigTuiApp {
|
|
|
3138
3266
|
this.showEditUpstreamScreen(name);
|
|
3139
3267
|
break;
|
|
3140
3268
|
case "delete":
|
|
3141
|
-
|
|
3142
|
-
this.state.isDirty = true;
|
|
3269
|
+
this.state.isDirty = deleteUpstreamByName(this.state.config.upstreams, name) || this.state.isDirty;
|
|
3143
3270
|
this.showUpstreamsScreen();
|
|
3144
3271
|
break;
|
|
3145
3272
|
case "back":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-squared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "MCP² (Mercury Control Plane) - A local-first meta-server and proxy for the Model Context Protocol",
|
|
5
5
|
"author": "aditzel",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"test": "bun test",
|
|
29
29
|
"test:fast": "SKIP_SLOW_TESTS=true bun test",
|
|
30
30
|
"test:watch": "bun test --watch",
|
|
31
|
+
"coverage:check": "bun run scripts/check-line-coverage.ts coverage/coverage-summary.txt 80",
|
|
31
32
|
"typecheck": "tsc --noEmit",
|
|
32
33
|
"lint": "biome check src tests scripts AGENTS.md CLAUDE.md WARP.md README.md CHANGELOG.md package.json biome.json tsconfig.json",
|
|
33
34
|
"release:check": "bun run audit && bun test && bun run build && bun run build:verify && bun run lint && bun run typecheck && bun pm pack --dry-run",
|
|
@@ -35,7 +36,8 @@
|
|
|
35
36
|
"lint:fix": "biome check --write .",
|
|
36
37
|
"format": "biome format --write .",
|
|
37
38
|
"clean": "rm -rf dist",
|
|
38
|
-
"safety:sim": "bun run agent_safety_kit/cost_model/simulate.ts --tasks agent_safety_kit/cost_model/tasks.csv --pricing agent_safety_kit/cost_model/pricing.csv --out agent_safety_kit/cost_model/report.md"
|
|
39
|
+
"safety:sim": "bun run agent_safety_kit/cost_model/simulate.ts --tasks agent_safety_kit/cost_model/tasks.csv --pricing agent_safety_kit/cost_model/pricing.csv --out agent_safety_kit/cost_model/report.md",
|
|
40
|
+
"eval:routing": "bun run scripts/eval-tool-routing.ts"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@biomejs/biome": "^2.4.4",
|