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 +8 -0
- package/dist/index.js +366 -35
- package/dist/tui/config.js +16 -1
- package/package.json +3 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
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 ${
|
|
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(
|
|
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
|
|
9029
|
+
capability
|
|
8728
9030
|
})
|
|
8729
9031
|
}
|
|
8730
9032
|
],
|
|
8731
9033
|
isError: true
|
|
8732
9034
|
};
|
|
8733
9035
|
}
|
|
8734
|
-
const
|
|
8735
|
-
const visibility = getToolVisibilityCompiled(
|
|
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
|
-
|
|
8741
|
-
summary: route2.summary,
|
|
8742
|
-
inputSchema: route2.inputSchema,
|
|
9042
|
+
route,
|
|
8743
9043
|
requiresConfirmation: visibility.requiresConfirmation
|
|
8744
9044
|
};
|
|
8745
|
-
}).filter((entry) => entry !== null)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
8779
|
-
if (
|
|
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
|
|
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
|
|
8797
|
-
action:
|
|
8798
|
-
|
|
8799
|
-
|
|
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,
|
|
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:
|
|
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 = [];
|
package/dist/tui/config.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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"
|