mcp-squared 0.4.0 → 0.6.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/dist/index.js CHANGED
@@ -124,6 +124,274 @@ function ensureConfigDir(configPath) {
124
124
  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";
125
125
  var init_paths = () => {};
126
126
 
127
+ // src/capabilities/inference.ts
128
+ function createEmptyScores() {
129
+ return CAPABILITY_IDS.reduce((acc, capability) => {
130
+ acc[capability] = 0;
131
+ return acc;
132
+ }, {});
133
+ }
134
+ function extractSchemaSignal(schema) {
135
+ if (!schema || schema.type !== "object") {
136
+ return "";
137
+ }
138
+ const keys = Object.keys(schema.properties ?? {});
139
+ const required = schema.required ?? [];
140
+ return `${keys.join(" ")} ${required.join(" ")}`;
141
+ }
142
+ function scoreTextSignals(scores, text) {
143
+ for (const capability of CAPABILITY_IDS) {
144
+ for (const pattern of CAPABILITY_PATTERNS[capability]) {
145
+ if (pattern.test(text)) {
146
+ scores[capability] += 4;
147
+ }
148
+ }
149
+ }
150
+ }
151
+ function getHighestScoringCapability(scores) {
152
+ const bestScore = Math.max(...Object.values(scores));
153
+ if (bestScore <= 0) {
154
+ return "general";
155
+ }
156
+ for (const capability of CAPABILITY_PRIORITY) {
157
+ if (scores[capability] === bestScore) {
158
+ return capability;
159
+ }
160
+ }
161
+ return "general";
162
+ }
163
+ function inferNamespaceCapability(namespace, tools, capabilityOverrides = {}) {
164
+ const override = capabilityOverrides[namespace];
165
+ if (override) {
166
+ return override;
167
+ }
168
+ const scores = createEmptyScores();
169
+ const namespaceText = namespace.toLowerCase();
170
+ for (const hint of NAMESPACE_HINTS) {
171
+ if (hint.pattern.test(namespaceText)) {
172
+ scores[hint.capability] += hint.score;
173
+ }
174
+ }
175
+ for (const tool of tools) {
176
+ const signal = [
177
+ tool.name,
178
+ tool.description ?? "",
179
+ extractSchemaSignal(tool.inputSchema)
180
+ ].join(" ").toLowerCase();
181
+ scoreTextSignals(scores, signal);
182
+ }
183
+ return getHighestScoringCapability(scores);
184
+ }
185
+ function groupNamespacesByCapability(inventories, capabilityOverrides = {}) {
186
+ const grouped = CAPABILITY_IDS.reduce((acc, capability) => {
187
+ acc[capability] = [];
188
+ return acc;
189
+ }, {});
190
+ const byNamespace = {};
191
+ const sorted = [...inventories].sort((a, b) => a.namespace.localeCompare(b.namespace));
192
+ for (const inventory of sorted) {
193
+ const capability = inferNamespaceCapability(inventory.namespace, inventory.tools, capabilityOverrides);
194
+ byNamespace[inventory.namespace] = capability;
195
+ grouped[capability].push(inventory.namespace);
196
+ }
197
+ return { byNamespace, grouped };
198
+ }
199
+ var CAPABILITY_IDS, CAPABILITY_PRIORITY, NAMESPACE_HINTS, CAPABILITY_PATTERNS;
200
+ var init_inference = __esm(() => {
201
+ CAPABILITY_IDS = [
202
+ "code_search",
203
+ "docs",
204
+ "browser_automation",
205
+ "issue_tracking",
206
+ "cms_content",
207
+ "design",
208
+ "ai_media_generation",
209
+ "hosting_deploy",
210
+ "time_util",
211
+ "research",
212
+ "general"
213
+ ];
214
+ CAPABILITY_PRIORITY = [
215
+ "code_search",
216
+ "docs",
217
+ "browser_automation",
218
+ "issue_tracking",
219
+ "cms_content",
220
+ "design",
221
+ "ai_media_generation",
222
+ "hosting_deploy",
223
+ "time_util",
224
+ "research",
225
+ "general"
226
+ ];
227
+ NAMESPACE_HINTS = [
228
+ {
229
+ capability: "code_search",
230
+ pattern: /(auggie|augment|ctxdb|code|source|repo|symbol|search)/i,
231
+ score: 24
232
+ },
233
+ {
234
+ capability: "docs",
235
+ pattern: /(context7|ref|docs?|documentation|shadcn)/i,
236
+ score: 18
237
+ },
238
+ {
239
+ capability: "browser_automation",
240
+ pattern: /(chrome|devtools|browser|playwright|puppeteer|webdriver)/i,
241
+ score: 24
242
+ },
243
+ {
244
+ capability: "issue_tracking",
245
+ pattern: /(linear|jira|issue|ticket|project|milestone)/i,
246
+ score: 20
247
+ },
248
+ {
249
+ capability: "cms_content",
250
+ pattern: /(sanity|content|cms|dataset|schema|studio)/i,
251
+ score: 20
252
+ },
253
+ {
254
+ capability: "design",
255
+ pattern: /(pencil|figma|ui|design|artifact|visual)/i,
256
+ score: 20
257
+ },
258
+ {
259
+ capability: "hosting_deploy",
260
+ pattern: /(vercel|host|hosting|domain|dns|vps|deploy|infra)/i,
261
+ score: 22
262
+ },
263
+ {
264
+ capability: "time_util",
265
+ pattern: /(time|timezone|clock|date|utc)/i,
266
+ score: 22
267
+ },
268
+ {
269
+ capability: "ai_media_generation",
270
+ pattern: /(wavespeed|stability|replicate|midjourney|dall.?e|runway|imagen|flux|fal\.ai|dreamstudio)/i,
271
+ score: 24
272
+ },
273
+ {
274
+ capability: "research",
275
+ pattern: /(exa|perplexity|firecrawl|crawl|scrape|research|search)/i,
276
+ score: 16
277
+ }
278
+ ];
279
+ CAPABILITY_PATTERNS = {
280
+ code_search: [
281
+ /\bcodebase\b/i,
282
+ /\bsource\b/i,
283
+ /\brepo(?:sitory)?\b/i,
284
+ /\bsymbol(?:s)?\b/i,
285
+ /\bdefinition(?:s)?\b/i,
286
+ /\breference(?:s)?\b/i,
287
+ /\busage(?:s)?\b/i,
288
+ /\bsearch_context\b/i,
289
+ /\bcodebase-retrieval\b/i,
290
+ /\bdirectory_path\b/i
291
+ ],
292
+ docs: [
293
+ /\bdoc(?:s|umentation)?\b/i,
294
+ /\bread_docs\b/i,
295
+ /\bquery-docs\b/i,
296
+ /\breference\b/i,
297
+ /\bmanual\b/i,
298
+ /\bknowledge\b/i,
299
+ /\bregist(?:ry|ries)\b/i,
300
+ /\bcomponent(?:s)?\b/i,
301
+ /\bexample(?:s)?\b/i
302
+ ],
303
+ browser_automation: [
304
+ /\bbrowser\b/i,
305
+ /\bdevtools\b/i,
306
+ /\bnavigate\b/i,
307
+ /\bclick\b/i,
308
+ /\bhover\b/i,
309
+ /\bnetwork\b/i,
310
+ /\bconsole\b/i,
311
+ /\bscreenshot\b/i,
312
+ /\bpage\b/i
313
+ ],
314
+ issue_tracking: [
315
+ /\bissue(?:s)?\b/i,
316
+ /\bticket(?:s)?\b/i,
317
+ /\bmilestone(?:s)?\b/i,
318
+ /\bcycle(?:s)?\b/i,
319
+ /\bproject(?:s)?\b/i,
320
+ /\bcomment(?:s)?\b/i,
321
+ /\blinear\b/i
322
+ ],
323
+ cms_content: [
324
+ /\bcms\b/i,
325
+ /\bcontent\b/i,
326
+ /\bdocument(?:s)?\b/i,
327
+ /\bdataset(?:s)?\b/i,
328
+ /\bschema\b/i,
329
+ /\bpublish\b/i,
330
+ /\bdraft(?:s)?\b/i,
331
+ /\bsanity\b/i,
332
+ /\bmigration\b/i
333
+ ],
334
+ design: [
335
+ /\bdesign\b/i,
336
+ /\bui\b/i,
337
+ /\bartifact\b/i,
338
+ /\bstyle_guide\b/i,
339
+ /\bscreenshot\b/i,
340
+ /\bdiagram\b/i,
341
+ /\bimage\b/i,
342
+ /\blayout\b/i,
343
+ /\bframe\b/i
344
+ ],
345
+ ai_media_generation: [
346
+ /\btext.to.image\b/i,
347
+ /\bimage.to.image\b/i,
348
+ /\btext.to.video\b/i,
349
+ /\binpaint(?:ing)?\b/i,
350
+ /\bupscal(?:e|ing)\b/i,
351
+ /\bgenerat(?:e|ion)\b.*\bimage/i,
352
+ /\bimage\b.*\bgenerat(?:e|ion)\b/i,
353
+ /\bai\b.*\b(?:image|photo|video)\b/i,
354
+ /\bstable.diffusion\b/i,
355
+ /\bdiffusion\b/i,
356
+ /\bprompt\b.*\b(?:image|visual)\b/i
357
+ ],
358
+ hosting_deploy: [
359
+ /\bdeploy(?:ment)?\b/i,
360
+ /\bhosting\b/i,
361
+ /\bdomain(?:s)?\b/i,
362
+ /\bdns\b/i,
363
+ /\bvps\b/i,
364
+ /\bvirtual_machine(?:s)?\b/i,
365
+ /\bfirewall\b/i,
366
+ /\bwebsite\b/i,
367
+ /\bbilling\b/i,
368
+ /\bnameserver(?:s)?\b/i
369
+ ],
370
+ time_util: [
371
+ /\btime\b/i,
372
+ /\btimezone\b/i,
373
+ /\butc\b/i,
374
+ /\bclock\b/i,
375
+ /\bdate\b/i,
376
+ /\bconvert_time\b/i,
377
+ /\bget_current_time\b/i
378
+ ],
379
+ research: [
380
+ /\bresearch\b/i,
381
+ /\bcrawl\b/i,
382
+ /\bscrape\b/i,
383
+ /\bextract\b/i,
384
+ /\bsemantic_search\b/i,
385
+ /\bweb_search\b/i,
386
+ /\bask\b/i,
387
+ /\bperplexity\b/i,
388
+ /\bfirecrawl\b/i,
389
+ /\bexa\b/i
390
+ ],
391
+ general: []
392
+ };
393
+ });
394
+
127
395
  // src/embeddings/generator.ts
128
396
  import {
129
397
  env,
@@ -281,33 +549,147 @@ var init_embeddings = __esm(() => {
281
549
  init_generator();
282
550
  });
283
551
 
552
+ // src/capabilities/semantic-classifier.ts
553
+ var exports_semantic_classifier = {};
554
+ __export(exports_semantic_classifier, {
555
+ SemanticCapabilityClassifier: () => SemanticCapabilityClassifier
556
+ });
557
+
558
+ class SemanticCapabilityClassifier {
559
+ generator;
560
+ confidenceThreshold;
561
+ referenceEmbeddings = null;
562
+ constructor(generator, options) {
563
+ this.generator = generator;
564
+ this.confidenceThreshold = options?.confidenceThreshold ?? 0.45;
565
+ }
566
+ async initializeReferences() {
567
+ const texts = CAPABILITY_IDS.map((id) => CAPABILITY_REFERENCE_TEXTS[id]);
568
+ const result = await this.generator.embedBatch(texts, false);
569
+ this.referenceEmbeddings = new Map;
570
+ for (let i = 0;i < CAPABILITY_IDS.length; i++) {
571
+ this.referenceEmbeddings.set(CAPABILITY_IDS[i], result.embeddings[i]);
572
+ }
573
+ }
574
+ isInitialized() {
575
+ return this.referenceEmbeddings !== null;
576
+ }
577
+ async classify(namespace, tools) {
578
+ if (!this.referenceEmbeddings) {
579
+ throw new Error("SemanticCapabilityClassifier not initialized. Call initializeReferences() first.");
580
+ }
581
+ const startTime = performance.now();
582
+ const signalText = this.buildSignalText(namespace, tools);
583
+ const signalResult = await this.generator.embed(signalText, true);
584
+ const scores = [];
585
+ for (const [capId, refEmb] of this.referenceEmbeddings) {
586
+ const similarity = EmbeddingGenerator.cosineSimilarity(signalResult.embedding, refEmb);
587
+ scores.push({ capability: capId, similarity });
588
+ }
589
+ scores.sort((a, b) => b.similarity - a.similarity);
590
+ const best = scores[0];
591
+ if (!best) {
592
+ throw new Error("No capability references available for classification");
593
+ }
594
+ const runnerUp = scores[1];
595
+ const inferenceMs = performance.now() - startTime;
596
+ return {
597
+ capability: best.capability,
598
+ confidence: best.similarity,
599
+ inferenceMs,
600
+ runnerUp: runnerUp ? { capability: runnerUp.capability, confidence: runnerUp.similarity } : undefined
601
+ };
602
+ }
603
+ async classifyBatch(inventories) {
604
+ const startTime = performance.now();
605
+ const overrides = {};
606
+ const classifications = [];
607
+ for (const { namespace, tools } of inventories) {
608
+ const result = await this.classify(namespace, tools);
609
+ classifications.push({ namespace, ...result });
610
+ if (result.confidence >= this.confidenceThreshold) {
611
+ overrides[namespace] = result.capability;
612
+ }
613
+ }
614
+ return {
615
+ overrides,
616
+ classifications,
617
+ inferenceMs: performance.now() - startTime
618
+ };
619
+ }
620
+ buildSignalText(namespace, tools) {
621
+ const parts = [namespace];
622
+ for (const tool of tools) {
623
+ parts.push(tool.name);
624
+ if (tool.description) {
625
+ parts.push(tool.description);
626
+ }
627
+ const schemaSignal = extractSchemaSignal(tool.inputSchema);
628
+ if (schemaSignal) {
629
+ parts.push(schemaSignal);
630
+ }
631
+ }
632
+ return parts.join(" ");
633
+ }
634
+ }
635
+ var CAPABILITY_REFERENCE_TEXTS;
636
+ var init_semantic_classifier = __esm(() => {
637
+ init_generator();
638
+ init_inference();
639
+ CAPABILITY_REFERENCE_TEXTS = {
640
+ code_search: "Search source code, symbols, function definitions, class references, and code context across repositories and codebases. Find implementations, usages, and navigate source files.",
641
+ docs: "Query technical documentation, API references, library guides, SDK docs, component registries, and code example lookups. Read documentation pages and fetch reference material.",
642
+ browser_automation: "Automate web browser interactions: click elements, fill forms, take screenshots, inspect DOM nodes, navigate URLs, execute JavaScript in page context, and run browser diagnostics.",
643
+ issue_tracking: "Manage project management tickets, kanban boards, sprints, and work items. Create, update, and track issues in project trackers like Jira, Linear, Asana, and ClickUp.",
644
+ cms_content: "Manage wiki pages, knowledge base articles, content documents, blog posts, editorial workflows, and structured content. Create and organize content in systems like Notion, Confluence, and Sanity.",
645
+ design: "Create and inspect visual design artifacts, UI mockups, wireframes, and design system components. Work with design tools like Figma, Sketch, and Pencil for visual layout and styling.",
646
+ ai_media_generation: "Generate and edit images, videos, and visual media using AI models. Create images from text prompts, edit existing images with AI, upscale resolution, apply style transfer, inpaint or outpaint regions, and generate sequential or consistent media. Supports text-to-image, image-to-image, and AI-powered visual content creation.",
647
+ hosting_deploy: "Manage server deployments, cloud infrastructure, hosting configurations, CI/CD pipelines, containers, databases, DNS records, and domain management.",
648
+ time_util: "Convert between timezones, get current time, format dates, calculate time differences, and resolve scheduling utilities.",
649
+ research: "Search the web, collect information from multiple sources, synthesize findings, and perform web research and data gathering operations.",
650
+ general: "General-purpose utility operations, API integrations, data transformations, notifications, messaging, payments, and miscellaneous tool actions."
651
+ };
652
+ });
653
+
284
654
  // src/tui/monitor-loader.ts
285
655
  var exports_monitor_loader = {};
286
656
  __export(exports_monitor_loader, {
287
657
  runMonitorTui: () => runMonitorTui
288
658
  });
659
+ function getMonitorModuleSpecifier() {
660
+ const self = import.meta.url;
661
+ if (self.includes("/tui/")) {
662
+ return new URL("./monitor.js", self).href;
663
+ }
664
+ return "./tui/monitor.js";
665
+ }
289
666
  async function loadMonitorModule() {
290
- return import(MONITOR_MODULE_SPECIFIER);
667
+ return import(getMonitorModuleSpecifier());
291
668
  }
292
669
  async function runMonitorTui(options = {}) {
293
670
  const { runMonitorTui: runMonitor } = await loadMonitorModule();
294
671
  return runMonitor(options);
295
672
  }
296
- var MONITOR_MODULE_SPECIFIER = "./tui/monitor.js";
297
673
 
298
674
  // src/tui/config-loader.ts
299
675
  var exports_config_loader = {};
300
676
  __export(exports_config_loader, {
301
677
  runConfigTui: () => runConfigTui
302
678
  });
679
+ function getConfigModuleSpecifier() {
680
+ const self = import.meta.url;
681
+ if (self.includes("/tui/")) {
682
+ return new URL("./config.js", self).href;
683
+ }
684
+ return "./tui/config.js";
685
+ }
303
686
  function loadConfigModule() {
304
- return import(CONFIG_MODULE_SPECIFIER);
687
+ return import(getConfigModuleSpecifier());
305
688
  }
306
689
  async function runConfigTui() {
307
690
  const { runConfigTui: _run } = await loadConfigModule();
308
691
  return _run();
309
692
  }
310
- var CONFIG_MODULE_SPECIFIER = "./tui/config.js";
311
693
 
312
694
  // src/init/runner.ts
313
695
  var exports_runner = {};
@@ -359,6 +741,15 @@ defaultDetailLevel = "L1"
359
741
  # Adjust or clear if your stack uses different code indexers.
360
742
  codeSearch = ["auggie", "ctxdb"]
361
743
 
744
+ [operations.dynamicToolSurface]
745
+ # Capability-first tool surface is always enabled.
746
+ inference = "heuristic_with_overrides"
747
+ refresh = "on_connect"
748
+
749
+ [operations.dynamicToolSurface.capabilityOverrides]
750
+ # Optional namespace -> capability pinning.
751
+ # auggie = "code_search"
752
+
362
753
  [operations.embeddings]
363
754
  # Enable to use semantic or hybrid search modes.
364
755
  # Requires onnxruntime shared library on the system.
@@ -525,6 +916,9 @@ function parseArgs(args) {
525
916
  case "monitor":
526
917
  result.mode = "monitor";
527
918
  break;
919
+ case "status":
920
+ result.mode = "status";
921
+ break;
528
922
  case "daemon":
529
923
  result.mode = "daemon";
530
924
  break;
@@ -699,6 +1093,7 @@ Usage:
699
1093
  mcp-squared init [options] Generate a starter config file with security profile
700
1094
  mcp-squared migrate [options] Apply config migrations to existing config
701
1095
  mcp-squared install [options] Install MCP\xB2 into other MCP clients
1096
+ mcp-squared status [options] Show upstream server status and capability routing
702
1097
  mcp-squared monitor [options] Launch server monitor TUI
703
1098
  mcp-squared daemon [options] Start shared MCP\xB2 daemon
704
1099
  mcp-squared proxy [options] Start stdio proxy (connects to daemon)
@@ -713,6 +1108,7 @@ Commands:
713
1108
  init Generate a starter config with security profile
714
1109
  migrate Apply one-time config migrations to existing config
715
1110
  install Install MCP\xB2 as a server in other MCP clients
1111
+ status Show upstream status and capability routing
716
1112
  monitor Launch server monitor TUI
717
1113
  daemon Start shared daemon for multiple clients
718
1114
  proxy Start stdio proxy for daemon
@@ -752,6 +1148,9 @@ Install Options:
752
1148
  --dry-run Preview changes without writing
753
1149
  --no-interactive Disable interactive prompts
754
1150
 
1151
+ Status Options:
1152
+ --verbose, -V Show schema parameters and extra detail
1153
+
755
1154
  Monitor Options:
756
1155
  (daemon-first; attaches to shared daemon monitor by default)
757
1156
  --refresh-interval=<ms> Auto-refresh interval in milliseconds (default: 2000)
@@ -771,6 +1170,8 @@ Supported Tools:
771
1170
  ${VALID_TOOL_IDS.join(", ")}
772
1171
 
773
1172
  Examples:
1173
+ mcp-squared status Show upstream status and capability routing table
1174
+ mcp-squared status --verbose Show status with schema parameters
774
1175
  mcp-squared test github Test connection to 'github' upstream
775
1176
  mcp-squared test Test all configured upstreams
776
1177
  mcp-squared auth vercel-mcp Authenticate with 'vercel-mcp' upstream (OAuth)
@@ -1039,6 +1440,7 @@ import { parse as parseToml } from "smol-toml";
1039
1440
  import { ZodError } from "zod";
1040
1441
 
1041
1442
  // src/config/schema.ts
1443
+ init_inference();
1042
1444
  import { z } from "zod";
1043
1445
  var LATEST_SCHEMA_VERSION = 1;
1044
1446
  var LogLevelSchema = z.enum([
@@ -1095,6 +1497,18 @@ var SecuritySchema = z.object({
1095
1497
  });
1096
1498
  var SearchModeSchema = z.enum(["fast", "semantic", "hybrid"]);
1097
1499
  var DetailLevelSchema = z.enum(["L0", "L1", "L2"]);
1500
+ var CapabilityIdSchema = z.enum(CAPABILITY_IDS);
1501
+ var DynamicToolSurfaceInferenceSchema = z.enum([
1502
+ "heuristic_with_overrides",
1503
+ "hybrid"
1504
+ ]);
1505
+ var DynamicToolSurfaceRefreshSchema = z.enum(["on_connect"]);
1506
+ var DynamicToolSurfaceSchema = z.object({
1507
+ inference: DynamicToolSurfaceInferenceSchema.default("heuristic_with_overrides"),
1508
+ refresh: DynamicToolSurfaceRefreshSchema.default("on_connect"),
1509
+ capabilityOverrides: z.record(z.string().min(1), CapabilityIdSchema).default({}),
1510
+ semanticConfidenceThreshold: z.number().min(0).max(1).default(0.45)
1511
+ });
1098
1512
  var PreferredNamespacesByIntentSchema = z.object({
1099
1513
  codeSearch: z.array(z.string().min(1)).default([])
1100
1514
  });
@@ -1136,6 +1550,12 @@ var OperationsSchema = z.object({
1136
1550
  enabled: true,
1137
1551
  minCooccurrenceThreshold: 2,
1138
1552
  maxBundleSuggestions: 3
1553
+ }),
1554
+ dynamicToolSurface: DynamicToolSurfaceSchema.default({
1555
+ inference: "heuristic_with_overrides",
1556
+ refresh: "on_connect",
1557
+ capabilityOverrides: {},
1558
+ semanticConfidenceThreshold: 0.45
1139
1559
  })
1140
1560
  }).default({
1141
1561
  findTools: {
@@ -1152,6 +1572,12 @@ var OperationsSchema = z.object({
1152
1572
  enabled: true,
1153
1573
  minCooccurrenceThreshold: 2,
1154
1574
  maxBundleSuggestions: 3
1575
+ },
1576
+ dynamicToolSurface: {
1577
+ inference: "heuristic_with_overrides",
1578
+ refresh: "on_connect",
1579
+ capabilityOverrides: {},
1580
+ semanticConfidenceThreshold: 0.45
1155
1581
  }
1156
1582
  });
1157
1583
  var ConfigSchema = z.object({
@@ -1205,6 +1631,34 @@ function migrateV0ToV1(config) {
1205
1631
 
1206
1632
  // src/config/load.ts
1207
1633
  init_paths();
1634
+ function isRecord(value) {
1635
+ return typeof value === "object" && value !== null;
1636
+ }
1637
+ function getDeprecatedDynamicToolSurfaceKeys(raw) {
1638
+ const operations = raw["operations"];
1639
+ if (!isRecord(operations)) {
1640
+ return [];
1641
+ }
1642
+ const dynamicToolSurface = operations["dynamicToolSurface"];
1643
+ if (!isRecord(dynamicToolSurface)) {
1644
+ return [];
1645
+ }
1646
+ const deprecated = [];
1647
+ if ("mode" in dynamicToolSurface) {
1648
+ deprecated.push("operations.dynamicToolSurface.mode");
1649
+ }
1650
+ if ("naming" in dynamicToolSurface) {
1651
+ deprecated.push("operations.dynamicToolSurface.naming");
1652
+ }
1653
+ return deprecated;
1654
+ }
1655
+ function warnDeprecatedDynamicToolSurfaceKeys(filePath, keys) {
1656
+ if (keys.length === 0) {
1657
+ return;
1658
+ }
1659
+ console.warn(`[mcp\xB2] Deprecated config keys in ${filePath}: ${keys.join(", ")}. Run 'mcp-squared migrate' to clean up legacy settings.`);
1660
+ }
1661
+
1208
1662
  class ConfigError extends Error {
1209
1663
  cause;
1210
1664
  constructor(message, cause) {
@@ -1272,6 +1726,7 @@ async function loadConfigFromPath(filePath, source) {
1272
1726
  } catch (err) {
1273
1727
  throw new ConfigParseError(filePath, err);
1274
1728
  }
1729
+ warnDeprecatedDynamicToolSurfaceKeys(filePath, getDeprecatedDynamicToolSurfaceKeys(rawConfig));
1275
1730
  const migrated = migrateConfig(rawConfig);
1276
1731
  let config;
1277
1732
  try {
@@ -4305,33 +4760,109 @@ Installation cancelled.`);
4305
4760
  }
4306
4761
 
4307
4762
  // src/migrate/runner.ts
4763
+ init_inference();
4308
4764
  import { readFileSync as readFileSync5 } from "fs";
4309
4765
  import { parse as parseToml4 } from "smol-toml";
4310
4766
  var DEFAULT_CODE_SEARCH_NAMESPACES = ["auggie", "ctxdb"];
4311
- function isRecord(value) {
4767
+ var RESERVED_DESCRIBE_ACTION = "__describe_actions";
4768
+ function isRecord2(value) {
4312
4769
  return typeof value === "object" && value !== null;
4313
4770
  }
4314
4771
  function hasOwn(obj, key) {
4315
4772
  return Object.hasOwn(obj, key);
4316
4773
  }
4774
+ function toActionToken(value) {
4775
+ const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
4776
+ const token = normalized.length > 0 ? normalized : "tool";
4777
+ const reservedNormalized = RESERVED_DESCRIBE_ACTION.replace(/^_+|_+$/g, "").replace(/_+/g, "_");
4778
+ if (token === reservedNormalized) {
4779
+ return `${RESERVED_DESCRIBE_ACTION}__tool`;
4780
+ }
4781
+ return token;
4782
+ }
4783
+ function isCapabilityId(value) {
4784
+ return CAPABILITY_IDS.includes(value);
4785
+ }
4786
+ function hasLegacyDynamicToolSurfaceKeys(rawConfig) {
4787
+ if (!isRecord2(rawConfig)) {
4788
+ return false;
4789
+ }
4790
+ const operations = rawConfig["operations"];
4791
+ if (!isRecord2(operations)) {
4792
+ return false;
4793
+ }
4794
+ const dynamicToolSurface = operations["dynamicToolSurface"];
4795
+ if (!isRecord2(dynamicToolSurface)) {
4796
+ return false;
4797
+ }
4798
+ return hasOwn(dynamicToolSurface, "mode") || hasOwn(dynamicToolSurface, "naming");
4799
+ }
4317
4800
  function isCodeSearchExplicitlyConfigured(rawConfig) {
4318
- if (!isRecord(rawConfig)) {
4801
+ if (!isRecord2(rawConfig)) {
4319
4802
  return false;
4320
4803
  }
4321
4804
  const operations = rawConfig["operations"];
4322
- if (!isRecord(operations)) {
4805
+ if (!isRecord2(operations)) {
4323
4806
  return false;
4324
4807
  }
4325
4808
  const findTools = operations["findTools"];
4326
- if (!isRecord(findTools)) {
4809
+ if (!isRecord2(findTools)) {
4327
4810
  return false;
4328
4811
  }
4329
4812
  const preferredNamespacesByIntent = findTools["preferredNamespacesByIntent"];
4330
- if (!isRecord(preferredNamespacesByIntent)) {
4813
+ if (!isRecord2(preferredNamespacesByIntent)) {
4331
4814
  return false;
4332
4815
  }
4333
4816
  return hasOwn(preferredNamespacesByIntent, "codeSearch");
4334
4817
  }
4818
+ function dedupePreserveOrder(values) {
4819
+ const seen = new Set;
4820
+ const result = [];
4821
+ for (const value of values) {
4822
+ if (seen.has(value)) {
4823
+ continue;
4824
+ }
4825
+ seen.add(value);
4826
+ result.push(value);
4827
+ }
4828
+ return result;
4829
+ }
4830
+ function translateSecurityPattern(pattern, config) {
4831
+ const [scopeRaw, actionRaw] = pattern.split(":", 2);
4832
+ if (!scopeRaw || !actionRaw) {
4833
+ return {
4834
+ original: pattern,
4835
+ translated: pattern,
4836
+ changed: false,
4837
+ unresolvedReason: "invalid pattern format"
4838
+ };
4839
+ }
4840
+ const translatedAction = actionRaw === "*" ? "*" : toActionToken(actionRaw);
4841
+ if (scopeRaw === "*") {
4842
+ const translated2 = `*:${translatedAction}`;
4843
+ return {
4844
+ original: pattern,
4845
+ translated: translated2,
4846
+ changed: translated2 !== pattern
4847
+ };
4848
+ }
4849
+ if (isCapabilityId(scopeRaw)) {
4850
+ const translated2 = `${scopeRaw}:${translatedAction}`;
4851
+ return {
4852
+ original: pattern,
4853
+ translated: translated2,
4854
+ changed: translated2 !== pattern
4855
+ };
4856
+ }
4857
+ const override = config.operations.dynamicToolSurface.capabilityOverrides[scopeRaw];
4858
+ const capability = override ?? inferNamespaceCapability(scopeRaw, [], {});
4859
+ const translated = `${capability}:${translatedAction}`;
4860
+ return {
4861
+ original: pattern,
4862
+ translated,
4863
+ changed: translated !== pattern
4864
+ };
4865
+ }
4335
4866
  function applyCodeSearchPreferenceMigration(config, options) {
4336
4867
  if (options.codeSearchExplicitlyConfigured) {
4337
4868
  return { config, changed: false };
@@ -4353,6 +4884,53 @@ function applyCodeSearchPreferenceMigration(config, options) {
4353
4884
  }
4354
4885
  };
4355
4886
  }
4887
+ function applySecurityPatternMigration(config) {
4888
+ const report = {
4889
+ translated: [],
4890
+ unresolved: []
4891
+ };
4892
+ const migrateList = (patterns) => {
4893
+ const next = [];
4894
+ for (const pattern of patterns) {
4895
+ const translation = translateSecurityPattern(pattern, config);
4896
+ next.push(translation.translated);
4897
+ if (translation.unresolvedReason) {
4898
+ report.unresolved.push({
4899
+ pattern: translation.original,
4900
+ reason: translation.unresolvedReason
4901
+ });
4902
+ } else if (translation.changed) {
4903
+ report.translated.push({
4904
+ from: translation.original,
4905
+ to: translation.translated
4906
+ });
4907
+ }
4908
+ }
4909
+ return dedupePreserveOrder(next);
4910
+ };
4911
+ const allow = migrateList(config.security.tools.allow);
4912
+ const block = migrateList(config.security.tools.block);
4913
+ const confirm = migrateList(config.security.tools.confirm);
4914
+ const changed = allow.join("|") !== config.security.tools.allow.join("|") || block.join("|") !== config.security.tools.block.join("|") || confirm.join("|") !== config.security.tools.confirm.join("|");
4915
+ if (!changed && report.unresolved.length === 0) {
4916
+ return { config, changed: false, report };
4917
+ }
4918
+ return {
4919
+ changed,
4920
+ report,
4921
+ config: {
4922
+ ...config,
4923
+ security: {
4924
+ ...config.security,
4925
+ tools: {
4926
+ allow,
4927
+ block,
4928
+ confirm
4929
+ }
4930
+ }
4931
+ }
4932
+ };
4933
+ }
4356
4934
  async function runMigrate(args) {
4357
4935
  const discovered = discoverConfigPath();
4358
4936
  if (!discovered) {
@@ -4362,21 +4940,50 @@ async function runMigrate(args) {
4362
4940
  const loaded = await loadConfigFromPath(discovered.path, discovered.source);
4363
4941
  const rawConfig = parseToml4(readFileSync5(discovered.path, "utf-8"));
4364
4942
  const codeSearchExplicitlyConfigured = isCodeSearchExplicitlyConfigured(rawConfig);
4365
- const { config: migratedConfig, changed } = applyCodeSearchPreferenceMigration(loaded.config, {
4943
+ const hasLegacySurfaceKeys = hasLegacyDynamicToolSurfaceKeys(rawConfig);
4944
+ const codeSearchMigration = applyCodeSearchPreferenceMigration(loaded.config, {
4366
4945
  codeSearchExplicitlyConfigured
4367
4946
  });
4368
- if (!changed) {
4369
- console.log(`No migration needed: code-search preferences already configured in ${discovered.path}`);
4947
+ const securityMigration = applySecurityPatternMigration(codeSearchMigration.config);
4948
+ const changed = codeSearchMigration.changed || securityMigration.changed || hasLegacySurfaceKeys;
4949
+ if (!changed && securityMigration.report.unresolved.length === 0) {
4950
+ console.log(`No migration needed for ${discovered.path}`);
4370
4951
  return;
4371
4952
  }
4372
4953
  if (args.dryRun) {
4373
4954
  console.log(`[dry-run] Would update ${discovered.path}`);
4374
- console.log('[dry-run] Set operations.findTools.preferredNamespacesByIntent.codeSearch = ["auggie", "ctxdb"]');
4955
+ if (codeSearchMigration.changed) {
4956
+ console.log('[dry-run] Set operations.findTools.preferredNamespacesByIntent.codeSearch = ["auggie", "ctxdb"]');
4957
+ }
4958
+ if (securityMigration.report.translated.length > 0) {
4959
+ for (const item of securityMigration.report.translated) {
4960
+ console.log(`[dry-run] Translate security pattern: ${item.from} -> ${item.to}`);
4961
+ }
4962
+ }
4963
+ if (hasLegacySurfaceKeys) {
4964
+ console.log("[dry-run] Remove deprecated keys: operations.dynamicToolSurface.mode, operations.dynamicToolSurface.naming");
4965
+ }
4966
+ if (securityMigration.report.unresolved.length > 0) {
4967
+ for (const unresolved of securityMigration.report.unresolved) {
4968
+ console.warn(`[dry-run] Unresolved security pattern '${unresolved.pattern}': ${unresolved.reason}`);
4969
+ }
4970
+ }
4375
4971
  return;
4376
4972
  }
4377
- await saveConfig(discovered.path, migratedConfig);
4973
+ await saveConfig(discovered.path, securityMigration.config);
4378
4974
  console.log(`Updated ${discovered.path}`);
4379
- console.log('Set operations.findTools.preferredNamespacesByIntent.codeSearch = ["auggie", "ctxdb"]');
4975
+ if (codeSearchMigration.changed) {
4976
+ console.log('Set operations.findTools.preferredNamespacesByIntent.codeSearch = ["auggie", "ctxdb"]');
4977
+ }
4978
+ for (const item of securityMigration.report.translated) {
4979
+ console.log(`Translated security pattern: ${item.from} -> ${item.to}`);
4980
+ }
4981
+ if (hasLegacySurfaceKeys) {
4982
+ console.log("Removed deprecated keys: operations.dynamicToolSurface.mode, operations.dynamicToolSurface.naming");
4983
+ }
4984
+ for (const unresolved of securityMigration.report.unresolved) {
4985
+ console.warn(`Unresolved security pattern '${unresolved.pattern}': ${unresolved.reason}. Please review manually.`);
4986
+ }
4380
4987
  }
4381
4988
 
4382
4989
  // src/oauth/browser.ts
@@ -5878,6 +6485,83 @@ class SelectionTracker {
5878
6485
  this.sessionTools.clear();
5879
6486
  }
5880
6487
  }
6488
+ // src/server/index.ts
6489
+ init_inference();
6490
+
6491
+ // src/capabilities/routing.ts
6492
+ var DESCRIBE_ACTION = "__describe_actions";
6493
+ function toActionToken2(value) {
6494
+ const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
6495
+ return normalized.length > 0 ? normalized : "tool";
6496
+ }
6497
+ function buildCapabilityRouters(inventories, grouping, summarize) {
6498
+ if (inventories.length === 0) {
6499
+ return [];
6500
+ }
6501
+ const defaultSummarize = (desc, cap) => {
6502
+ if (desc) {
6503
+ const firstLine = desc.split(`
6504
+ `)[0]?.trim();
6505
+ if (firstLine && firstLine.length > 0) {
6506
+ return firstLine.length > 120 ? `${firstLine.slice(0, 117)}...` : firstLine;
6507
+ }
6508
+ }
6509
+ const title = cap.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
6510
+ return `Execute ${title} action`;
6511
+ };
6512
+ const resolveSummary = summarize ?? defaultSummarize;
6513
+ const candidates = [];
6514
+ const reservedNormalized = toActionToken2(DESCRIBE_ACTION);
6515
+ for (const inventory of inventories) {
6516
+ const capability = grouping.byNamespace[inventory.namespace] ?? "general";
6517
+ const sortedTools = [...inventory.tools].sort((a, b) => a.name.localeCompare(b.name));
6518
+ for (const tool of sortedTools) {
6519
+ let baseAction = toActionToken2(tool.name);
6520
+ if (baseAction === DESCRIBE_ACTION || baseAction === reservedNormalized) {
6521
+ baseAction = `${DESCRIBE_ACTION}__tool`;
6522
+ }
6523
+ candidates.push({
6524
+ capability,
6525
+ action: baseAction,
6526
+ baseAction,
6527
+ serverKey: inventory.namespace,
6528
+ toolName: tool.name,
6529
+ qualifiedName: `${inventory.namespace}:${tool.name}`,
6530
+ inputSchema: tool.inputSchema ?? { type: "object" },
6531
+ summary: resolveSummary(tool.description ?? undefined, capability)
6532
+ });
6533
+ }
6534
+ }
6535
+ const byCapabilityAction = new Map;
6536
+ for (const candidate of candidates) {
6537
+ const key = `${candidate.capability}:${candidate.baseAction}`;
6538
+ const existing = byCapabilityAction.get(key) ?? [];
6539
+ existing.push(candidate);
6540
+ byCapabilityAction.set(key, existing);
6541
+ }
6542
+ const resolved = [];
6543
+ const sortedKeys = [...byCapabilityAction.keys()].sort((a, b) => a.localeCompare(b));
6544
+ for (const key of sortedKeys) {
6545
+ const records = (byCapabilityAction.get(key) ?? []).sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName));
6546
+ records.forEach((record, index) => {
6547
+ resolved.push({
6548
+ ...record,
6549
+ action: index === 0 ? record.baseAction : `${record.baseAction}__${index + 1}`
6550
+ });
6551
+ });
6552
+ }
6553
+ const byCapability = new Map;
6554
+ for (const route of resolved) {
6555
+ const existing = byCapability.get(route.capability) ?? [];
6556
+ existing.push(route);
6557
+ byCapability.set(route.capability, existing);
6558
+ }
6559
+ return [...byCapability.entries()].map(([capability, actions]) => ({
6560
+ capability,
6561
+ actions: actions.sort((a, b) => a.action.localeCompare(b.action))
6562
+ })).sort((a, b) => a.capability.localeCompare(b.capability));
6563
+ }
6564
+
5881
6565
  // src/index/store.ts
5882
6566
  import { Database } from "bun:sqlite";
5883
6567
  function hashSchema2(schema) {
@@ -6424,6 +7108,9 @@ class Retriever {
6424
7108
  clearIndex() {
6425
7109
  this.indexStore.clear();
6426
7110
  }
7111
+ getEmbeddingGenerator() {
7112
+ return this.embeddingGenerator;
7113
+ }
6427
7114
  async initializeEmbeddings() {
6428
7115
  if (!this.embeddingsAvailable) {
6429
7116
  return;
@@ -6545,60 +7232,68 @@ function cleanupExpiredTokens() {
6545
7232
  }
6546
7233
  }
6547
7234
  }
6548
- function validateConfirmationToken(token, serverKey, toolName) {
7235
+ function validateConfirmationToken(token, capability, action) {
6549
7236
  cleanupExpiredTokens();
6550
7237
  const confirmation = pendingConfirmations.get(token);
6551
7238
  if (!confirmation) {
6552
7239
  return false;
6553
7240
  }
6554
- if (confirmation.serverKey !== serverKey || confirmation.toolName !== toolName) {
7241
+ if (confirmation.capability !== capability || confirmation.action !== action) {
6555
7242
  return false;
6556
7243
  }
6557
7244
  pendingConfirmations.delete(token);
6558
7245
  return true;
6559
7246
  }
6560
- function createConfirmationToken(serverKey, toolName) {
7247
+ function createConfirmationToken(capability, action) {
6561
7248
  cleanupExpiredTokens();
6562
7249
  const token = generateToken();
6563
7250
  pendingConfirmations.set(token, {
6564
- serverKey,
6565
- toolName,
7251
+ capability,
7252
+ action,
6566
7253
  createdAt: Date.now()
6567
7254
  });
6568
7255
  return token;
6569
7256
  }
6570
7257
  function evaluatePolicy(context, config) {
6571
- const { serverKey, toolName, confirmationToken } = context;
7258
+ const capability = context.capability ?? context.serverKey;
7259
+ const action = context.action ?? context.toolName;
7260
+ const confirmationToken = context.confirmationToken;
6572
7261
  const { block, confirm, allow } = config.security.tools;
6573
- if (matchesAnyPattern(block, serverKey, toolName)) {
7262
+ if (!capability || !action) {
6574
7263
  return {
6575
7264
  decision: "block",
6576
- reason: `Tool "${toolName}" on server "${serverKey}" is blocked by security policy`
7265
+ reason: "Missing capability/action in security policy context"
6577
7266
  };
6578
7267
  }
6579
- if (matchesAnyPattern(allow, serverKey, toolName)) {
7268
+ if (matchesAnyPattern(block, capability, action)) {
7269
+ return {
7270
+ decision: "block",
7271
+ reason: `Action "${action}" in capability "${capability}" is blocked by security policy`
7272
+ };
7273
+ }
7274
+ if (matchesAnyPattern(allow, capability, action)) {
6580
7275
  return {
6581
7276
  decision: "allow",
6582
- reason: `Tool "${toolName}" is allowed by security policy`
7277
+ reason: `Action "${action}" is allowed by security policy`
6583
7278
  };
6584
7279
  }
6585
- if (matchesAnyPattern(confirm, serverKey, toolName)) {
6586
- if (confirmationToken && validateConfirmationToken(confirmationToken, serverKey, toolName)) {
7280
+ if (matchesAnyPattern(confirm, capability, action)) {
7281
+ if (confirmationToken && validateConfirmationToken(confirmationToken, capability, action)) {
6587
7282
  return {
6588
7283
  decision: "allow",
6589
- reason: `Tool "${toolName}" confirmed with valid token`
7284
+ reason: `Action "${action}" confirmed with valid token`
6590
7285
  };
6591
7286
  }
6592
- const token = createConfirmationToken(serverKey, toolName);
7287
+ const token = createConfirmationToken(capability, action);
6593
7288
  return {
6594
7289
  decision: "confirm",
6595
- reason: `Tool "${toolName}" on server "${serverKey}" requires confirmation`,
7290
+ reason: `Action "${action}" in capability "${capability}" requires confirmation`,
6596
7291
  confirmationToken: token
6597
7292
  };
6598
7293
  }
6599
7294
  return {
6600
7295
  decision: "block",
6601
- reason: `Tool "${toolName}" on server "${serverKey}" is not in the allow or confirm list`
7296
+ reason: `Action "${action}" in capability "${capability}" is not in the allow or confirm list`
6602
7297
  };
6603
7298
  }
6604
7299
  function compilePolicy(config) {
@@ -6763,6 +7458,26 @@ async function waitForProcessExit(proc, timeoutMs) {
6763
7458
  }
6764
7459
 
6765
7460
  // src/upstream/cataloger.ts
7461
+ var AUTH_ERROR_PATTERNS = [
7462
+ "invalid_token",
7463
+ "invalid token",
7464
+ "unauthorized",
7465
+ "no token provided",
7466
+ "no authorization",
7467
+ "api key",
7468
+ "api_key",
7469
+ "authentication required",
7470
+ "authentication failed",
7471
+ "invalid credentials",
7472
+ "invalid api",
7473
+ "forbidden",
7474
+ "access denied",
7475
+ "not authenticated"
7476
+ ];
7477
+ function isAuthError(message) {
7478
+ const lower = message.toLowerCase();
7479
+ return AUTH_ERROR_PATTERNS.some((pattern) => lower.includes(pattern));
7480
+ }
6766
7481
  function resolveEnvVars(env2) {
6767
7482
  const resolved = {};
6768
7483
  for (const [key, value] of Object.entries(env2)) {
@@ -6843,7 +7558,7 @@ class Cataloger {
6843
7558
  if (err instanceof UnauthorizedError2 && connection.authProvider) {
6844
7559
  if (connection.authProvider.isNonInteractive()) {
6845
7560
  connection.authPending = true;
6846
- connection.status = "error";
7561
+ connection.status = "needs_auth";
6847
7562
  connection.error = `OAuth authorization required. Run: mcp-squared auth ${key}`;
6848
7563
  return;
6849
7564
  }
@@ -6867,8 +7582,9 @@ class Cataloger {
6867
7582
  }));
6868
7583
  connection.status = "connected";
6869
7584
  } catch (err) {
6870
- connection.status = "error";
6871
- connection.error = err instanceof Error ? err.message : String(err);
7585
+ const errorMsg = err instanceof Error ? err.message : String(err);
7586
+ connection.status = isAuthError(errorMsg) ? "needs_auth" : "error";
7587
+ connection.error = errorMsg;
6872
7588
  try {
6873
7589
  await this.cleanupConnection(connection);
6874
7590
  } catch (_cleanupErr) {}
@@ -7030,13 +7746,14 @@ class Cataloger {
7030
7746
  if (err instanceof UnauthorizedError2 && connection.authProvider) {
7031
7747
  if (connection.authProvider.isNonInteractive()) {
7032
7748
  connection.authPending = true;
7033
- connection.status = "error";
7749
+ connection.status = "needs_auth";
7034
7750
  connection.error = `OAuth authorization required. Run: mcp-squared auth ${key}`;
7035
7751
  return;
7036
7752
  }
7037
7753
  }
7038
- connection.status = "error";
7039
- connection.error = err instanceof Error ? err.message : String(err);
7754
+ const errorMsg = err instanceof Error ? err.message : String(err);
7755
+ connection.status = isAuthError(errorMsg) ? "needs_auth" : "error";
7756
+ connection.error = errorMsg;
7040
7757
  }
7041
7758
  }
7042
7759
  async refreshAllTools() {
@@ -7341,6 +8058,37 @@ async function testUpstreamConnection(name, config, options = {}) {
7341
8058
  log("Done");
7342
8059
  }
7343
8060
  }
8061
+ // src/utils/capability-meta.ts
8062
+ function capabilityTitle(capability) {
8063
+ return capability.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
8064
+ }
8065
+ function capabilitySummary(capability) {
8066
+ switch (capability) {
8067
+ case "code_search":
8068
+ return "Search and retrieve source-code context.";
8069
+ case "docs":
8070
+ return "Query and read technical documentation.";
8071
+ case "browser_automation":
8072
+ return "Automate browser interactions and diagnostics.";
8073
+ case "issue_tracking":
8074
+ return "Work with issues, tickets, and project tracking.";
8075
+ case "cms_content":
8076
+ return "Manage content and CMS resources.";
8077
+ case "design":
8078
+ return "Create and inspect design artifacts and visuals.";
8079
+ case "ai_media_generation":
8080
+ return "Generate and edit images and media using AI models.";
8081
+ case "hosting_deploy":
8082
+ return "Manage deployments, hosting, and infrastructure operations.";
8083
+ case "time_util":
8084
+ return "Resolve time, timezone, and date utilities.";
8085
+ case "research":
8086
+ return "Run web/research collection and synthesis operations.";
8087
+ default:
8088
+ return "Run general-purpose capability actions.";
8089
+ }
8090
+ }
8091
+
7344
8092
  // src/server/monitor-server.ts
7345
8093
  import { existsSync as existsSync10, unlinkSync as unlinkSync5 } from "fs";
7346
8094
  import { createServer as createServer3 } from "net";
@@ -7778,12 +8526,16 @@ class StatsCollector {
7778
8526
  }
7779
8527
 
7780
8528
  // src/server/index.ts
8529
+ var DESCRIBE_ACTION2 = "__describe_actions";
8530
+ function isRecord3(value) {
8531
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8532
+ }
8533
+
7781
8534
  class McpSquaredServer {
7782
8535
  mcpServer;
7783
8536
  cataloger;
7784
8537
  retriever;
7785
8538
  config;
7786
- maxLimit;
7787
8539
  transport = null;
7788
8540
  ownsCataloger;
7789
8541
  selectionTracker;
@@ -7797,6 +8549,8 @@ class McpSquaredServer {
7797
8549
  safetyAgent;
7798
8550
  obsSink;
7799
8551
  guard;
8552
+ baseToolsRegistered = false;
8553
+ computedCapabilityOverrides = {};
7800
8554
  constructor(options = {}) {
7801
8555
  const name = options.name ?? "mcp-squared";
7802
8556
  const version = options.version ?? VERSION;
@@ -7811,11 +8565,11 @@ class McpSquaredServer {
7811
8565
  }
7812
8566
  this.config = options.config ?? DEFAULT_CONFIG;
7813
8567
  const findToolsConfig = this.config.operations.findTools;
7814
- this.maxLimit = options.maxLimit ?? findToolsConfig.maxLimit;
8568
+ const retrieverMaxLimit = options.maxLimit ?? findToolsConfig.maxLimit;
7815
8569
  this.retriever = new Retriever(this.cataloger, {
7816
8570
  indexDbPath: options.indexDbPath,
7817
8571
  defaultLimit: options.defaultLimit ?? findToolsConfig.defaultLimit,
7818
- maxLimit: this.maxLimit,
8572
+ maxLimit: retrieverMaxLimit,
7819
8573
  defaultMode: findToolsConfig.defaultMode
7820
8574
  });
7821
8575
  const safetyEnv = readSafetyEnv();
@@ -7866,14 +8620,13 @@ class McpSquaredServer {
7866
8620
  });
7867
8621
  }
7868
8622
  });
7869
- this.registerMetaTools(this.mcpServer);
7870
8623
  }
7871
8624
  createMcpServer(name, version) {
7872
8625
  return new McpServer({
7873
8626
  name,
7874
8627
  version,
7875
- title: "MCP\xB2 Meta Router",
7876
- description: "Discover and execute tools across upstream MCP servers through a compact meta-tool interface."
8628
+ title: "MCP\xB2 Capability Router",
8629
+ description: "Execute capability-first tools routed to connected upstream MCP servers."
7877
8630
  }, {
7878
8631
  capabilities: {
7879
8632
  tools: {}
@@ -7882,73 +8635,21 @@ class McpSquaredServer {
7882
8635
  });
7883
8636
  }
7884
8637
  buildServerInstructions() {
7885
- const codeSearchNamespaces = this.getCodeSearchNamespaces();
7886
- const codeSearchHint = codeSearchNamespaces.length > 0 ? ` Prefer these configured code-search namespaces when relevant: ${codeSearchNamespaces.join(", ")}.` : "";
7887
8638
  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`."
8639
+ "Tool surface is generated at connect time from inferred upstream capabilities.",
8640
+ "Each capability tool accepts `action`, `arguments`, and optional `confirmation_token`.",
8641
+ 'Call a capability tool with `action = "__describe_actions"` to inspect available actions and schemas.',
8642
+ "Use returned action IDs for execution calls; if disambiguation is required, choose one candidate action and retry."
7892
8643
  ].join(" ");
7893
8644
  }
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;
7929
- }
7930
- return b.score - a.score;
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;
7946
- }
7947
8645
  createSessionServer() {
7948
8646
  const server = this.createMcpServer(this.serverName, this.serverVersion);
7949
- this.registerMetaTools(server);
8647
+ this.registerConfiguredToolSurface(server);
7950
8648
  return server;
7951
8649
  }
8650
+ registerConfiguredToolSurface(server) {
8651
+ this.registerCapabilityRouters(server);
8652
+ }
7952
8653
  runTaskSpan(taskName, run) {
7953
8654
  return task_span(this.obsSink, {
7954
8655
  agent: this.safetyAgent,
@@ -7957,389 +8658,239 @@ class McpSquaredServer {
7957
8658
  env: this.guard.agentEnv
7958
8659
  }, run);
7959
8660
  }
7960
- filterToolsByPolicy(tools) {
7961
- return tools.map((tool) => {
7962
- const visibility = getToolVisibilityCompiled(tool.serverKey, tool.name, this.compiledPolicy);
7963
- if (!visibility.visible) {
7964
- return null;
7965
- }
7966
- return visibility.requiresConfirmation ? { ...tool, requiresConfirmation: true } : tool;
7967
- }).filter((t) => t !== null);
7968
- }
7969
- registerMetaTools(server) {
7970
- server.registerTool("find_tools", {
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
- },
7977
- inputSchema: {
7978
- query: z3.string().describe('Natural language query describing the task (for example: "code search", "find symbol", "create issue")'),
7979
- limit: z3.number().int().min(1).max(this.maxLimit).default(this.config.operations.findTools.defaultLimit).describe("Maximum number of results to return"),
7980
- mode: SearchModeSchema.optional().describe('Search mode: "fast" (FTS5), "semantic" (embeddings), or "hybrid" (FTS5 + rerank)'),
7981
- detail_level: DetailLevelSchema.optional().describe('Level of detail: "L0" (name only), "L1" (summary with description, default), "L2" (full schema)')
7982
- }
7983
- }, async (args) => this.runTaskSpan("find_tools", async () => {
7984
- const requestId = this.statsCollector.startRequest();
7985
- const startTime = Date.now();
7986
- let success = false;
7987
- try {
7988
- const result = await this.retriever.search(args.query, {
7989
- limit: args.limit,
7990
- mode: args.mode
7991
- });
7992
- const filteredTools = this.filterToolsByPolicy(result.tools);
7993
- const rankedTools = this.rankToolsForQuery(filteredTools, args.query);
7994
- const detailLevel = args.detail_level ?? this.config.operations.findTools.defaultDetailLevel;
7995
- const tools = this.formatToolsForDetailLevel(rankedTools, detailLevel);
7996
- const guidance = this.buildFindToolsGuidance(args.query);
7997
- const selectionCacheConfig = this.config.operations.selectionCache;
7998
- let suggestedTools;
7999
- if (selectionCacheConfig.enabled && selectionCacheConfig.maxBundleSuggestions > 0) {
8000
- const toolKeys = filteredTools.map((t) => `${t.serverKey}:${t.name}`);
8001
- const suggestions = this.retriever.getIndexStore().getSuggestedBundles(toolKeys, selectionCacheConfig.minCooccurrenceThreshold, selectionCacheConfig.maxBundleSuggestions);
8002
- if (suggestions.length > 0) {
8003
- suggestedTools = suggestions.map((s) => ({
8004
- tools: [s.toolKey],
8005
- frequency: s.count
8006
- }));
8007
- }
8008
- }
8009
- success = true;
8010
- return {
8011
- content: [
8012
- {
8013
- type: "text",
8014
- text: JSON.stringify({
8015
- query: result.query,
8016
- totalMatches: filteredTools.length,
8017
- detailLevel,
8018
- searchMode: result.searchMode,
8019
- embeddingsAvailable: this.retriever.hasEmbeddings(),
8020
- tools,
8021
- guidance,
8022
- ...suggestedTools && { suggestedTools }
8023
- })
8024
- }
8025
- ]
8026
- };
8027
- } finally {
8028
- const responseTime = Date.now() - startTime;
8029
- this.statsCollector.endRequest(requestId, success, responseTime, "find_tools", "mcp-squared");
8030
- }
8031
- }));
8032
- server.registerTool("describe_tools", {
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
- },
8039
- inputSchema: {
8040
- tool_names: z3.array(z3.string()).min(1).max(20).describe("List of tool names to get schemas for")
8661
+ capabilityTitle(capability) {
8662
+ return capabilityTitle(capability);
8663
+ }
8664
+ capabilitySummary(capability) {
8665
+ return capabilitySummary(capability);
8666
+ }
8667
+ actionSummary(description, capability) {
8668
+ if (typeof description === "string") {
8669
+ const singleLine = description.split(/\r?\n/, 1)[0]?.trim() ?? "";
8670
+ if (singleLine.length > 0) {
8671
+ return singleLine;
8041
8672
  }
8042
- }, async (args) => this.runTaskSpan("describe_tools", async () => {
8043
- const requestId = this.statsCollector.startRequest();
8044
- const startTime = Date.now();
8045
- let success = false;
8046
- try {
8047
- const result = this.retriever.getTools(args.tool_names);
8048
- const filteredTools = this.filterToolsByPolicy(result.tools);
8049
- const schemas = filteredTools.map((tool) => ({
8050
- name: tool.name,
8051
- qualifiedName: `${tool.serverKey}:${tool.name}`,
8052
- description: tool.description,
8053
- serverKey: tool.serverKey,
8054
- inputSchema: tool.inputSchema,
8055
- ...tool.requiresConfirmation && { requiresConfirmation: true }
8056
- }));
8057
- const toolKey = (tool) => `${tool.serverKey}:${tool.name}`;
8058
- const filteredKeys = new Set(filteredTools.map(toolKey));
8059
- const blocked = result.tools.filter((tool) => !filteredKeys.has(toolKey(tool))).map(toolKey);
8060
- const foundQualified = new Set(result.tools.map(toolKey));
8061
- const foundBare = new Set(result.tools.map((t) => t.name));
8062
- const ambiguousNames = new Set(result.ambiguous.map((a) => a.name));
8063
- const notFound = args.tool_names.filter((name) => !ambiguousNames.has(name) && !foundQualified.has(name) && !foundBare.has(name));
8064
- success = true;
8065
- return {
8066
- content: [
8067
- {
8068
- type: "text",
8069
- text: JSON.stringify({
8070
- schemas,
8071
- ambiguous: result.ambiguous.length > 0 ? result.ambiguous : undefined,
8072
- notFound: notFound.length > 0 ? notFound : undefined,
8073
- blocked: blocked.length > 0 ? blocked : undefined
8074
- })
8075
- }
8076
- ]
8077
- };
8078
- } finally {
8079
- const responseTime = Date.now() - startTime;
8080
- this.statsCollector.endRequest(requestId, success, responseTime, "describe_tools", "mcp-squared");
8673
+ }
8674
+ return `Execute ${this.capabilityTitle(capability)} action`;
8675
+ }
8676
+ buildCapabilityRouters() {
8677
+ const status = this.cataloger.getStatus();
8678
+ const inventories = [...status.entries()].filter(([, info]) => info.status === "connected").map(([namespace]) => ({
8679
+ namespace,
8680
+ tools: this.cataloger.getToolsForServer(namespace)
8681
+ })).sort((a, b) => a.namespace.localeCompare(b.namespace));
8682
+ if (inventories.length === 0) {
8683
+ return [];
8684
+ }
8685
+ const overrides = {
8686
+ ...this.computedCapabilityOverrides,
8687
+ ...this.config.operations.dynamicToolSurface.capabilityOverrides
8688
+ };
8689
+ const grouping = groupNamespacesByCapability(inventories, overrides);
8690
+ return buildCapabilityRouters(inventories, grouping, (desc, cap) => this.actionSummary(desc, cap));
8691
+ }
8692
+ registerCapabilityRouters(server) {
8693
+ const routers = this.buildCapabilityRouters();
8694
+ for (const router of routers) {
8695
+ if (router.actions.length === 0) {
8696
+ continue;
8081
8697
  }
8082
- }));
8083
- server.registerTool("execute", {
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
- },
8091
- inputSchema: {
8092
- tool_name: z3.string().describe("Name of the tool to execute"),
8093
- arguments: z3.record(z3.string(), z3.unknown()).default({}).describe("Arguments to pass to the tool"),
8094
- confirmation_token: z3.string().optional().describe("Optional confirmation token for tools that require explicit confirmation")
8095
- }
8096
- }, async (args) => this.runTaskSpan("execute", async () => {
8097
- const requestId = this.statsCollector.startRequest();
8098
- const startTime = Date.now();
8099
- let success = false;
8100
- let toolName;
8101
- let serverKey;
8102
- try {
8103
- const lookupResult = this.cataloger.findTool(args.tool_name);
8104
- if (lookupResult.ambiguous) {
8105
- return {
8106
- content: [
8107
- {
8108
- type: "text",
8109
- text: JSON.stringify({
8110
- error: `Ambiguous tool name "${args.tool_name}". Use a qualified name.`,
8111
- alternatives: lookupResult.alternatives
8112
- })
8113
- }
8114
- ],
8115
- isError: true
8116
- };
8698
+ server.registerTool(router.capability, {
8699
+ title: this.capabilityTitle(router.capability),
8700
+ description: this.capabilitySummary(router.capability),
8701
+ annotations: {
8702
+ readOnlyHint: false,
8703
+ destructiveHint: false,
8704
+ openWorldHint: true
8705
+ },
8706
+ inputSchema: {
8707
+ action: z3.string().describe(`Action ID for ${router.capability}. Use "${DESCRIBE_ACTION2}" to inspect available actions and schemas.`),
8708
+ arguments: z3.record(z3.string(), z3.unknown()).default({}).describe("Arguments for the selected capability action"),
8709
+ confirmation_token: z3.string().optional().describe("Optional confirmation token for actions that require explicit confirmation")
8117
8710
  }
8118
- if (!lookupResult.tool) {
8119
- return {
8120
- content: [
8121
- {
8122
- type: "text",
8123
- text: JSON.stringify({
8124
- error: `Tool not found: ${args.tool_name}`
8125
- })
8126
- }
8127
- ],
8128
- isError: true
8129
- };
8130
- }
8131
- const tool = lookupResult.tool;
8132
- toolName = tool.name;
8133
- serverKey = tool.serverKey;
8134
- const policyResult = evaluatePolicy({
8135
- serverKey: tool.serverKey,
8136
- toolName: tool.name,
8137
- confirmationToken: args.confirmation_token
8138
- }, this.config);
8139
- if (policyResult.decision === "block") {
8711
+ }, async (rawArgs) => this.runTaskSpan(router.capability, async () => {
8712
+ const requestId = this.statsCollector.startRequest();
8713
+ const startTime = Date.now();
8714
+ let success = false;
8715
+ try {
8716
+ const parsedArgs = isRecord3(rawArgs) ? { ...rawArgs } : {};
8717
+ const action = typeof parsedArgs["action"] === "string" ? parsedArgs["action"] : "";
8718
+ const confirmationToken = typeof parsedArgs["confirmation_token"] === "string" ? parsedArgs["confirmation_token"] : undefined;
8719
+ const actionArgs = isRecord3(parsedArgs["arguments"]) ? parsedArgs["arguments"] : {};
8720
+ if (action.length === 0) {
8721
+ return {
8722
+ content: [
8723
+ {
8724
+ type: "text",
8725
+ text: JSON.stringify({
8726
+ error: "Missing required action",
8727
+ capability: router.capability
8728
+ })
8729
+ }
8730
+ ],
8731
+ isError: true
8732
+ };
8733
+ }
8734
+ const visibleActions = router.actions.map((route2) => {
8735
+ const visibility = getToolVisibilityCompiled(router.capability, route2.action, this.compiledPolicy);
8736
+ if (!visibility.visible) {
8737
+ return null;
8738
+ }
8739
+ return {
8740
+ action: route2.action,
8741
+ summary: route2.summary,
8742
+ inputSchema: route2.inputSchema,
8743
+ requiresConfirmation: visibility.requiresConfirmation
8744
+ };
8745
+ }).filter((entry) => entry !== null).sort((a, b) => a.action.localeCompare(b.action));
8746
+ if (action === DESCRIBE_ACTION2) {
8747
+ success = true;
8748
+ return {
8749
+ content: [
8750
+ {
8751
+ type: "text",
8752
+ text: JSON.stringify({
8753
+ capability: router.capability,
8754
+ actions: visibleActions,
8755
+ totalActions: visibleActions.length
8756
+ })
8757
+ }
8758
+ ]
8759
+ };
8760
+ }
8761
+ const ambiguousCandidates = router.actions.filter((route2) => route2.baseAction === action).map((route2) => route2.action).sort((a, b) => a.localeCompare(b));
8762
+ if (ambiguousCandidates.length > 1) {
8763
+ return {
8764
+ content: [
8765
+ {
8766
+ type: "text",
8767
+ text: JSON.stringify({
8768
+ requires_disambiguation: true,
8769
+ capability: router.capability,
8770
+ action,
8771
+ candidates: ambiguousCandidates
8772
+ })
8773
+ }
8774
+ ],
8775
+ isError: true
8776
+ };
8777
+ }
8778
+ const route = router.actions.find((entry) => entry.action === action);
8779
+ if (!route) {
8780
+ return {
8781
+ content: [
8782
+ {
8783
+ type: "text",
8784
+ text: JSON.stringify({
8785
+ error: "Unknown action",
8786
+ capability: router.capability,
8787
+ action,
8788
+ availableActions: visibleActions.map((a) => a.action)
8789
+ })
8790
+ }
8791
+ ],
8792
+ isError: true
8793
+ };
8794
+ }
8795
+ const callResult = await this.executeRoutedTool({
8796
+ capability: router.capability,
8797
+ action: route.action,
8798
+ qualifiedToolName: route.qualifiedName,
8799
+ toolNameForCall: route.qualifiedName,
8800
+ args: actionArgs,
8801
+ ...confirmationToken != null ? { confirmationToken } : {}
8802
+ });
8803
+ success = !callResult.isError;
8804
+ return callResult;
8805
+ } catch {
8140
8806
  return {
8141
8807
  content: [
8142
8808
  {
8143
8809
  type: "text",
8144
8810
  text: JSON.stringify({
8145
- error: policyResult.reason,
8146
- blocked: true
8811
+ error: "Action execution failed"
8147
8812
  })
8148
8813
  }
8149
8814
  ],
8150
8815
  isError: true
8151
8816
  };
8817
+ } finally {
8818
+ const responseTime = Date.now() - startTime;
8819
+ this.statsCollector.endRequest(requestId, success, responseTime, router.capability, "capability");
8152
8820
  }
8153
- if (policyResult.decision === "confirm") {
8154
- return {
8155
- content: [
8156
- {
8157
- type: "text",
8158
- text: JSON.stringify({
8159
- requires_confirmation: true,
8160
- confirmation_token: policyResult.confirmationToken,
8161
- message: policyResult.reason
8162
- })
8163
- }
8164
- ],
8165
- isError: false
8166
- };
8167
- }
8168
- const qualifiedToolName = `${tool.serverKey}:${tool.name}`;
8169
- this.guard.enforce({
8170
- agent: this.safetyAgent,
8171
- tool: qualifiedToolName,
8172
- action: "call",
8173
- params: args.arguments
8174
- });
8175
- const result = await tool_span(this.obsSink, {
8176
- agent: this.safetyAgent,
8177
- tool: qualifiedToolName,
8178
- action: "call",
8179
- playbook: this.guard.playbook,
8180
- env: this.guard.agentEnv
8181
- }, () => this.cataloger.callTool(args.tool_name, args.arguments));
8182
- if (!result.isError && this.config.operations.selectionCache.enabled) {
8183
- const toolKey = `${tool.serverKey}:${tool.name}`;
8184
- this.selectionTracker.trackToolUsage(toolKey);
8185
- if (this.selectionTracker.getSessionToolCount() >= 2) {
8186
- this.selectionTracker.flushToStore(this.retriever.getIndexStore());
8187
- }
8188
- }
8189
- success = !result.isError;
8190
- return {
8191
- content: result.content.map((c) => {
8192
- if (typeof c === "object" && c !== null && "type" in c) {
8193
- return c;
8194
- }
8195
- return {
8196
- type: "text",
8197
- text: JSON.stringify(c)
8198
- };
8199
- }),
8200
- isError: result.isError
8201
- };
8202
- } catch (err) {
8203
- const errorMessage2 = err instanceof Error ? err.message : String(err);
8204
- return {
8205
- content: [
8206
- {
8207
- type: "text",
8208
- text: JSON.stringify({
8209
- error: errorMessage2
8210
- })
8211
- }
8212
- ],
8213
- isError: true
8214
- };
8215
- } finally {
8216
- const responseTime = Date.now() - startTime;
8217
- this.statsCollector.endRequest(requestId, success, responseTime, toolName, serverKey);
8218
- }
8219
- }));
8220
- server.registerTool("clear_selection_cache", {
8221
- title: "Reset Selection Cache",
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
- },
8229
- inputSchema: {}
8230
- }, async () => this.runTaskSpan("clear_selection_cache", async () => {
8231
- const requestId = this.statsCollector.startRequest();
8232
- const startTime = Date.now();
8233
- let success = false;
8234
- try {
8235
- const countBefore = this.retriever.getIndexStore().getCooccurrenceCount();
8236
- this.retriever.getIndexStore().clearCooccurrences();
8237
- this.selectionTracker.reset();
8238
- success = true;
8239
- return {
8240
- content: [
8241
- {
8242
- type: "text",
8243
- text: JSON.stringify({
8244
- message: "Selection cache cleared",
8245
- patternsRemoved: countBefore
8246
- })
8247
- }
8248
- ]
8249
- };
8250
- } finally {
8251
- const responseTime = Date.now() - startTime;
8252
- this.statsCollector.endRequest(requestId, success, responseTime, "clear_selection_cache", "mcp-squared");
8253
- }
8254
- }));
8255
- server.registerTool("list_namespaces", {
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
- },
8262
- inputSchema: {
8263
- include_tools: z3.boolean().default(false).describe("If true, includes the list of tool names available in each namespace")
8821
+ }));
8822
+ }
8823
+ }
8824
+ normalizeToolResultContent(content) {
8825
+ return content.map((entry) => {
8826
+ if (typeof entry === "object" && entry !== null && "type" in entry) {
8827
+ return entry;
8264
8828
  }
8265
- }, async (args) => this.runTaskSpan("list_namespaces", async () => {
8266
- const requestId = this.statsCollector.startRequest();
8267
- const startTime = Date.now();
8268
- let success = false;
8269
- try {
8270
- const status = this.cataloger.getStatus();
8271
- const namespaces = [];
8272
- for (const [key, info] of status) {
8273
- const tools = this.cataloger.getToolsForServer(key);
8274
- const namespace = {
8275
- name: key,
8276
- status: info.status,
8277
- toolCount: tools.length
8278
- };
8279
- if (info.error) {
8280
- namespace.error = info.error;
8829
+ return {
8830
+ type: "text",
8831
+ text: JSON.stringify(entry)
8832
+ };
8833
+ });
8834
+ }
8835
+ async executeRoutedTool(args) {
8836
+ const policyResult = evaluatePolicy({
8837
+ capability: args.capability,
8838
+ action: args.action,
8839
+ confirmationToken: args.confirmationToken
8840
+ }, this.config);
8841
+ if (policyResult.decision === "block") {
8842
+ return {
8843
+ content: [
8844
+ {
8845
+ type: "text",
8846
+ text: JSON.stringify({
8847
+ error: "Action blocked by security policy",
8848
+ blocked: true
8849
+ })
8281
8850
  }
8282
- if (args.include_tools && tools.length > 0) {
8283
- namespace.tools = tools.map((t) => t.name);
8851
+ ],
8852
+ isError: true
8853
+ };
8854
+ }
8855
+ if (policyResult.decision === "confirm") {
8856
+ return {
8857
+ content: [
8858
+ {
8859
+ type: "text",
8860
+ text: JSON.stringify({
8861
+ requires_confirmation: true,
8862
+ confirmation_token: policyResult.confirmationToken,
8863
+ message: "Action requires confirmation by security policy"
8864
+ })
8284
8865
  }
8285
- namespaces.push(namespace);
8286
- }
8287
- const conflicts = this.cataloger.getConflictingTools();
8288
- const conflictingTools = {};
8289
- for (const [toolName, qualifiedNames] of conflicts) {
8290
- conflictingTools[toolName] = qualifiedNames;
8291
- }
8292
- success = true;
8293
- return {
8294
- content: [
8295
- {
8296
- type: "text",
8297
- text: JSON.stringify({
8298
- namespaces,
8299
- totalNamespaces: namespaces.length,
8300
- connectedCount: namespaces.filter((n) => n.status === "connected").length,
8301
- ...Object.keys(conflictingTools).length > 0 && {
8302
- conflictingTools,
8303
- conflictNote: "These tools exist on multiple servers. Use qualified names (namespace:tool_name) to disambiguate."
8304
- }
8305
- })
8306
- }
8307
- ]
8308
- };
8309
- } finally {
8310
- const responseTime = Date.now() - startTime;
8311
- this.statsCollector.endRequest(requestId, success, responseTime, "list_namespaces", "mcp-squared");
8312
- }
8313
- }));
8314
- }
8315
- formatToolsForDetailLevel(tools, level) {
8316
- switch (level) {
8317
- case "L0":
8318
- return tools.map((t) => ({
8319
- name: t.name,
8320
- serverKey: t.serverKey,
8321
- ...t.requiresConfirmation && { requiresConfirmation: true }
8322
- }));
8323
- case "L2": {
8324
- return tools.map((t) => {
8325
- const { tool } = this.cataloger.findTool(`${t.serverKey}:${t.name}`);
8326
- return {
8327
- name: t.name,
8328
- description: t.description,
8329
- serverKey: t.serverKey,
8330
- inputSchema: tool?.inputSchema ?? { type: "object" },
8331
- ...t.requiresConfirmation && { requiresConfirmation: true }
8332
- };
8333
- });
8866
+ ],
8867
+ isError: false
8868
+ };
8869
+ }
8870
+ this.guard.enforce({
8871
+ agent: this.safetyAgent,
8872
+ tool: `${args.capability}:${args.action}`,
8873
+ action: "call",
8874
+ params: args.args
8875
+ });
8876
+ const result = await tool_span(this.obsSink, {
8877
+ agent: this.safetyAgent,
8878
+ tool: `${args.capability}:${args.action}`,
8879
+ action: "call",
8880
+ playbook: this.guard.playbook,
8881
+ env: this.guard.agentEnv
8882
+ }, () => this.cataloger.callTool(args.toolNameForCall, args.args));
8883
+ if (!result.isError && this.config.operations.selectionCache.enabled) {
8884
+ const toolKey = `${args.capability}:${args.action}`;
8885
+ this.selectionTracker.trackToolUsage(toolKey);
8886
+ if (this.selectionTracker.getSessionToolCount() >= 2) {
8887
+ this.selectionTracker.flushToStore(this.retriever.getIndexStore());
8334
8888
  }
8335
- default:
8336
- return tools.map((t) => ({
8337
- name: t.name,
8338
- description: t.description,
8339
- serverKey: t.serverKey,
8340
- ...t.requiresConfirmation && { requiresConfirmation: true }
8341
- }));
8342
8889
  }
8890
+ return {
8891
+ content: this.normalizeToolResultContent(result.content),
8892
+ isError: result.isError ?? false
8893
+ };
8343
8894
  }
8344
8895
  syncIndex() {
8345
8896
  this.retriever.syncFromCataloger();
@@ -8363,8 +8914,39 @@ class McpSquaredServer {
8363
8914
  getToolStats(limit = 100) {
8364
8915
  return this.statsCollector.getToolStats(limit);
8365
8916
  }
8917
+ async classifyNamespacesSemantic() {
8918
+ const generator = this.retriever.getEmbeddingGenerator();
8919
+ if (!generator) {
8920
+ console.error("[mcp\xB2] Hybrid inference: embeddings not available, falling back to heuristic.");
8921
+ return;
8922
+ }
8923
+ try {
8924
+ const { SemanticCapabilityClassifier: SemanticCapabilityClassifier2 } = await Promise.resolve().then(() => (init_semantic_classifier(), exports_semantic_classifier));
8925
+ const threshold = this.config.operations.dynamicToolSurface.semanticConfidenceThreshold;
8926
+ const classifier = new SemanticCapabilityClassifier2(generator, {
8927
+ confidenceThreshold: threshold
8928
+ });
8929
+ await classifier.initializeReferences();
8930
+ const status = this.cataloger.getStatus();
8931
+ const inventories = [...status.entries()].filter(([, info]) => info.status === "connected").map(([namespace]) => ({
8932
+ namespace,
8933
+ tools: this.cataloger.getToolsForServer(namespace)
8934
+ }));
8935
+ const result = await classifier.classifyBatch(inventories);
8936
+ this.computedCapabilityOverrides = result.overrides;
8937
+ const count = Object.keys(result.overrides).length;
8938
+ console.error(`[mcp\xB2] Hybrid inference: classified ${count}/${inventories.length} namespaces semantically (${Math.round(result.inferenceMs)}ms).`);
8939
+ } catch (err) {
8940
+ const message = err instanceof Error ? err.message : String(err);
8941
+ console.error(`[mcp\xB2] Hybrid inference: classification failed \u2014 ${message}. Falling back to heuristic.`);
8942
+ }
8943
+ }
8366
8944
  async start() {
8367
8945
  await this.startCore();
8946
+ if (!this.baseToolsRegistered) {
8947
+ this.registerConfiguredToolSurface(this.mcpServer);
8948
+ this.baseToolsRegistered = true;
8949
+ }
8368
8950
  this.transport = new StdioServerTransport2;
8369
8951
  await this.mcpServer.connect(this.transport);
8370
8952
  this.statsCollector.incrementActiveConnections();
@@ -8402,6 +8984,9 @@ class McpSquaredServer {
8402
8984
  console.error(`[mcp\xB2] Embeddings: initialization failed \u2014 ${message}. Falling back to fast (FTS5) search.`);
8403
8985
  }
8404
8986
  }
8987
+ if (this.config.operations.dynamicToolSurface.inference === "hybrid") {
8988
+ await this.classifyNamespacesSemantic();
8989
+ }
8405
8990
  this.statsCollector.updateIndexRefreshTime(Date.now());
8406
8991
  this.indexRefreshManager.start();
8407
8992
  await this.monitorServer.start();
@@ -8429,6 +9014,249 @@ class McpSquaredServer {
8429
9014
  }
8430
9015
  }
8431
9016
 
9017
+ // src/status/runner.ts
9018
+ init_inference();
9019
+
9020
+ // src/utils/context-stats.ts
9021
+ var CHARS_PER_TOKEN_ESTIMATE = 4;
9022
+ function estimateTokens(text) {
9023
+ return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE);
9024
+ }
9025
+ function reconstructCapabilityToolMetadata(router) {
9026
+ return {
9027
+ name: router.capability,
9028
+ title: capabilityTitle(router.capability),
9029
+ description: capabilitySummary(router.capability),
9030
+ annotations: {
9031
+ readOnlyHint: false,
9032
+ destructiveHint: false,
9033
+ openWorldHint: true
9034
+ },
9035
+ inputSchema: {
9036
+ type: "object",
9037
+ properties: {
9038
+ action: {
9039
+ type: "string",
9040
+ description: `Action ID for ${router.capability}. Use "__describe_actions" to inspect available actions and schemas.`
9041
+ },
9042
+ arguments: {
9043
+ type: "object",
9044
+ additionalProperties: {},
9045
+ default: {},
9046
+ description: "Arguments for the selected capability action"
9047
+ },
9048
+ confirmation_token: {
9049
+ type: "string",
9050
+ description: "Optional confirmation token for actions that require explicit confirmation"
9051
+ }
9052
+ },
9053
+ required: ["action"]
9054
+ }
9055
+ };
9056
+ }
9057
+ function computeContextStats(upstreamTools, routers) {
9058
+ const withoutTokens = upstreamTools.reduce((sum, tool) => {
9059
+ const serialized = JSON.stringify({
9060
+ name: tool.name,
9061
+ description: tool.description,
9062
+ inputSchema: tool.inputSchema
9063
+ });
9064
+ return sum + estimateTokens(serialized);
9065
+ }, 0);
9066
+ const activeRouters = routers.filter((r) => r.actions.length > 0);
9067
+ const withTokens = activeRouters.reduce((sum, router) => {
9068
+ const meta = reconstructCapabilityToolMetadata(router);
9069
+ return sum + estimateTokens(JSON.stringify(meta));
9070
+ }, 0);
9071
+ const saved = withoutTokens - withTokens;
9072
+ const percent = withoutTokens > 0 ? saved / withoutTokens * 100 : 0;
9073
+ return {
9074
+ withoutMcp2Tokens: withoutTokens,
9075
+ withMcp2Tokens: withTokens,
9076
+ savedTokens: saved,
9077
+ savedPercent: Math.round(percent * 10) / 10,
9078
+ upstreamToolCount: upstreamTools.length,
9079
+ capabilityToolCount: activeRouters.length
9080
+ };
9081
+ }
9082
+
9083
+ // src/status/runner.ts
9084
+ var GREEN = "\x1B[32m";
9085
+ var YELLOW = "\x1B[33m";
9086
+ var RED = "\x1B[31m";
9087
+ var DIM = "\x1B[90m";
9088
+ var BOLD = "\x1B[1m";
9089
+ var RESET = "\x1B[0m";
9090
+ async function collectStatus(config) {
9091
+ const cataloger = new Cataloger({ connectTimeoutMs: 15000 });
9092
+ const upstreams = [];
9093
+ try {
9094
+ await cataloger.connectAll(config);
9095
+ const status = cataloger.getStatus();
9096
+ for (const [name, serverConfig] of Object.entries(config.upstreams)) {
9097
+ if (!serverConfig.enabled) {
9098
+ upstreams.push({
9099
+ name,
9100
+ enabled: false,
9101
+ status: "disconnected",
9102
+ toolCount: 0
9103
+ });
9104
+ continue;
9105
+ }
9106
+ const connStatus = status.get(name);
9107
+ const connection = cataloger.getConnection(name);
9108
+ upstreams.push({
9109
+ name,
9110
+ enabled: true,
9111
+ status: connStatus?.status ?? "disconnected",
9112
+ error: connStatus?.error,
9113
+ toolCount: connection?.tools.length ?? 0,
9114
+ serverName: connection?.serverName,
9115
+ serverVersion: connection?.serverVersion
9116
+ });
9117
+ }
9118
+ const inventories = [...status.entries()].filter(([, info]) => info.status === "connected").map(([namespace]) => ({
9119
+ namespace,
9120
+ tools: cataloger.getToolsForServer(namespace)
9121
+ })).sort((a, b) => a.namespace.localeCompare(b.namespace));
9122
+ let routers = [];
9123
+ if (inventories.length > 0) {
9124
+ const overrides = config.operations.dynamicToolSurface.capabilityOverrides ?? {};
9125
+ const grouping = groupNamespacesByCapability(inventories, overrides);
9126
+ routers = buildCapabilityRouters(inventories, grouping);
9127
+ }
9128
+ const allUpstreamTools = inventories.flatMap((inv) => inv.tools);
9129
+ const contextStats = computeContextStats(allUpstreamTools, routers);
9130
+ return { upstreams, routers, contextStats };
9131
+ } finally {
9132
+ await cataloger.disconnectAll();
9133
+ }
9134
+ }
9135
+ function formatStatus(result, options) {
9136
+ const lines = [];
9137
+ lines.push("");
9138
+ lines.push(`${BOLD}MCP\xB2 Status Report${RESET}`);
9139
+ lines.push("");
9140
+ lines.push(`${DIM}\u2500\u2500 Upstream Servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET}`);
9141
+ if (result.upstreams.length === 0) {
9142
+ lines.push(` ${DIM}(no upstreams configured)${RESET}`);
9143
+ }
9144
+ for (const upstream of result.upstreams) {
9145
+ if (!upstream.enabled) {
9146
+ lines.push(` ${DIM}\u2298 ${upstream.name.padEnd(24)} disabled${RESET}`);
9147
+ continue;
9148
+ }
9149
+ if (upstream.status === "connected") {
9150
+ const toolInfo = `(${upstream.toolCount} tool${upstream.toolCount !== 1 ? "s" : ""})`;
9151
+ const version = upstream.serverVersion ? ` ${DIM}v${upstream.serverVersion}${RESET}` : "";
9152
+ lines.push(` ${GREEN}\u2713${RESET} ${upstream.name.padEnd(24)} connected ${toolInfo}${version}`);
9153
+ } else if (upstream.status === "needs_auth") {
9154
+ const errorMsg = upstream.error ?? "Authentication required";
9155
+ lines.push(` ${YELLOW}\u26A0${RESET} ${upstream.name.padEnd(24)} needs auth ${DIM}${errorMsg}${RESET}`);
9156
+ } else if (upstream.status === "error") {
9157
+ const errorMsg = upstream.error ?? "Unknown error";
9158
+ lines.push(` ${RED}\u2717${RESET} ${upstream.name.padEnd(24)} error ${DIM}${errorMsg}${RESET}`);
9159
+ } else {
9160
+ lines.push(` ${DIM}? ${upstream.name.padEnd(24)} ${upstream.status}${RESET}`);
9161
+ }
9162
+ }
9163
+ lines.push("");
9164
+ lines.push(`${DIM}\u2500\u2500 Capability Routing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET}`);
9165
+ if (result.routers.length === 0) {
9166
+ lines.push(` ${DIM}(no capabilities routed \u2014 no connected upstreams)${RESET}`);
9167
+ }
9168
+ let totalActions = 0;
9169
+ for (const router of result.routers) {
9170
+ const count = router.actions.length;
9171
+ totalActions += count;
9172
+ const plural = count !== 1 ? "s" : "";
9173
+ lines.push(` ${BOLD}${router.capability}${RESET} ${DIM}(${count} action${plural})${RESET}`);
9174
+ if (count === 0) {
9175
+ lines.push(` ${DIM}(no actions)${RESET}`);
9176
+ }
9177
+ for (const action of router.actions) {
9178
+ const actionName = action.action.padEnd(28);
9179
+ const mapping = `${DIM}\u2192${RESET} ${action.qualifiedName}`;
9180
+ if (options.verbose) {
9181
+ const schemaKeys = Object.keys(action.inputSchema?.properties ?? {});
9182
+ const params = schemaKeys.length > 0 ? ` ${DIM}(${schemaKeys.join(", ")})${RESET}` : "";
9183
+ lines.push(` ${actionName} ${mapping}${params}`);
9184
+ } else {
9185
+ lines.push(` ${actionName} ${mapping}`);
9186
+ }
9187
+ }
9188
+ lines.push("");
9189
+ }
9190
+ if (options.verbose && result.contextStats) {
9191
+ const cs = result.contextStats;
9192
+ if (cs.upstreamToolCount > 0) {
9193
+ lines.push(`${DIM}\u2500\u2500 Context Savings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET}`);
9194
+ const fmt = (n) => n.toLocaleString("en-US");
9195
+ const withoutLabel = `Without MCP\xB2:`;
9196
+ const withLabel = `With MCP\xB2:`;
9197
+ const savedLabel = `Saved:`;
9198
+ lines.push(` ${withoutLabel.padEnd(16)} ${fmt(cs.withoutMcp2Tokens).padStart(8)} tokens ${DIM}(${cs.upstreamToolCount} tool${cs.upstreamToolCount !== 1 ? "s" : ""})${RESET}`);
9199
+ lines.push(` ${withLabel.padEnd(16)} ${fmt(cs.withMcp2Tokens).padStart(8)} tokens ${DIM}(${cs.capabilityToolCount} tool${cs.capabilityToolCount !== 1 ? "s" : ""})${RESET}`);
9200
+ if (cs.savedTokens > 0) {
9201
+ lines.push(` ${DIM}${"\u2500".repeat(40)}${RESET}`);
9202
+ lines.push(` ${GREEN}${savedLabel.padEnd(16)} ${fmt(cs.savedTokens).padStart(8)} tokens (${cs.savedPercent}%)${RESET}`);
9203
+ }
9204
+ lines.push("");
9205
+ }
9206
+ }
9207
+ const connected = result.upstreams.filter((u) => u.status === "connected").length;
9208
+ const needsAuth = result.upstreams.filter((u) => u.status === "needs_auth").length;
9209
+ const errors = result.upstreams.filter((u) => u.status === "error").length;
9210
+ const disabled = result.upstreams.filter((u) => !u.enabled).length;
9211
+ const capCount = result.routers.filter((r) => r.actions.length > 0).length;
9212
+ const parts = [];
9213
+ if (connected > 0)
9214
+ parts.push(`${GREEN}${connected} connected${RESET}`);
9215
+ if (needsAuth > 0)
9216
+ parts.push(`${YELLOW}${needsAuth} needs auth${RESET}`);
9217
+ if (errors > 0)
9218
+ parts.push(`${RED}${errors} error${RESET}`);
9219
+ if (disabled > 0)
9220
+ parts.push(`${DIM}${disabled} disabled${RESET}`);
9221
+ const actionSummary = totalActions > 0 ? `${totalActions} action${totalActions !== 1 ? "s" : ""} across ${capCount} capabilit${capCount !== 1 ? "ies" : "y"}` : "no actions";
9222
+ lines.push(`${DIM}Summary:${RESET} ${parts.join(", ")} | ${actionSummary}`);
9223
+ lines.push("");
9224
+ return lines.join(`
9225
+ `);
9226
+ }
9227
+ async function runStatus(options) {
9228
+ let config;
9229
+ let configPath;
9230
+ try {
9231
+ const loaded = await loadConfig();
9232
+ config = loaded.config;
9233
+ configPath = loaded.path;
9234
+ } catch (err) {
9235
+ const message = err instanceof Error ? err.message : String(err);
9236
+ console.error(`Error loading configuration: ${message}`);
9237
+ console.error("Run 'mcp-squared config' to create or fix your configuration.");
9238
+ process.exit(1);
9239
+ }
9240
+ const upstreamEntries = Object.entries(config.upstreams);
9241
+ if (upstreamEntries.length === 0) {
9242
+ console.error("Error: No upstreams configured. Run 'mcp-squared config' to add one.");
9243
+ process.exit(1);
9244
+ }
9245
+ const validationIssues = validateConfig(config);
9246
+ if (validationIssues.length > 0) {
9247
+ console.error(formatValidationIssues(validationIssues));
9248
+ console.error("");
9249
+ }
9250
+ if (options.verbose && configPath) {
9251
+ console.log(`${DIM}Config: ${configPath}${RESET}`);
9252
+ }
9253
+ console.log("Connecting to upstream servers...");
9254
+ const result = await collectStatus(config);
9255
+ result.configPath = configPath;
9256
+ const output = formatStatus(result, options);
9257
+ console.log(output);
9258
+ }
9259
+
8432
9260
  // src/index.ts
8433
9261
  function logSecurityProfile(config) {
8434
9262
  const { allow, confirm } = config.security.tools;
@@ -9153,6 +9981,9 @@ async function main(argv = process.argv.slice(2)) {
9153
9981
  case "monitor":
9154
9982
  await runMonitor(args.monitor);
9155
9983
  break;
9984
+ case "status":
9985
+ await runStatus({ verbose: args.testVerbose });
9986
+ break;
9156
9987
  case "daemon":
9157
9988
  await runDaemon(args.daemon);
9158
9989
  break;