mcp-squared 0.6.0 → 0.7.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 CHANGED
@@ -222,6 +222,14 @@ Each capability tool uses a thin router contract:
222
222
 
223
223
  Every capability tool supports `action = "__describe_actions"` for introspection. This returns capability-local action IDs, summaries, and input schemas without exposing upstream namespace/tool identifiers.
224
224
 
225
+ When multiple upstream instances expose the same capability action, MCP² now
226
+ uses stable, instance-aware action IDs such as `create_issue__github_work`
227
+ instead of order-dependent numeric suffixes. `__describe_actions` also includes
228
+ `baseAction`, `instance`, and `instanceTitle` fields for colliding routes so
229
+ clients can distinguish duplicate accounts or environments. Legacy numeric
230
+ aliases such as `create_issue__2` are still accepted during the transition, but
231
+ the instance-aware IDs are the preferred public contract.
232
+
225
233
  Recommended workflow for LLM clients:
226
234
  1. Call the relevant capability (for example `code_search`) with `action = "__describe_actions"`.
227
235
  2. Select an action ID and call the same capability with `arguments`.
package/dist/index.js CHANGED
@@ -1530,6 +1530,14 @@ var LoggingSchema = z.object({
1530
1530
  var EmbeddingsSchema = z.object({
1531
1531
  enabled: z.boolean().default(false)
1532
1532
  });
1533
+ var ResponseResourceSchema = z.object({
1534
+ enabled: z.boolean().default(false),
1535
+ thresholdBytes: z.number().int().min(1024).default(51200),
1536
+ maxInlineLines: z.number().int().min(1).default(20),
1537
+ maxResources: z.number().int().min(1).default(100),
1538
+ ttlMs: z.number().int().min(0).default(600000)
1539
+ });
1540
+ var DEFAULT_RESPONSE_RESOURCE_CONFIG = ResponseResourceSchema.parse({});
1533
1541
  var SelectionCacheSchema = z.object({
1534
1542
  enabled: z.boolean().default(true),
1535
1543
  minCooccurrenceThreshold: z.number().int().min(1).default(2),
@@ -1546,6 +1554,7 @@ var OperationsSchema = z.object({
1546
1554
  index: IndexSchema.default({ refreshIntervalMs: 30000 }),
1547
1555
  logging: LoggingSchema.default({ level: "info" }),
1548
1556
  embeddings: EmbeddingsSchema.default({ enabled: false }),
1557
+ responseResource: ResponseResourceSchema.default(DEFAULT_RESPONSE_RESOURCE_CONFIG),
1549
1558
  selectionCache: SelectionCacheSchema.default({
1550
1559
  enabled: true,
1551
1560
  minCooccurrenceThreshold: 2,
@@ -1568,6 +1577,7 @@ var OperationsSchema = z.object({
1568
1577
  index: { refreshIntervalMs: 30000 },
1569
1578
  logging: { level: "info" },
1570
1579
  embeddings: { enabled: false },
1580
+ responseResource: DEFAULT_RESPONSE_RESOURCE_CONFIG,
1571
1581
  selectionCache: {
1572
1582
  enabled: true,
1573
1583
  minCooccurrenceThreshold: 2,
@@ -6490,6 +6500,44 @@ init_inference();
6490
6500
 
6491
6501
  // src/capabilities/routing.ts
6492
6502
  var DESCRIBE_ACTION = "__describe_actions";
6503
+ function buildCanonicalRouteId(capability, instanceKey, toolName) {
6504
+ return `${capability}:${instanceKey}:${toolName}`;
6505
+ }
6506
+ function buildInstanceActionBase(baseAction, instanceToken) {
6507
+ return `${baseAction}__${instanceToken}`;
6508
+ }
6509
+ function allocateUniqueActionId(emittedActions, actionBase, index) {
6510
+ let candidate = index === 0 ? actionBase : `${actionBase}__${index + 1}`;
6511
+ const suffixMatch = actionBase.match(/^(.*)__(\d+)$/);
6512
+ const dedupeBase = suffixMatch?.[1] ?? actionBase;
6513
+ let dedupe = suffixMatch?.[2] ? Number.parseInt(suffixMatch[2], 10) + 1 : index + 2;
6514
+ while (emittedActions.has(candidate)) {
6515
+ candidate = `${dedupeBase}__${dedupe}`;
6516
+ dedupe += 1;
6517
+ }
6518
+ emittedActions.add(candidate);
6519
+ return candidate;
6520
+ }
6521
+ function resolveInstanceTokens(records) {
6522
+ const byToken = new Map;
6523
+ for (const record of records) {
6524
+ const instanceKey = record.instanceKey ?? record.serverKey;
6525
+ const token = toActionToken2(instanceKey);
6526
+ const existing = byToken.get(token) ?? [];
6527
+ if (!existing.includes(instanceKey)) {
6528
+ existing.push(instanceKey);
6529
+ existing.sort((a, b) => a.localeCompare(b));
6530
+ byToken.set(token, existing);
6531
+ }
6532
+ }
6533
+ const resolved = new Map;
6534
+ for (const [token, instanceKeys] of [...byToken.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
6535
+ instanceKeys.forEach((instanceKey, index) => {
6536
+ resolved.set(instanceKey, index === 0 ? token : `${token}__${index + 1}`);
6537
+ });
6538
+ }
6539
+ return resolved;
6540
+ }
6493
6541
  function toActionToken2(value) {
6494
6542
  const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
6495
6543
  return normalized.length > 0 ? normalized : "tool";
@@ -6514,6 +6562,8 @@ function buildCapabilityRouters(inventories, grouping, summarize) {
6514
6562
  const reservedNormalized = toActionToken2(DESCRIBE_ACTION);
6515
6563
  for (const inventory of inventories) {
6516
6564
  const capability = grouping.byNamespace[inventory.namespace] ?? "general";
6565
+ const instanceKey = inventory.namespace;
6566
+ const instanceTitle = inventory.title ?? inventory.namespace;
6517
6567
  const sortedTools = [...inventory.tools].sort((a, b) => a.name.localeCompare(b.name));
6518
6568
  for (const tool of sortedTools) {
6519
6569
  let baseAction = toActionToken2(tool.name);
@@ -6525,8 +6575,12 @@ function buildCapabilityRouters(inventories, grouping, summarize) {
6525
6575
  action: baseAction,
6526
6576
  baseAction,
6527
6577
  serverKey: inventory.namespace,
6578
+ instanceKey,
6579
+ instanceTitle,
6528
6580
  toolName: tool.name,
6529
6581
  qualifiedName: `${inventory.namespace}:${tool.name}`,
6582
+ canonicalRouteId: buildCanonicalRouteId(capability, instanceKey, tool.name),
6583
+ collisionGroupSize: 1,
6530
6584
  inputSchema: tool.inputSchema ?? { type: "object" },
6531
6585
  summary: resolveSummary(tool.description ?? undefined, capability)
6532
6586
  });
@@ -6543,12 +6597,44 @@ function buildCapabilityRouters(inventories, grouping, summarize) {
6543
6597
  const sortedKeys = [...byCapabilityAction.keys()].sort((a, b) => a.localeCompare(b));
6544
6598
  for (const key of sortedKeys) {
6545
6599
  const records = (byCapabilityAction.get(key) ?? []).sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName));
6600
+ if (records.length === 1) {
6601
+ const [record] = records;
6602
+ if (record) {
6603
+ resolved.push({
6604
+ ...record,
6605
+ action: record.baseAction,
6606
+ collisionGroupSize: 1,
6607
+ legacyActions: []
6608
+ });
6609
+ }
6610
+ continue;
6611
+ }
6612
+ const legacyActionsByQualifiedName = new Map;
6546
6613
  records.forEach((record, index) => {
6547
- resolved.push({
6548
- ...record,
6549
- action: index === 0 ? record.baseAction : `${record.baseAction}__${index + 1}`
6550
- });
6614
+ legacyActionsByQualifiedName.set(record.qualifiedName, index === 0 ? [] : [`${record.baseAction}__${index + 1}`]);
6551
6615
  });
6616
+ const instanceTokens = resolveInstanceTokens(records);
6617
+ const byInstance = new Map;
6618
+ for (const record of records) {
6619
+ const instanceKey = record.instanceKey ?? record.serverKey;
6620
+ const existing = byInstance.get(instanceKey) ?? [];
6621
+ existing.push(record);
6622
+ byInstance.set(instanceKey, existing);
6623
+ }
6624
+ const emittedActions = new Set;
6625
+ for (const instanceKey of [...byInstance.keys()].sort((a, b) => a.localeCompare(b))) {
6626
+ const instanceRecords = (byInstance.get(instanceKey) ?? []).sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName));
6627
+ const instanceToken = instanceTokens.get(instanceKey) ?? toActionToken2(instanceKey);
6628
+ const actionBase = buildInstanceActionBase(records[0]?.baseAction ?? "tool", instanceToken);
6629
+ instanceRecords.forEach((record, index) => {
6630
+ resolved.push({
6631
+ ...record,
6632
+ action: allocateUniqueActionId(emittedActions, actionBase, index),
6633
+ collisionGroupSize: records.length,
6634
+ legacyActions: legacyActionsByQualifiedName.get(record.qualifiedName) ?? []
6635
+ });
6636
+ });
6637
+ }
6552
6638
  }
6553
6639
  const byCapability = new Map;
6554
6640
  for (const route of resolved) {
@@ -7718,10 +7804,15 @@ class Cataloger {
7718
7804
  name: bareToolName,
7719
7805
  arguments: args
7720
7806
  });
7721
- return {
7807
+ const response = {
7722
7808
  content: callResult.content,
7723
7809
  isError: callResult.isError
7724
7810
  };
7811
+ const sc = callResult["structuredContent"];
7812
+ if (sc != null && typeof sc === "object" && !Array.isArray(sc)) {
7813
+ response.structuredContent = sc;
7814
+ }
7815
+ return response;
7725
7816
  }
7726
7817
  async refreshTools(key) {
7727
7818
  const connection = this.connections.get(key);
@@ -8338,6 +8429,178 @@ class MonitorServer {
8338
8429
  }
8339
8430
  }
8340
8431
 
8432
+ // src/server/response-resource.ts
8433
+ import { randomBytes } from "crypto";
8434
+ class ResponseResourceManager {
8435
+ static MAX_PREVIEW_BYTES = 2048;
8436
+ config;
8437
+ resources = new Map;
8438
+ insertionOrder = [];
8439
+ constructor(config) {
8440
+ this.config = config;
8441
+ }
8442
+ isEnabled() {
8443
+ return this.config.enabled;
8444
+ }
8445
+ shouldOffload(content) {
8446
+ if (!this.config.enabled)
8447
+ return false;
8448
+ const { byteCount } = this.canonicalizeContent(content);
8449
+ return byteCount > this.config.thresholdBytes;
8450
+ }
8451
+ offload(content, context) {
8452
+ const { fullText, byteCount } = this.canonicalizeContent(content);
8453
+ const id = this.generateId(context);
8454
+ const uri = `mcp2://response/${context.capability}/${id}`;
8455
+ const entry = {
8456
+ uri,
8457
+ name: `${context.capability}:${context.action} response`,
8458
+ description: `Full response from ${context.capability}:${context.action} (${byteCount} bytes)`,
8459
+ mimeType: "text/plain",
8460
+ fullText,
8461
+ byteCount,
8462
+ createdAt: Date.now()
8463
+ };
8464
+ this.store(uri, entry);
8465
+ const preview = this.buildPreview(fullText);
8466
+ const pointer = {
8467
+ truncated: true,
8468
+ resource_uri: uri,
8469
+ total_bytes: byteCount,
8470
+ preview,
8471
+ instructions: "Full response available via resources/read with the resource_uri above."
8472
+ };
8473
+ return {
8474
+ resourceUri: uri,
8475
+ inlineContent: [{ type: "text", text: JSON.stringify(pointer) }]
8476
+ };
8477
+ }
8478
+ readResource(uri) {
8479
+ const entry = this.resources.get(uri);
8480
+ if (!entry)
8481
+ return null;
8482
+ if (Date.now() - entry.createdAt >= this.config.ttlMs) {
8483
+ this.deleteResource(uri);
8484
+ return null;
8485
+ }
8486
+ this.touchInsertionOrder(uri);
8487
+ return {
8488
+ contents: [
8489
+ {
8490
+ uri: entry.uri,
8491
+ mimeType: entry.mimeType,
8492
+ text: entry.fullText
8493
+ }
8494
+ ]
8495
+ };
8496
+ }
8497
+ listResources() {
8498
+ this.evictExpired();
8499
+ const result = [];
8500
+ for (const entry of this.resources.values()) {
8501
+ result.push({
8502
+ uri: entry.uri,
8503
+ name: entry.name,
8504
+ description: entry.description,
8505
+ mimeType: entry.mimeType,
8506
+ size: entry.byteCount
8507
+ });
8508
+ }
8509
+ return result;
8510
+ }
8511
+ getResourceCount() {
8512
+ return this.resources.size;
8513
+ }
8514
+ canonicalizeContent(content) {
8515
+ const fullText = content.map((block) => block.text).join(`
8516
+
8517
+ ---
8518
+
8519
+ `);
8520
+ return {
8521
+ fullText,
8522
+ byteCount: this.measureBytes(fullText)
8523
+ };
8524
+ }
8525
+ measureBytes(text) {
8526
+ return Buffer.byteLength(text, "utf8");
8527
+ }
8528
+ generateId(context) {
8529
+ const timestamp = Date.now().toString(36);
8530
+ const random = randomBytes(4).toString("hex");
8531
+ const actionSlug = context.action.toLowerCase().replace(/[^a-z0-9]+/g, "_").slice(0, 24);
8532
+ return `${actionSlug}_${timestamp}_${random}`;
8533
+ }
8534
+ buildPreview(fullText) {
8535
+ const lines = fullText.split(`
8536
+ `);
8537
+ let preview;
8538
+ if (lines.length <= this.config.maxInlineLines) {
8539
+ preview = fullText;
8540
+ } else {
8541
+ preview = `${lines.slice(0, this.config.maxInlineLines).join(`
8542
+ `)}
8543
+ ...`;
8544
+ }
8545
+ if (this.measureBytes(preview) > ResponseResourceManager.MAX_PREVIEW_BYTES) {
8546
+ const truncated = this.truncateUtf8(preview, ResponseResourceManager.MAX_PREVIEW_BYTES);
8547
+ return `${truncated}...`;
8548
+ }
8549
+ return preview;
8550
+ }
8551
+ truncateUtf8(text, maxBytes) {
8552
+ const bytes = Buffer.from(text, "utf8");
8553
+ if (bytes.length <= maxBytes)
8554
+ return text;
8555
+ const decoder = new TextDecoder("utf-8", { fatal: true });
8556
+ for (let end = maxBytes;end > 0; end--) {
8557
+ try {
8558
+ return decoder.decode(bytes.subarray(0, end));
8559
+ } catch {}
8560
+ }
8561
+ return "";
8562
+ }
8563
+ store(uri, entry) {
8564
+ this.evictExpired();
8565
+ while (this.resources.size >= this.config.maxResources && this.insertionOrder.length > 0) {
8566
+ const oldest = this.insertionOrder[0];
8567
+ if (!oldest)
8568
+ break;
8569
+ this.deleteResource(oldest);
8570
+ }
8571
+ this.resources.set(uri, entry);
8572
+ this.touchInsertionOrder(uri);
8573
+ }
8574
+ evictExpired() {
8575
+ const now = Date.now();
8576
+ const toRemove = [];
8577
+ for (const [uri, entry] of this.resources) {
8578
+ if (now - entry.createdAt >= this.config.ttlMs) {
8579
+ toRemove.push(uri);
8580
+ }
8581
+ }
8582
+ for (const uri of toRemove) {
8583
+ this.deleteResource(uri);
8584
+ }
8585
+ }
8586
+ deleteResource(uri) {
8587
+ this.resources.delete(uri);
8588
+ this.removeFromInsertionOrder(uri);
8589
+ }
8590
+ removeFromInsertionOrder(uri) {
8591
+ const idx = this.insertionOrder.indexOf(uri);
8592
+ if (idx !== -1)
8593
+ this.insertionOrder.splice(idx, 1);
8594
+ }
8595
+ appendToInsertionOrder(uri) {
8596
+ this.insertionOrder.push(uri);
8597
+ }
8598
+ touchInsertionOrder(uri) {
8599
+ this.removeFromInsertionOrder(uri);
8600
+ this.appendToInsertionOrder(uri);
8601
+ }
8602
+ }
8603
+
8341
8604
  // src/server/stats.ts
8342
8605
  class StatsCollector {
8343
8606
  indexStore;
@@ -8542,6 +8805,7 @@ class McpSquaredServer {
8542
8805
  compiledPolicy;
8543
8806
  indexRefreshManager;
8544
8807
  statsCollector;
8808
+ responseResourceManager;
8545
8809
  monitorServer;
8546
8810
  isCoreStarted = false;
8547
8811
  serverName;
@@ -8593,6 +8857,8 @@ class McpSquaredServer {
8593
8857
  policy: loadedPolicy,
8594
8858
  sink: this.obsSink
8595
8859
  });
8860
+ const rrConfig = this.config.operations.responseResource ?? DEFAULT_RESPONSE_RESOURCE_CONFIG;
8861
+ this.responseResourceManager = new ResponseResourceManager(rrConfig);
8596
8862
  this.mcpServer = this.createMcpServer(name, version);
8597
8863
  this.selectionTracker = new SelectionTracker;
8598
8864
  this.compiledPolicy = compilePolicy(this.config);
@@ -8629,7 +8895,8 @@ class McpSquaredServer {
8629
8895
  description: "Execute capability-first tools routed to connected upstream MCP servers."
8630
8896
  }, {
8631
8897
  capabilities: {
8632
- tools: {}
8898
+ tools: {},
8899
+ ...this.responseResourceManager.isEnabled() ? { resources: {} } : {}
8633
8900
  },
8634
8901
  instructions: this.buildServerInstructions()
8635
8902
  });
@@ -8649,6 +8916,32 @@ class McpSquaredServer {
8649
8916
  }
8650
8917
  registerConfiguredToolSurface(server) {
8651
8918
  this.registerCapabilityRouters(server);
8919
+ if (this.responseResourceManager.isEnabled()) {
8920
+ this.registerResponseResources(server);
8921
+ }
8922
+ }
8923
+ registerResponseResources(server) {
8924
+ const mgr = this.responseResourceManager;
8925
+ server.registerResource("response-resources", "mcp2://response/{capability}/{id}", {
8926
+ description: "Temporary resources containing full tool responses that exceeded the inline size threshold.",
8927
+ mimeType: "text/plain"
8928
+ }, async (uri) => {
8929
+ const result = mgr.readResource(uri.href);
8930
+ if (!result) {
8931
+ return {
8932
+ contents: [
8933
+ {
8934
+ uri: uri.href,
8935
+ mimeType: "text/plain",
8936
+ text: JSON.stringify({
8937
+ error: "Resource not found or expired"
8938
+ })
8939
+ }
8940
+ ]
8941
+ };
8942
+ }
8943
+ return result;
8944
+ });
8652
8945
  }
8653
8946
  runTaskSpan(taskName, run) {
8654
8947
  return task_span(this.obsSink, {
@@ -8677,6 +8970,7 @@ class McpSquaredServer {
8677
8970
  const status = this.cataloger.getStatus();
8678
8971
  const inventories = [...status.entries()].filter(([, info]) => info.status === "connected").map(([namespace]) => ({
8679
8972
  namespace,
8973
+ title: this.config.upstreams[namespace]?.label ?? namespace,
8680
8974
  tools: this.cataloger.getToolsForServer(namespace)
8681
8975
  })).sort((a, b) => a.namespace.localeCompare(b.namespace));
8682
8976
  if (inventories.length === 0) {
@@ -8689,30 +8983,38 @@ class McpSquaredServer {
8689
8983
  const grouping = groupNamespacesByCapability(inventories, overrides);
8690
8984
  return buildCapabilityRouters(inventories, grouping, (desc, cap) => this.actionSummary(desc, cap));
8691
8985
  }
8986
+ getCapabilityRouter(capability) {
8987
+ return this.buildCapabilityRouters().find((router) => router.capability === capability) ?? {
8988
+ capability,
8989
+ actions: []
8990
+ };
8991
+ }
8692
8992
  registerCapabilityRouters(server) {
8693
8993
  const routers = this.buildCapabilityRouters();
8694
8994
  for (const router of routers) {
8695
8995
  if (router.actions.length === 0) {
8696
8996
  continue;
8697
8997
  }
8698
- server.registerTool(router.capability, {
8699
- title: this.capabilityTitle(router.capability),
8700
- description: this.capabilitySummary(router.capability),
8998
+ const capability = router.capability;
8999
+ server.registerTool(capability, {
9000
+ title: this.capabilityTitle(capability),
9001
+ description: this.capabilitySummary(capability),
8701
9002
  annotations: {
8702
9003
  readOnlyHint: false,
8703
9004
  destructiveHint: false,
8704
9005
  openWorldHint: true
8705
9006
  },
8706
9007
  inputSchema: {
8707
- action: z3.string().describe(`Action ID for ${router.capability}. Use "${DESCRIBE_ACTION2}" to inspect available actions and schemas.`),
9008
+ action: z3.string().describe(`Action ID for ${capability}. Use "${DESCRIBE_ACTION2}" to inspect available actions and schemas.`),
8708
9009
  arguments: z3.record(z3.string(), z3.unknown()).default({}).describe("Arguments for the selected capability action"),
8709
9010
  confirmation_token: z3.string().optional().describe("Optional confirmation token for actions that require explicit confirmation")
8710
9011
  }
8711
- }, async (rawArgs) => this.runTaskSpan(router.capability, async () => {
9012
+ }, async (rawArgs) => this.runTaskSpan(capability, async () => {
8712
9013
  const requestId = this.statsCollector.startRequest();
8713
9014
  const startTime = Date.now();
8714
9015
  let success = false;
8715
9016
  try {
9017
+ const liveRouter = this.getCapabilityRouter(capability);
8716
9018
  const parsedArgs = isRecord3(rawArgs) ? { ...rawArgs } : {};
8717
9019
  const action = typeof parsedArgs["action"] === "string" ? parsedArgs["action"] : "";
8718
9020
  const confirmationToken = typeof parsedArgs["confirmation_token"] === "string" ? parsedArgs["confirmation_token"] : undefined;
@@ -8724,25 +9026,37 @@ class McpSquaredServer {
8724
9026
  type: "text",
8725
9027
  text: JSON.stringify({
8726
9028
  error: "Missing required action",
8727
- capability: router.capability
9029
+ capability
8728
9030
  })
8729
9031
  }
8730
9032
  ],
8731
9033
  isError: true
8732
9034
  };
8733
9035
  }
8734
- const visibleActions = router.actions.map((route2) => {
8735
- const visibility = getToolVisibilityCompiled(router.capability, route2.action, this.compiledPolicy);
9036
+ const visibleRoutes = liveRouter.actions.map((route) => {
9037
+ const visibility = getToolVisibilityCompiled(capability, route.action, this.compiledPolicy);
8736
9038
  if (!visibility.visible) {
8737
9039
  return null;
8738
9040
  }
8739
9041
  return {
8740
- action: route2.action,
8741
- summary: route2.summary,
8742
- inputSchema: route2.inputSchema,
9042
+ route,
8743
9043
  requiresConfirmation: visibility.requiresConfirmation
8744
9044
  };
8745
- }).filter((entry) => entry !== null).sort((a, b) => a.action.localeCompare(b.action));
9045
+ }).filter((entry) => entry !== null);
9046
+ const visibleActions = visibleRoutes.map(({ route, requiresConfirmation }) => {
9047
+ const actionInfo = {
9048
+ action: route.action,
9049
+ summary: route.summary,
9050
+ inputSchema: route.inputSchema,
9051
+ requiresConfirmation
9052
+ };
9053
+ if ((route.collisionGroupSize ?? 1) > 1) {
9054
+ actionInfo.baseAction = route.baseAction;
9055
+ actionInfo.instance = route.instanceKey ?? route.serverKey;
9056
+ actionInfo.instanceTitle = route.instanceTitle ?? route.instanceKey ?? route.serverKey;
9057
+ }
9058
+ return actionInfo;
9059
+ }).sort((a, b) => a.action.localeCompare(b.action));
8746
9060
  if (action === DESCRIBE_ACTION2) {
8747
9061
  success = true;
8748
9062
  return {
@@ -8750,7 +9064,7 @@ class McpSquaredServer {
8750
9064
  {
8751
9065
  type: "text",
8752
9066
  text: JSON.stringify({
8753
- capability: router.capability,
9067
+ capability,
8754
9068
  actions: visibleActions,
8755
9069
  totalActions: visibleActions.length
8756
9070
  })
@@ -8758,7 +9072,8 @@ class McpSquaredServer {
8758
9072
  ]
8759
9073
  };
8760
9074
  }
8761
- const ambiguousCandidates = router.actions.filter((route2) => route2.baseAction === action).map((route2) => route2.action).sort((a, b) => a.localeCompare(b));
9075
+ const exactRoute = liveRouter.actions.find((entry) => entry.action === action || (entry.legacyActions ?? []).includes(action));
9076
+ const ambiguousCandidates = visibleRoutes.filter(({ route }) => route.baseAction === action).map(({ route }) => route.action).sort((a, b) => a.localeCompare(b));
8762
9077
  if (ambiguousCandidates.length > 1) {
8763
9078
  return {
8764
9079
  content: [
@@ -8766,7 +9081,7 @@ class McpSquaredServer {
8766
9081
  type: "text",
8767
9082
  text: JSON.stringify({
8768
9083
  requires_disambiguation: true,
8769
- capability: router.capability,
9084
+ capability,
8770
9085
  action,
8771
9086
  candidates: ambiguousCandidates
8772
9087
  })
@@ -8775,15 +9090,15 @@ class McpSquaredServer {
8775
9090
  isError: true
8776
9091
  };
8777
9092
  }
8778
- const route = router.actions.find((entry) => entry.action === action);
8779
- if (!route) {
9093
+ const selectedRoute = exactRoute ?? (ambiguousCandidates.length === 1 ? visibleRoutes.find(({ route }) => route.baseAction === action)?.route : undefined);
9094
+ if (selectedRoute == null) {
8780
9095
  return {
8781
9096
  content: [
8782
9097
  {
8783
9098
  type: "text",
8784
9099
  text: JSON.stringify({
8785
9100
  error: "Unknown action",
8786
- capability: router.capability,
9101
+ capability,
8787
9102
  action,
8788
9103
  availableActions: visibleActions.map((a) => a.action)
8789
9104
  })
@@ -8793,10 +9108,12 @@ class McpSquaredServer {
8793
9108
  };
8794
9109
  }
8795
9110
  const callResult = await this.executeRoutedTool({
8796
- capability: router.capability,
8797
- action: route.action,
8798
- qualifiedToolName: route.qualifiedName,
8799
- toolNameForCall: route.qualifiedName,
9111
+ capability,
9112
+ action: selectedRoute.action,
9113
+ policyAction: exactRoute != null ? action : selectedRoute.action,
9114
+ routeId: selectedRoute.canonicalRouteId ?? `${capability}:${selectedRoute.action}`,
9115
+ qualifiedToolName: selectedRoute.qualifiedName,
9116
+ toolNameForCall: selectedRoute.qualifiedName,
8800
9117
  args: actionArgs,
8801
9118
  ...confirmationToken != null ? { confirmationToken } : {}
8802
9119
  });
@@ -8816,7 +9133,7 @@ class McpSquaredServer {
8816
9133
  };
8817
9134
  } finally {
8818
9135
  const responseTime = Date.now() - startTime;
8819
- this.statsCollector.endRequest(requestId, success, responseTime, router.capability, "capability");
9136
+ this.statsCollector.endRequest(requestId, success, responseTime, capability, "capability");
8820
9137
  }
8821
9138
  }));
8822
9139
  }
@@ -8835,7 +9152,7 @@ class McpSquaredServer {
8835
9152
  async executeRoutedTool(args) {
8836
9153
  const policyResult = evaluatePolicy({
8837
9154
  capability: args.capability,
8838
- action: args.action,
9155
+ action: args.policyAction ?? args.action,
8839
9156
  confirmationToken: args.confirmationToken
8840
9157
  }, this.config);
8841
9158
  if (policyResult.decision === "block") {
@@ -8869,27 +9186,40 @@ class McpSquaredServer {
8869
9186
  }
8870
9187
  this.guard.enforce({
8871
9188
  agent: this.safetyAgent,
8872
- tool: `${args.capability}:${args.action}`,
9189
+ tool: args.routeId ?? `${args.capability}:${args.action}`,
8873
9190
  action: "call",
8874
9191
  params: args.args
8875
9192
  });
8876
9193
  const result = await tool_span(this.obsSink, {
8877
9194
  agent: this.safetyAgent,
8878
- tool: `${args.capability}:${args.action}`,
9195
+ tool: args.routeId ?? `${args.capability}:${args.action}`,
8879
9196
  action: "call",
8880
9197
  playbook: this.guard.playbook,
8881
9198
  env: this.guard.agentEnv
8882
9199
  }, () => this.cataloger.callTool(args.toolNameForCall, args.args));
8883
9200
  if (!result.isError && this.config.operations.selectionCache.enabled) {
8884
- const toolKey = `${args.capability}:${args.action}`;
9201
+ const toolKey = args.routeId ?? `${args.capability}:${args.action}`;
8885
9202
  this.selectionTracker.trackToolUsage(toolKey);
8886
9203
  if (this.selectionTracker.getSessionToolCount() >= 2) {
8887
9204
  this.selectionTracker.flushToStore(this.retriever.getIndexStore());
8888
9205
  }
8889
9206
  }
9207
+ const normalizedContent = this.normalizeToolResultContent(result.content);
9208
+ const structuredContent = result.structuredContent;
9209
+ if (!result.isError && this.responseResourceManager.isEnabled() && this.responseResourceManager.shouldOffload(normalizedContent)) {
9210
+ try {
9211
+ const offloaded = this.responseResourceManager.offload(normalizedContent, { capability: args.capability, action: args.action });
9212
+ return {
9213
+ content: offloaded.inlineContent,
9214
+ isError: false,
9215
+ ...structuredContent != null ? { structuredContent } : {}
9216
+ };
9217
+ } catch {}
9218
+ }
8890
9219
  return {
8891
- content: this.normalizeToolResultContent(result.content),
8892
- isError: result.isError ?? false
9220
+ content: normalizedContent,
9221
+ isError: result.isError ?? false,
9222
+ ...structuredContent != null ? { structuredContent } : {}
8893
9223
  };
8894
9224
  }
8895
9225
  syncIndex() {
@@ -9117,6 +9447,7 @@ async function collectStatus(config) {
9117
9447
  }
9118
9448
  const inventories = [...status.entries()].filter(([, info]) => info.status === "connected").map(([namespace]) => ({
9119
9449
  namespace,
9450
+ title: config.upstreams[namespace]?.label ?? namespace,
9120
9451
  tools: cataloger.getToolsForServer(namespace)
9121
9452
  })).sort((a, b) => a.namespace.localeCompare(b.namespace));
9122
9453
  let routers = [];
@@ -736,6 +736,14 @@ var LoggingSchema = z.object({
736
736
  var EmbeddingsSchema = z.object({
737
737
  enabled: z.boolean().default(false)
738
738
  });
739
+ var ResponseResourceSchema = z.object({
740
+ enabled: z.boolean().default(false),
741
+ thresholdBytes: z.number().int().min(1024).default(51200),
742
+ maxInlineLines: z.number().int().min(1).default(20),
743
+ maxResources: z.number().int().min(1).default(100),
744
+ ttlMs: z.number().int().min(0).default(600000)
745
+ });
746
+ var DEFAULT_RESPONSE_RESOURCE_CONFIG = ResponseResourceSchema.parse({});
739
747
  var SelectionCacheSchema = z.object({
740
748
  enabled: z.boolean().default(true),
741
749
  minCooccurrenceThreshold: z.number().int().min(1).default(2),
@@ -752,6 +760,7 @@ var OperationsSchema = z.object({
752
760
  index: IndexSchema.default({ refreshIntervalMs: 30000 }),
753
761
  logging: LoggingSchema.default({ level: "info" }),
754
762
  embeddings: EmbeddingsSchema.default({ enabled: false }),
763
+ responseResource: ResponseResourceSchema.default(DEFAULT_RESPONSE_RESOURCE_CONFIG),
755
764
  selectionCache: SelectionCacheSchema.default({
756
765
  enabled: true,
757
766
  minCooccurrenceThreshold: 2,
@@ -774,6 +783,7 @@ var OperationsSchema = z.object({
774
783
  index: { refreshIntervalMs: 30000 },
775
784
  logging: { level: "info" },
776
785
  embeddings: { enabled: false },
786
+ responseResource: DEFAULT_RESPONSE_RESOURCE_CONFIG,
777
787
  selectionCache: {
778
788
  enabled: true,
779
789
  minCooccurrenceThreshold: 2,
@@ -2275,10 +2285,15 @@ class Cataloger {
2275
2285
  name: bareToolName,
2276
2286
  arguments: args
2277
2287
  });
2278
- return {
2288
+ const response = {
2279
2289
  content: callResult.content,
2280
2290
  isError: callResult.isError
2281
2291
  };
2292
+ const sc = callResult["structuredContent"];
2293
+ if (sc != null && typeof sc === "object" && !Array.isArray(sc)) {
2294
+ response.structuredContent = sc;
2295
+ }
2296
+ return response;
2282
2297
  }
2283
2298
  async refreshTools(key) {
2284
2299
  const connection = this.connections.get(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-squared",
3
- "version": "0.6.0",
3
+ "version": "0.7.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,7 +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
+ "coverage:check": "bun run scripts/check-line-coverage.ts coverage/coverage-summary.txt coverage/lcov.info 80",
32
32
  "typecheck": "tsc --noEmit",
33
33
  "lint": "biome check src tests scripts AGENTS.md CLAUDE.md WARP.md README.md CHANGELOG.md package.json biome.json tsconfig.json",
34
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",
@@ -78,6 +78,7 @@
78
78
  "@hono/node-server": "^1.19.11",
79
79
  "ajv": "^8.18.0",
80
80
  "diff": "8.0.3",
81
+ "express-rate-limit": "8.2.2",
81
82
  "hono": "^4.12.5",
82
83
  "qs": "6.15.0",
83
84
  "tar": "^7.5.10"