agent-trellis 0.6.3 → 0.8.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 +97 -38
- package/dist/adapters/claude-code.js +3 -2
- package/dist/adapters/codex.js +3 -2
- package/dist/adapters/jsonMcp.d.ts +3 -1
- package/dist/adapters/jsonMcp.js +18 -4
- package/dist/adapters/kimi-code.d.ts +21 -0
- package/dist/adapters/kimi-code.js +107 -0
- package/dist/adapters/kiro.js +3 -2
- package/dist/adapters/mcpPlan.d.ts +26 -2
- package/dist/adapters/mcpPlan.js +135 -15
- package/dist/adapters/pi.js +22 -3
- package/dist/adapters/symlinkPlan.d.ts +7 -0
- package/dist/adapters/symlinkPlan.js +16 -7
- package/dist/cli.js +129 -20
- package/dist/commands/doctor.d.ts +7 -2
- package/dist/commands/doctor.js +38 -2
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.js +26 -13
- package/dist/commands/kimi.d.ts +16 -0
- package/dist/commands/kimi.js +64 -0
- package/dist/commands/manage.d.ts +39 -0
- package/dist/commands/manage.js +126 -0
- package/dist/commands/mcp.d.ts +22 -1
- package/dist/commands/mcp.js +65 -2
- package/dist/commands/mcpGateway.d.ts +6 -0
- package/dist/commands/mcpGateway.js +34 -14
- package/dist/commands/mcpImport.d.ts +27 -0
- package/dist/commands/mcpImport.js +336 -0
- package/dist/commands/memory.d.ts +5 -3
- package/dist/commands/memory.js +12 -8
- package/dist/commands/migrate.d.ts +37 -7
- package/dist/commands/migrate.js +253 -27
- package/dist/commands/onboard.d.ts +74 -2
- package/dist/commands/onboard.js +463 -72
- package/dist/commands/onboardVerdict.js +20 -7
- package/dist/commands/rollback.js +4 -1
- package/dist/commands/secretsAudit.d.ts +21 -12
- package/dist/commands/secretsAudit.js +39 -10
- package/dist/commands/skill.d.ts +17 -0
- package/dist/commands/skill.js +47 -2
- package/dist/commands/sync.js +2 -0
- package/dist/core/canonical.d.ts +58 -4
- package/dist/core/canonical.js +183 -11
- package/dist/core/capabilitySelection.d.ts +45 -0
- package/dist/core/capabilitySelection.js +124 -0
- package/dist/core/types.d.ts +25 -2
- package/dist/core/types.js +7 -0
- package/dist/lib/backup.d.ts +2 -0
- package/dist/lib/backup.js +4 -0
- package/dist/lib/builtinSkills.d.ts +2 -0
- package/dist/lib/builtinSkills.js +4 -0
- package/dist/lib/cliMetadata.d.ts +1 -0
- package/dist/lib/cliMetadata.js +14 -0
- package/dist/lib/gatewayBackend.d.ts +28 -0
- package/dist/lib/gatewayBackend.js +72 -1
- package/dist/lib/installAgent.d.ts +7 -5
- package/dist/lib/installAgent.js +4 -3
- package/dist/lib/mcpConnect.js +5 -1
- package/dist/lib/mcpMigrateRead.d.ts +1 -0
- package/dist/lib/mcpMigrateRead.js +3 -0
- package/dist/lib/mcpRuntime.d.ts +46 -0
- package/dist/lib/mcpRuntime.js +185 -0
- package/dist/lib/mcpStatusProvider.d.ts +10 -0
- package/dist/lib/mcpStatusProvider.js +25 -0
- package/dist/lib/mcpToolRegistry.d.ts +18 -0
- package/dist/lib/mcpToolRegistry.js +107 -14
- package/dist/lib/memoryGraph.d.ts +1 -1
- package/dist/lib/memoryGraph.js +9 -3
- package/dist/lib/memoryProvider.d.ts +58 -0
- package/dist/lib/memoryProvider.js +175 -0
- package/dist/lib/memoryStatus.d.ts +17 -0
- package/dist/lib/memoryStatus.js +88 -0
- package/dist/lib/nativeMemory.d.ts +19 -0
- package/dist/lib/nativeMemory.js +129 -0
- package/dist/lib/runtimeControlProvider.d.ts +16 -0
- package/dist/lib/runtimeControlProvider.js +168 -0
- package/dist/lib/secretEnv.d.ts +17 -0
- package/dist/lib/secretEnv.js +31 -1
- package/dist/lib/skillProvider.d.ts +13 -0
- package/dist/lib/skillProvider.js +210 -0
- package/dist/lib/taskProvider.d.ts +7 -0
- package/dist/lib/taskProvider.js +43 -0
- package/dist/lib/taskStore.d.ts +46 -0
- package/dist/lib/taskStore.js +156 -0
- package/dist/lib/terminalPicker.d.ts +23 -28
- package/dist/lib/terminalPicker.js +54 -164
- package/dist/pi-bridge/bundle.js +493 -51
- package/dist/pi-bridge/index.js +42 -21
- package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
- package/dist/pi-bridge/schemaTranslate.js +4 -8
- package/dist/probes/kimi-code.d.ts +12 -0
- package/dist/probes/kimi-code.js +55 -0
- package/dist/sdk.d.ts +1 -1
- package/docs/architecture.md +224 -26
- package/docs/assets/agent-trellis-architecture-horizontal.svg +80 -0
- package/docs/assets/agent-trellis-architecture-vertical.svg +82 -0
- package/docs/assets/trellis-mark.svg +12 -0
- package/docs/getting-started.md +403 -35
- package/docs/overview.md +131 -0
- package/docs/overview.zh-CN.md +100 -0
- package/docs/research.md +59 -0
- package/docs/roadmap.md +10 -5
- package/package.json +5 -3
- package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
- package/schema/capability-selection.example.yaml +23 -0
- package/schema/scope.example.yaml +1 -1
- package/schema/servers.example.yaml +15 -1
package/dist/pi-bridge/bundle.js
CHANGED
|
@@ -55,7 +55,7 @@ var require_identity = __commonJS({
|
|
|
55
55
|
var isMap2 = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === MAP;
|
|
56
56
|
var isPair = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === PAIR;
|
|
57
57
|
var isScalar = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === SCALAR;
|
|
58
|
-
var
|
|
58
|
+
var isSeq2 = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === SEQ;
|
|
59
59
|
function isCollection(node2) {
|
|
60
60
|
if (node2 && typeof node2 === "object")
|
|
61
61
|
switch (node2[NODE_TYPE]) {
|
|
@@ -92,7 +92,7 @@ var require_identity = __commonJS({
|
|
|
92
92
|
exports.isNode = isNode;
|
|
93
93
|
exports.isPair = isPair;
|
|
94
94
|
exports.isScalar = isScalar;
|
|
95
|
-
exports.isSeq =
|
|
95
|
+
exports.isSeq = isSeq2;
|
|
96
96
|
}
|
|
97
97
|
});
|
|
98
98
|
|
|
@@ -15140,21 +15140,25 @@ import { homedir as homedir2 } from "node:os";
|
|
|
15140
15140
|
|
|
15141
15141
|
// src/core/canonical.ts
|
|
15142
15142
|
var import_yaml = __toESM(require_dist(), 1);
|
|
15143
|
-
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
15143
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
15144
15144
|
import { homedir } from "node:os";
|
|
15145
|
-
import { basename, join } from "node:path";
|
|
15145
|
+
import { basename, dirname, join } from "node:path";
|
|
15146
15146
|
|
|
15147
15147
|
// src/core/types.ts
|
|
15148
15148
|
var ALL_AGENTS = [
|
|
15149
15149
|
"claude-code",
|
|
15150
15150
|
"codex",
|
|
15151
15151
|
"kiro",
|
|
15152
|
-
"pi"
|
|
15152
|
+
"pi",
|
|
15153
|
+
"kimi-code"
|
|
15153
15154
|
];
|
|
15154
15155
|
function resolveScope(scope, managedAgents) {
|
|
15155
15156
|
const candidates = scope ?? managedAgents;
|
|
15156
15157
|
return candidates.filter((id) => managedAgents.includes(id));
|
|
15157
15158
|
}
|
|
15159
|
+
function capabilityDeliveryForAgent(agentId, mcp) {
|
|
15160
|
+
return mcp.runtime?.delivery?.[agentId] ?? "native";
|
|
15161
|
+
}
|
|
15158
15162
|
|
|
15159
15163
|
// src/core/canonical.ts
|
|
15160
15164
|
function fromServerDefYaml(def) {
|
|
@@ -15212,9 +15216,34 @@ function loadServersYaml(path) {
|
|
|
15212
15216
|
servers,
|
|
15213
15217
|
knownHostInjected: parsed.known_host_injected ?? [],
|
|
15214
15218
|
hub: parsed.hub,
|
|
15215
|
-
gateway: fromGatewayYaml(parsed.gateway)
|
|
15219
|
+
gateway: fromGatewayYaml(parsed.gateway),
|
|
15220
|
+
routes: fromRoutesYaml(parsed.routes),
|
|
15221
|
+
runtime: fromRuntimeYaml(parsed.runtime)
|
|
15216
15222
|
};
|
|
15217
15223
|
}
|
|
15224
|
+
function fromRuntimeYaml(runtime) {
|
|
15225
|
+
if (!runtime?.delivery) return void 0;
|
|
15226
|
+
const delivery = {};
|
|
15227
|
+
for (const [rawAgent, rawDelivery] of Object.entries(runtime.delivery)) {
|
|
15228
|
+
if (!ALL_AGENTS.includes(rawAgent)) continue;
|
|
15229
|
+
if (rawDelivery !== "native" && rawDelivery !== "mcp" && rawDelivery !== "both") continue;
|
|
15230
|
+
delivery[rawAgent] = rawDelivery;
|
|
15231
|
+
}
|
|
15232
|
+
return Object.keys(delivery).length > 0 ? { delivery } : void 0;
|
|
15233
|
+
}
|
|
15234
|
+
function fromRoutesYaml(routes) {
|
|
15235
|
+
if (!routes) return void 0;
|
|
15236
|
+
const out = {};
|
|
15237
|
+
for (const [rawAgent, rawRoute] of Object.entries(routes)) {
|
|
15238
|
+
if (!ALL_AGENTS.includes(rawAgent)) continue;
|
|
15239
|
+
if (!rawRoute?.mode || !["direct", "gateway", "hub"].includes(rawRoute.mode)) continue;
|
|
15240
|
+
out[rawAgent] = {
|
|
15241
|
+
mode: rawRoute.mode,
|
|
15242
|
+
...rawRoute.servers ? { servers: rawRoute.servers } : {}
|
|
15243
|
+
};
|
|
15244
|
+
}
|
|
15245
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
15246
|
+
}
|
|
15218
15247
|
function fromGatewayYaml(gateway) {
|
|
15219
15248
|
if (!gateway) return void 0;
|
|
15220
15249
|
const out = { enabled: gateway.enabled === true };
|
|
@@ -15329,7 +15358,7 @@ function codexBearerTokenEnvVar(def) {
|
|
|
15329
15358
|
}
|
|
15330
15359
|
|
|
15331
15360
|
// src/lib/secretEnv.ts
|
|
15332
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
15361
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
15333
15362
|
var LINE_RE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
|
|
15334
15363
|
function parseDotenv(content) {
|
|
15335
15364
|
const result = {};
|
|
@@ -15357,14 +15386,28 @@ function resolveSecretEnv(names, policy) {
|
|
|
15357
15386
|
|
|
15358
15387
|
// src/adapters/mcpPlan.ts
|
|
15359
15388
|
var HUB_ENTRY_NAME = "trellis-hub";
|
|
15360
|
-
var GATEWAY_ENTRY_NAME = "trellis
|
|
15389
|
+
var GATEWAY_ENTRY_NAME = "trellis";
|
|
15390
|
+
var RUNTIME_ENTRY_NAME = "trellis";
|
|
15361
15391
|
var GATEWAY_COMMAND = "trellis";
|
|
15392
|
+
var RUNTIME_COMMAND = "trellis";
|
|
15393
|
+
function isOAuthMcpServer(def) {
|
|
15394
|
+
return def.auth === "oauth";
|
|
15395
|
+
}
|
|
15362
15396
|
function isGatewayAgent(agentId, mcp, managedAgents) {
|
|
15397
|
+
const route = mcp.routes?.[agentId];
|
|
15398
|
+
if (route) return route.mode === "gateway";
|
|
15363
15399
|
const gateway = mcp.gateway;
|
|
15364
15400
|
if (!gateway?.enabled) return false;
|
|
15365
15401
|
if (gateway.agents) return gateway.agents.includes(agentId);
|
|
15366
15402
|
return managedAgents.includes(agentId);
|
|
15367
15403
|
}
|
|
15404
|
+
function routeForAgent(agentId, mcp, managedAgents) {
|
|
15405
|
+
const explicit = mcp.routes?.[agentId];
|
|
15406
|
+
if (explicit) return explicit;
|
|
15407
|
+
if (isGatewayAgent(agentId, mcp, managedAgents)) return { mode: "gateway" };
|
|
15408
|
+
if (mcp.hub) return { mode: "hub" };
|
|
15409
|
+
return { mode: "direct" };
|
|
15410
|
+
}
|
|
15368
15411
|
var CODEX_STDIO_URL_CRASH_NOTE = ' On Codex specifically, this crashes the entire process at startup ("url is not supported for stdio"), not just this one server \u2014 see docs/research.md.';
|
|
15369
15412
|
function collisionMessage(name, agentId) {
|
|
15370
15413
|
return `refusing to write MCP server "${name}": also appears in known_host_injected.${agentId === "codex" ? CODEX_STDIO_URL_CRASH_NOTE : ""}`;
|
|
@@ -15379,13 +15422,17 @@ var DANGEROUS_LITERAL_PATTERNS = [
|
|
|
15379
15422
|
{ label: "mcp-router token (mcpr_)", pattern: /mcpr_/ }
|
|
15380
15423
|
];
|
|
15381
15424
|
function findLiteralSecret(def) {
|
|
15382
|
-
const candidates = [
|
|
15383
|
-
|
|
15384
|
-
|
|
15385
|
-
|
|
15425
|
+
const candidates = [
|
|
15426
|
+
...def.command !== void 0 ? [{ value: def.command, field: "command" }] : [],
|
|
15427
|
+
...def.url !== void 0 ? [{ value: def.url, field: "url" }] : [],
|
|
15428
|
+
...(def.args ?? []).map((value) => ({ value, field: "args" })),
|
|
15429
|
+
...Object.entries(def.headers ?? {}).map(([key, value]) => ({ value, field: "headers", key })),
|
|
15430
|
+
...Object.entries(def.staticEnv ?? {}).map(([key, value]) => ({ value, field: "staticEnv", key }))
|
|
15431
|
+
];
|
|
15432
|
+
for (const { value, field, key } of candidates) {
|
|
15386
15433
|
for (const { label, pattern } of DANGEROUS_LITERAL_PATTERNS) {
|
|
15387
|
-
if (pattern.test(
|
|
15388
|
-
return label;
|
|
15434
|
+
if (pattern.test(value)) {
|
|
15435
|
+
return { label, field, key };
|
|
15389
15436
|
}
|
|
15390
15437
|
}
|
|
15391
15438
|
}
|
|
@@ -15398,45 +15445,111 @@ function findUnresolvedEnvName(def, policy) {
|
|
|
15398
15445
|
return names.find((name) => !resolved[name]);
|
|
15399
15446
|
}
|
|
15400
15447
|
function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
15401
|
-
|
|
15402
|
-
|
|
15448
|
+
const route = routeForAgent(agentId, mcp, managedAgents);
|
|
15449
|
+
const runtimeEnabled = capabilityDeliveryForAgent(agentId, mcp) !== "native";
|
|
15450
|
+
if (route.mode === "gateway") {
|
|
15451
|
+
const entryName = runtimeEnabled ? RUNTIME_ENTRY_NAME : GATEWAY_ENTRY_NAME;
|
|
15452
|
+
if (mcp.knownHostInjected.includes(entryName)) {
|
|
15403
15453
|
return {
|
|
15404
15454
|
desired: [],
|
|
15405
|
-
conflicts: [{ name:
|
|
15455
|
+
conflicts: [{ name: entryName, message: collisionMessage(entryName, agentId), remediation: collisionRemediation(entryName) }]
|
|
15406
15456
|
};
|
|
15407
15457
|
}
|
|
15458
|
+
const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route, isOAuthMcpServer);
|
|
15408
15459
|
return {
|
|
15409
|
-
desired: [{
|
|
15410
|
-
|
|
15460
|
+
desired: [{
|
|
15461
|
+
name: entryName,
|
|
15462
|
+
def: {
|
|
15463
|
+
transport: "stdio",
|
|
15464
|
+
command: runtimeEnabled ? RUNTIME_COMMAND : GATEWAY_COMMAND,
|
|
15465
|
+
args: [runtimeEnabled ? "mcp-runtime" : "mcp-gateway", "--agent", agentId]
|
|
15466
|
+
}
|
|
15467
|
+
}, ...oauth.desired],
|
|
15468
|
+
conflicts: oauth.conflicts
|
|
15411
15469
|
};
|
|
15412
15470
|
}
|
|
15413
|
-
if (
|
|
15471
|
+
if (route.mode === "hub") {
|
|
15472
|
+
if (!mcp.hub) {
|
|
15473
|
+
return {
|
|
15474
|
+
desired: [],
|
|
15475
|
+
conflicts: [{
|
|
15476
|
+
name: HUB_ENTRY_NAME,
|
|
15477
|
+
message: `cannot resolve hub route "${agentId}": canonical mcp.hub.url is not configured`,
|
|
15478
|
+
remediation: `configure mcp.hub.url or choose direct/gateway mode for ${agentId}`
|
|
15479
|
+
}]
|
|
15480
|
+
};
|
|
15481
|
+
}
|
|
15414
15482
|
if (mcp.knownHostInjected.includes(HUB_ENTRY_NAME)) {
|
|
15415
15483
|
return {
|
|
15416
15484
|
desired: [],
|
|
15417
15485
|
conflicts: [{ name: HUB_ENTRY_NAME, message: collisionMessage(HUB_ENTRY_NAME, agentId), remediation: collisionRemediation(HUB_ENTRY_NAME) }]
|
|
15418
15486
|
};
|
|
15419
15487
|
}
|
|
15420
|
-
|
|
15488
|
+
if (route.servers) {
|
|
15489
|
+
return {
|
|
15490
|
+
desired: [],
|
|
15491
|
+
conflicts: [{
|
|
15492
|
+
name: HUB_ENTRY_NAME,
|
|
15493
|
+
message: `cannot restrict external hub route "${agentId}" to individual MCP servers: the hub owns its upstream set`,
|
|
15494
|
+
remediation: `remove the servers list from the ${agentId} hub route, or use direct/gateway mode for per-server selection`
|
|
15495
|
+
}]
|
|
15496
|
+
};
|
|
15497
|
+
}
|
|
15498
|
+
const desired = [{ name: HUB_ENTRY_NAME, def: { transport: "http", url: mcp.hub.url } }];
|
|
15499
|
+
if (runtimeEnabled) {
|
|
15500
|
+
if (mcp.knownHostInjected.includes(RUNTIME_ENTRY_NAME)) {
|
|
15501
|
+
return {
|
|
15502
|
+
desired: [],
|
|
15503
|
+
conflicts: [{ name: RUNTIME_ENTRY_NAME, message: collisionMessage(RUNTIME_ENTRY_NAME, agentId), remediation: collisionRemediation(RUNTIME_ENTRY_NAME) }]
|
|
15504
|
+
};
|
|
15505
|
+
}
|
|
15506
|
+
desired.push({
|
|
15507
|
+
name: RUNTIME_ENTRY_NAME,
|
|
15508
|
+
def: { transport: "stdio", command: RUNTIME_COMMAND, args: ["mcp-runtime", "--agent", agentId] }
|
|
15509
|
+
});
|
|
15510
|
+
}
|
|
15511
|
+
return { desired, conflicts: [] };
|
|
15421
15512
|
}
|
|
15513
|
+
if (runtimeEnabled) {
|
|
15514
|
+
if (mcp.knownHostInjected.includes(RUNTIME_ENTRY_NAME)) {
|
|
15515
|
+
return {
|
|
15516
|
+
desired: [],
|
|
15517
|
+
conflicts: [{ name: RUNTIME_ENTRY_NAME, message: collisionMessage(RUNTIME_ENTRY_NAME, agentId), remediation: collisionRemediation(RUNTIME_ENTRY_NAME) }]
|
|
15518
|
+
};
|
|
15519
|
+
}
|
|
15520
|
+
return {
|
|
15521
|
+
desired: [{
|
|
15522
|
+
name: RUNTIME_ENTRY_NAME,
|
|
15523
|
+
def: { transport: "stdio", command: RUNTIME_COMMAND, args: ["mcp-runtime", "--agent", agentId] }
|
|
15524
|
+
}],
|
|
15525
|
+
conflicts: []
|
|
15526
|
+
};
|
|
15527
|
+
}
|
|
15528
|
+
return planDirectServers(agentId, mcp, managedAgents, policy, route);
|
|
15529
|
+
}
|
|
15530
|
+
function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
|
|
15422
15531
|
const desired = [];
|
|
15423
15532
|
const conflicts = [];
|
|
15424
15533
|
for (const [name, def] of Object.entries(mcp.servers)) {
|
|
15425
15534
|
if (def.enabled === false) {
|
|
15426
15535
|
continue;
|
|
15427
15536
|
}
|
|
15537
|
+
if (filter && !filter(def)) continue;
|
|
15428
15538
|
if (!isInScope(agentId, def.agents, managedAgents)) {
|
|
15429
15539
|
continue;
|
|
15430
15540
|
}
|
|
15541
|
+
if (route.servers && !route.servers.includes(name)) {
|
|
15542
|
+
continue;
|
|
15543
|
+
}
|
|
15431
15544
|
if (mcp.knownHostInjected.includes(name)) {
|
|
15432
15545
|
conflicts.push({ name, message: collisionMessage(name, agentId), remediation: collisionRemediation(name) });
|
|
15433
15546
|
continue;
|
|
15434
15547
|
}
|
|
15435
|
-
const
|
|
15436
|
-
if (
|
|
15548
|
+
const secretMatch = findLiteralSecret(def);
|
|
15549
|
+
if (secretMatch?.field === "staticEnv") {
|
|
15437
15550
|
conflicts.push({
|
|
15438
15551
|
name,
|
|
15439
|
-
message: `refusing to write MCP server "${name}": a value matches a known-dangerous literal pattern (${
|
|
15552
|
+
message: `refusing to write MCP server "${name}": a value matches a known-dangerous literal pattern (${secretMatch.label}) \u2014 configs must hold variable NAMES only, never real values (docs/research.md "Secrets")`,
|
|
15440
15553
|
remediation: `replace the literal value in servers.yaml with a \`\${VAR_NAME}\` reference and put the real value wherever secrets.policy.yaml resolves it from`
|
|
15441
15554
|
});
|
|
15442
15555
|
continue;
|
|
@@ -27575,6 +27688,7 @@ function extractTemplateVarNames(value) {
|
|
|
27575
27688
|
|
|
27576
27689
|
// src/lib/mcpConnect.ts
|
|
27577
27690
|
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
27691
|
+
var FAILED_TRANSPORT_CLEANUP_TIMEOUT_MS = 1e3;
|
|
27578
27692
|
function withTimeout(promise, timeoutMs, message) {
|
|
27579
27693
|
return new Promise((resolve, reject) => {
|
|
27580
27694
|
const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
@@ -27596,7 +27710,11 @@ async function connectWithCleanup(client, transport, timeoutMs, message) {
|
|
|
27596
27710
|
return client;
|
|
27597
27711
|
} catch (err) {
|
|
27598
27712
|
try {
|
|
27599
|
-
await
|
|
27713
|
+
await withTimeout(
|
|
27714
|
+
Promise.resolve().then(() => transport.close()),
|
|
27715
|
+
FAILED_TRANSPORT_CLEANUP_TIMEOUT_MS,
|
|
27716
|
+
"transport cleanup timed out"
|
|
27717
|
+
);
|
|
27600
27718
|
} catch {
|
|
27601
27719
|
}
|
|
27602
27720
|
throw err;
|
|
@@ -27658,6 +27776,325 @@ function connectServer(def, secretsPolicy, timeoutMs, clientInfo) {
|
|
|
27658
27776
|
return connectStdio(def, secretsPolicy, timeoutMs, clientInfo);
|
|
27659
27777
|
}
|
|
27660
27778
|
|
|
27779
|
+
// src/lib/mcpToolRegistry.ts
|
|
27780
|
+
var MAX_EXPOSED_TOOL_NAME_LENGTH = 64;
|
|
27781
|
+
var SHORT_EXPOSED_TOOL_NAME_LENGTH = 12;
|
|
27782
|
+
var EXPOSED_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
27783
|
+
var TOOL_NAME_SEPARATOR = "__";
|
|
27784
|
+
function stableHash(value) {
|
|
27785
|
+
let hash = 2166136261;
|
|
27786
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
27787
|
+
hash ^= value.charCodeAt(index);
|
|
27788
|
+
hash = Math.imul(hash, 16777619);
|
|
27789
|
+
}
|
|
27790
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
27791
|
+
}
|
|
27792
|
+
function safeSegment(value, fallback) {
|
|
27793
|
+
const normalized = value.replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
27794
|
+
return normalized || fallback;
|
|
27795
|
+
}
|
|
27796
|
+
function compactSourceSegment(value) {
|
|
27797
|
+
const normalized = safeSegment(value, "server");
|
|
27798
|
+
return safeSegment(normalized.replace(/^mcp(?:_|-)+/i, ""), "server");
|
|
27799
|
+
}
|
|
27800
|
+
function boundedName(value, identity) {
|
|
27801
|
+
if (value.length <= MAX_EXPOSED_TOOL_NAME_LENGTH && EXPOSED_TOOL_NAME_PATTERN.test(value)) return value;
|
|
27802
|
+
const suffix = `__${stableHash(identity)}`;
|
|
27803
|
+
const prefixLength = Math.max(1, MAX_EXPOSED_TOOL_NAME_LENGTH - suffix.length);
|
|
27804
|
+
return `${value.slice(0, prefixLength)}${suffix}`;
|
|
27805
|
+
}
|
|
27806
|
+
function normalizedToolName(toolName) {
|
|
27807
|
+
return boundedName(safeSegment(toolName, "tool"), `tool\0${toolName}`);
|
|
27808
|
+
}
|
|
27809
|
+
function prefixedToolName(serverName, toolName) {
|
|
27810
|
+
const server = compactSourceSegment(serverName);
|
|
27811
|
+
const tool = safeSegment(toolName, "tool");
|
|
27812
|
+
return boundedName(`${server}${TOOL_NAME_SEPARATOR}${tool}`, `prefixed\0${serverName}\0${toolName}`);
|
|
27813
|
+
}
|
|
27814
|
+
function allocateExposedToolNames(candidates) {
|
|
27815
|
+
const counts = /* @__PURE__ */ new Map();
|
|
27816
|
+
for (const candidate of candidates) counts.set(candidate.toolName, (counts.get(candidate.toolName) ?? 0) + 1);
|
|
27817
|
+
const ordered = candidates.map((candidate, index) => ({ candidate, index })).sort((left, right) => left.candidate.serverName.localeCompare(right.candidate.serverName) || left.candidate.toolName.localeCompare(right.candidate.toolName) || left.index - right.index);
|
|
27818
|
+
const allocated = new Array(candidates.length);
|
|
27819
|
+
const used = /* @__PURE__ */ new Set();
|
|
27820
|
+
for (const { candidate, index } of ordered) {
|
|
27821
|
+
const normalized = normalizedToolName(candidate.toolName);
|
|
27822
|
+
const source = compactSourceSegment(candidate.serverName);
|
|
27823
|
+
const sourceQualified = normalized.includes("__") || normalized === source || normalized.startsWith(`${source}_`) || normalized.startsWith(`${source}__`);
|
|
27824
|
+
const unique = (counts.get(candidate.toolName) ?? 0) === 1;
|
|
27825
|
+
const base = unique && normalized.length > SHORT_EXPOSED_TOOL_NAME_LENGTH ? normalized : unique && sourceQualified ? normalized : prefixedToolName(candidate.serverName, candidate.toolName);
|
|
27826
|
+
let exposed = base;
|
|
27827
|
+
let suffix = 2;
|
|
27828
|
+
while (used.has(exposed)) {
|
|
27829
|
+
exposed = boundedName(`${base}${TOOL_NAME_SEPARATOR}${suffix}`, `collision\0${candidate.serverName}\0${candidate.toolName}\0${suffix}`);
|
|
27830
|
+
suffix += 1;
|
|
27831
|
+
}
|
|
27832
|
+
used.add(exposed);
|
|
27833
|
+
allocated[index] = exposed;
|
|
27834
|
+
}
|
|
27835
|
+
return allocated;
|
|
27836
|
+
}
|
|
27837
|
+
var McpToolRegistry = class {
|
|
27838
|
+
entries = /* @__PURE__ */ new Map();
|
|
27839
|
+
order = [];
|
|
27840
|
+
candidates = [];
|
|
27841
|
+
rebuild() {
|
|
27842
|
+
this.entries.clear();
|
|
27843
|
+
this.order.length = 0;
|
|
27844
|
+
const allocated = allocateExposedToolNames(this.candidates);
|
|
27845
|
+
const ordered = this.candidates.map((candidate, index) => ({ candidate, index, exposed: allocated[index] })).sort((left, right) => left.candidate.serverName.localeCompare(right.candidate.serverName) || left.candidate.toolName.localeCompare(right.candidate.toolName) || left.index - right.index);
|
|
27846
|
+
for (const { candidate, exposed } of ordered) {
|
|
27847
|
+
this.entries.set(exposed, candidate);
|
|
27848
|
+
this.order.push(exposed);
|
|
27849
|
+
}
|
|
27850
|
+
}
|
|
27851
|
+
/**
|
|
27852
|
+
* Register one connected upstream's tools. Callers add each upstream
|
|
27853
|
+
* independently, so an upstream that failed to connect or to list its
|
|
27854
|
+
* tools is simply never added — every other upstream's tools remain
|
|
27855
|
+
* aggregated, with no special-casing here.
|
|
27856
|
+
*/
|
|
27857
|
+
add(serverName, upstream, tools) {
|
|
27858
|
+
for (const tool of tools) this.candidates.push({ serverName, toolName: tool.name, upstream, tool });
|
|
27859
|
+
this.rebuild();
|
|
27860
|
+
const added = tools.map((tool) => this.exposedNameFor(serverName, tool.name)).filter((name) => name !== void 0);
|
|
27861
|
+
return { added, skipped: [] };
|
|
27862
|
+
}
|
|
27863
|
+
exposedNameFor(serverName, toolName) {
|
|
27864
|
+
for (const [name, entry] of this.entries) {
|
|
27865
|
+
if (entry.serverName === serverName && entry.toolName === toolName) return name;
|
|
27866
|
+
}
|
|
27867
|
+
return void 0;
|
|
27868
|
+
}
|
|
27869
|
+
/** Every registered tool, under its prefixed name, in registration
|
|
27870
|
+
* order. Each tool's own `description`/`inputSchema` and any additional
|
|
27871
|
+
* fields the upstream sent are preserved verbatim; only `name` changes. */
|
|
27872
|
+
listTools() {
|
|
27873
|
+
this.rebuild();
|
|
27874
|
+
return this.order.map((name) => {
|
|
27875
|
+
const entry = this.entries.get(name);
|
|
27876
|
+
const title = name === entry.toolName || entry.tool.title !== void 0 ? entry.tool.title : `${entry.serverName} / ${entry.toolName}`;
|
|
27877
|
+
return { ...entry.tool, name, ...title === void 0 ? {} : { title } };
|
|
27878
|
+
});
|
|
27879
|
+
}
|
|
27880
|
+
/** The owning upstream and original tool name for a prefixed name, or
|
|
27881
|
+
* `undefined` if nothing owns it. */
|
|
27882
|
+
resolve(prefixedName) {
|
|
27883
|
+
this.rebuild();
|
|
27884
|
+
const entry = this.entries.get(prefixedName);
|
|
27885
|
+
if (!entry) return void 0;
|
|
27886
|
+
return { serverName: entry.serverName, toolName: entry.toolName, upstream: entry.upstream };
|
|
27887
|
+
}
|
|
27888
|
+
/**
|
|
27889
|
+
* Route a call to the owning upstream, unprefixing the name on the way
|
|
27890
|
+
* out — an upstream knows its tool only by its own name, never by the
|
|
27891
|
+
* aggregated one. A name no upstream owns throws rather than guessing.
|
|
27892
|
+
*/
|
|
27893
|
+
async callTool(prefixedName, args) {
|
|
27894
|
+
const target = this.resolve(prefixedName);
|
|
27895
|
+
if (!target) {
|
|
27896
|
+
throw new Error(`unknown tool "${prefixedName}"`);
|
|
27897
|
+
}
|
|
27898
|
+
return target.upstream.callTool({ name: target.toolName, arguments: args });
|
|
27899
|
+
}
|
|
27900
|
+
/** Distinct server names with at least one registered tool, in
|
|
27901
|
+
* registration order. */
|
|
27902
|
+
servers() {
|
|
27903
|
+
this.rebuild();
|
|
27904
|
+
const seen = [];
|
|
27905
|
+
for (const name of this.order) {
|
|
27906
|
+
const { serverName } = this.entries.get(name);
|
|
27907
|
+
if (!seen.includes(serverName)) seen.push(serverName);
|
|
27908
|
+
}
|
|
27909
|
+
return seen;
|
|
27910
|
+
}
|
|
27911
|
+
get size() {
|
|
27912
|
+
this.rebuild();
|
|
27913
|
+
return this.entries.size;
|
|
27914
|
+
}
|
|
27915
|
+
};
|
|
27916
|
+
|
|
27917
|
+
// src/lib/oauth/store.ts
|
|
27918
|
+
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
27919
|
+
import { join as join2 } from "node:path";
|
|
27920
|
+
function oauthDir(homeDir) {
|
|
27921
|
+
return join2(homeDir, ".trellis", "mcp", "oauth");
|
|
27922
|
+
}
|
|
27923
|
+
function tokenPath(homeDir, serverName) {
|
|
27924
|
+
assertSafeServerName(serverName);
|
|
27925
|
+
return join2(oauthDir(homeDir), `${serverName}.json`);
|
|
27926
|
+
}
|
|
27927
|
+
function lockPath(homeDir, serverName) {
|
|
27928
|
+
assertSafeServerName(serverName);
|
|
27929
|
+
return join2(oauthDir(homeDir), `${serverName}.lock`);
|
|
27930
|
+
}
|
|
27931
|
+
function assertSafeServerName(serverName) {
|
|
27932
|
+
if (serverName === "" || serverName === "." || serverName === ".." || /[/\\]/.test(serverName) || serverName.startsWith(".")) {
|
|
27933
|
+
throw new Error(`refusing to use MCP server name "${serverName}" as an OAuth credential filename`);
|
|
27934
|
+
}
|
|
27935
|
+
}
|
|
27936
|
+
function readToken(homeDir, serverName) {
|
|
27937
|
+
const path = tokenPath(homeDir, serverName);
|
|
27938
|
+
if (!existsSync3(path)) return void 0;
|
|
27939
|
+
try {
|
|
27940
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
27941
|
+
} catch {
|
|
27942
|
+
return void 0;
|
|
27943
|
+
}
|
|
27944
|
+
}
|
|
27945
|
+
function writeToken(homeDir, serverName, token) {
|
|
27946
|
+
const dir = oauthDir(homeDir);
|
|
27947
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
27948
|
+
const path = tokenPath(homeDir, serverName);
|
|
27949
|
+
const tmp = `${path}.tmp`;
|
|
27950
|
+
writeFileSync2(tmp, `${JSON.stringify(token, null, 2)}
|
|
27951
|
+
`, { mode: 384 });
|
|
27952
|
+
chmodSync(tmp, 384);
|
|
27953
|
+
renameSync(tmp, path);
|
|
27954
|
+
chmodSync(path, 384);
|
|
27955
|
+
}
|
|
27956
|
+
function isExpired(token, skewMs = 6e4, now = Date.now()) {
|
|
27957
|
+
if (token.expiresAt === void 0) return false;
|
|
27958
|
+
return token.expiresAt - skewMs <= now;
|
|
27959
|
+
}
|
|
27960
|
+
|
|
27961
|
+
// src/lib/oauth/lock.ts
|
|
27962
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, openSync, closeSync, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
27963
|
+
import { dirname as dirname2 } from "node:path";
|
|
27964
|
+
var STALE_AFTER_MS = 6e4;
|
|
27965
|
+
var POLL_INTERVAL_MS = 50;
|
|
27966
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
27967
|
+
function processAlive(pid) {
|
|
27968
|
+
try {
|
|
27969
|
+
process.kill(pid, 0);
|
|
27970
|
+
return true;
|
|
27971
|
+
} catch (err) {
|
|
27972
|
+
return err.code === "EPERM";
|
|
27973
|
+
}
|
|
27974
|
+
}
|
|
27975
|
+
function isStale(path, now) {
|
|
27976
|
+
let contents;
|
|
27977
|
+
try {
|
|
27978
|
+
contents = JSON.parse(readFileSync4(path, "utf-8"));
|
|
27979
|
+
} catch {
|
|
27980
|
+
return true;
|
|
27981
|
+
}
|
|
27982
|
+
if (typeof contents.pid !== "number" || typeof contents.acquiredAt !== "number") return true;
|
|
27983
|
+
if (now - contents.acquiredAt > STALE_AFTER_MS) return true;
|
|
27984
|
+
return !processAlive(contents.pid);
|
|
27985
|
+
}
|
|
27986
|
+
async function withServerLock(homeDir, serverName, fn, opts = {}) {
|
|
27987
|
+
const path = lockPath(homeDir, serverName);
|
|
27988
|
+
const now = opts.now ?? Date.now;
|
|
27989
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
27990
|
+
const deadline = now() + timeoutMs;
|
|
27991
|
+
mkdirSync4(dirname2(path), { recursive: true, mode: 448 });
|
|
27992
|
+
for (; ; ) {
|
|
27993
|
+
try {
|
|
27994
|
+
const fd = openSync(path, "wx", 384);
|
|
27995
|
+
try {
|
|
27996
|
+
writeFileSync3(fd, JSON.stringify({ pid: process.pid, acquiredAt: now() }));
|
|
27997
|
+
} finally {
|
|
27998
|
+
closeSync(fd);
|
|
27999
|
+
}
|
|
28000
|
+
break;
|
|
28001
|
+
} catch (err) {
|
|
28002
|
+
if (err.code !== "EEXIST") throw err;
|
|
28003
|
+
if (existsSync4(path) && isStale(path, now())) {
|
|
28004
|
+
rmSync2(path, { force: true });
|
|
28005
|
+
continue;
|
|
28006
|
+
}
|
|
28007
|
+
if (now() >= deadline) {
|
|
28008
|
+
throw new Error(`timed out waiting for the OAuth refresh lock on "${serverName}" (${path})`);
|
|
28009
|
+
}
|
|
28010
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
28011
|
+
}
|
|
28012
|
+
}
|
|
28013
|
+
try {
|
|
28014
|
+
return await fn();
|
|
28015
|
+
} finally {
|
|
28016
|
+
rmSync2(path, { force: true });
|
|
28017
|
+
}
|
|
28018
|
+
}
|
|
28019
|
+
|
|
28020
|
+
// src/lib/oauth/refresh.ts
|
|
28021
|
+
var RefreshError = class extends Error {
|
|
28022
|
+
};
|
|
28023
|
+
async function ensureFreshToken(homeDir, serverName, opts = {}) {
|
|
28024
|
+
const now = opts.now ?? Date.now;
|
|
28025
|
+
const existing = readToken(homeDir, serverName);
|
|
28026
|
+
if (!existing) return void 0;
|
|
28027
|
+
if (!isExpired(existing, opts.skewMs, now())) return existing;
|
|
28028
|
+
if (!existing.refreshToken) {
|
|
28029
|
+
throw new RefreshError(`OAuth token for "${serverName}" has expired and there is no refresh token \u2014 run \`trellis mcp auth ${serverName}\``);
|
|
28030
|
+
}
|
|
28031
|
+
return withServerLock(
|
|
28032
|
+
homeDir,
|
|
28033
|
+
serverName,
|
|
28034
|
+
async () => {
|
|
28035
|
+
const current = readToken(homeDir, serverName);
|
|
28036
|
+
if (current && !isExpired(current, opts.skewMs, now())) return current;
|
|
28037
|
+
const source = current ?? existing;
|
|
28038
|
+
if (!source.refreshToken) {
|
|
28039
|
+
throw new RefreshError(`OAuth token for "${serverName}" has expired and there is no refresh token \u2014 run \`trellis mcp auth ${serverName}\``);
|
|
28040
|
+
}
|
|
28041
|
+
const refreshed = await performRefresh(serverName, source, opts);
|
|
28042
|
+
writeToken(homeDir, serverName, refreshed);
|
|
28043
|
+
return refreshed;
|
|
28044
|
+
},
|
|
28045
|
+
{ timeoutMs: opts.lockTimeoutMs, now }
|
|
28046
|
+
);
|
|
28047
|
+
}
|
|
28048
|
+
async function performRefresh(serverName, token, opts = {}) {
|
|
28049
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
28050
|
+
const now = opts.now ?? Date.now;
|
|
28051
|
+
if (!token.tokenEndpoint) {
|
|
28052
|
+
throw new RefreshError(`OAuth token for "${serverName}" has no recorded token endpoint \u2014 run \`trellis mcp auth ${serverName}\``);
|
|
28053
|
+
}
|
|
28054
|
+
const body = new URLSearchParams({ grant_type: "refresh_token", refresh_token: token.refreshToken });
|
|
28055
|
+
if (token.clientId) body.set("client_id", token.clientId);
|
|
28056
|
+
if (token.clientSecret) body.set("client_secret", token.clientSecret);
|
|
28057
|
+
const response = await fetchImpl(token.tokenEndpoint, {
|
|
28058
|
+
method: "POST",
|
|
28059
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
28060
|
+
body: body.toString()
|
|
28061
|
+
});
|
|
28062
|
+
if (!response.ok) {
|
|
28063
|
+
throw new RefreshError(`refresh_token grant for "${serverName}" failed with HTTP ${response.status}: ${await safeText(response)}`);
|
|
28064
|
+
}
|
|
28065
|
+
const payload = await response.json();
|
|
28066
|
+
if (!payload.access_token) {
|
|
28067
|
+
throw new RefreshError(`refresh_token grant for "${serverName}" returned no access_token`);
|
|
28068
|
+
}
|
|
28069
|
+
return {
|
|
28070
|
+
...token,
|
|
28071
|
+
accessToken: payload.access_token,
|
|
28072
|
+
// Rotation: a server that issues a new refresh token has invalidated
|
|
28073
|
+
// the old one, so keeping the old one would be keeping a dead
|
|
28074
|
+
// credential. A server that omits it is signalling the existing one
|
|
28075
|
+
// stays valid.
|
|
28076
|
+
refreshToken: payload.refresh_token ?? token.refreshToken,
|
|
28077
|
+
expiresAt: payload.expires_in !== void 0 ? now() + payload.expires_in * 1e3 : void 0,
|
|
28078
|
+
scope: payload.scope ?? token.scope
|
|
28079
|
+
};
|
|
28080
|
+
}
|
|
28081
|
+
async function safeText(response) {
|
|
28082
|
+
try {
|
|
28083
|
+
return (await response.text()).slice(0, 200);
|
|
28084
|
+
} catch {
|
|
28085
|
+
return "<unreadable body>";
|
|
28086
|
+
}
|
|
28087
|
+
}
|
|
28088
|
+
|
|
28089
|
+
// src/lib/gatewayBackend.ts
|
|
28090
|
+
async function withOAuthHeader(name, def, homeDir) {
|
|
28091
|
+
const hasExplicitAuth = Object.keys(def.headers ?? {}).some((key) => key.toLowerCase() === "authorization");
|
|
28092
|
+
if (hasExplicitAuth) return def;
|
|
28093
|
+
const token = await ensureFreshToken(homeDir, name);
|
|
28094
|
+
if (!token) return def;
|
|
28095
|
+
return { ...def, headers: { ...def.headers ?? {}, Authorization: `Bearer ${token.accessToken}` } };
|
|
28096
|
+
}
|
|
28097
|
+
|
|
27661
28098
|
// node_modules/typebox/build/system/memory/memory.mjs
|
|
27662
28099
|
var memory_exports = {};
|
|
27663
28100
|
__export(memory_exports, {
|
|
@@ -32062,12 +32499,6 @@ __export(typebox_exports, {
|
|
|
32062
32499
|
With: () => With2
|
|
32063
32500
|
});
|
|
32064
32501
|
|
|
32065
|
-
// src/lib/mcpToolRegistry.ts
|
|
32066
|
-
var TOOL_NAME_SEPARATOR = "__";
|
|
32067
|
-
function prefixedToolName(serverName, toolName) {
|
|
32068
|
-
return `${serverName}${TOOL_NAME_SEPARATOR}${toolName}`;
|
|
32069
|
-
}
|
|
32070
|
-
|
|
32071
32502
|
// src/pi-bridge/schemaTranslate.ts
|
|
32072
32503
|
function toParametersSchema(inputSchema) {
|
|
32073
32504
|
return typebox_exports.Unsafe(inputSchema);
|
|
@@ -32091,27 +32522,14 @@ function toPiContent(items) {
|
|
|
32091
32522
|
|
|
32092
32523
|
// src/pi-bridge/index.ts
|
|
32093
32524
|
var CLIENT_INFO = { name: "trellis-mcp-bridge", version: "0.0.0" };
|
|
32094
|
-
function
|
|
32095
|
-
return withTimeout(client.listTools(), timeoutMs, `listTools timed out after ${timeoutMs}ms`).then((result) =>
|
|
32096
|
-
for (const tool of result.tools) {
|
|
32097
|
-
pi.registerTool({
|
|
32098
|
-
name: prefixedToolName(serverName, tool.name),
|
|
32099
|
-
label: tool.name,
|
|
32100
|
-
description: tool.description ?? `MCP tool "${tool.name}" from server "${serverName}"`,
|
|
32101
|
-
parameters: toParametersSchema(tool.inputSchema),
|
|
32102
|
-
async execute(_toolCallId, params) {
|
|
32103
|
-
const result2 = await client.callTool({ name: tool.name, arguments: params });
|
|
32104
|
-
const content = Array.isArray(result2.content) ? result2.content : [];
|
|
32105
|
-
return { content: toPiContent(content), details: result2 };
|
|
32106
|
-
}
|
|
32107
|
-
});
|
|
32108
|
-
}
|
|
32109
|
-
});
|
|
32525
|
+
function listServerTools(client, timeoutMs) {
|
|
32526
|
+
return withTimeout(client.listTools(), timeoutMs, `listTools timed out after ${timeoutMs}ms`).then((result) => result.tools);
|
|
32110
32527
|
}
|
|
32111
32528
|
async function trellisMcpBridge(pi, homeDir = homedir2(), connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS) {
|
|
32112
32529
|
const canonical = loadCanonicalSource(homeDir);
|
|
32113
32530
|
const { desired } = resolveMcpPlan("pi", canonical.mcp, ALL_AGENTS, canonical.secretsPolicy);
|
|
32114
32531
|
const clients = /* @__PURE__ */ new Set();
|
|
32532
|
+
const registry2 = new McpToolRegistry();
|
|
32115
32533
|
const closeClient = async (client) => {
|
|
32116
32534
|
if (!clients.delete(client)) return;
|
|
32117
32535
|
try {
|
|
@@ -32134,24 +32552,48 @@ async function trellisMcpBridge(pi, homeDir = homedir2(), connectTimeoutMs = DEF
|
|
|
32134
32552
|
);
|
|
32135
32553
|
};
|
|
32136
32554
|
pi.on?.("session_shutdown", closeAllClients);
|
|
32137
|
-
await Promise.all(
|
|
32555
|
+
const discovered = await Promise.all(
|
|
32138
32556
|
desired.map(async ({ name, def }) => {
|
|
32139
32557
|
let client;
|
|
32140
32558
|
try {
|
|
32141
|
-
|
|
32559
|
+
const effectiveDef = def.auth === "oauth" && def.url ? await withOAuthHeader(name, def, homeDir) : def;
|
|
32560
|
+
client = await connectServer(effectiveDef, canonical.secretsPolicy, connectTimeoutMs, CLIENT_INFO);
|
|
32142
32561
|
} catch (err) {
|
|
32143
32562
|
console.error(`trellis-mcp-bridge: failed to connect to MCP server "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
32144
|
-
return;
|
|
32563
|
+
return void 0;
|
|
32145
32564
|
}
|
|
32146
32565
|
clients.add(client);
|
|
32147
32566
|
try {
|
|
32148
|
-
await
|
|
32567
|
+
const tools = await listServerTools(client, connectTimeoutMs);
|
|
32568
|
+
return { name, client, tools };
|
|
32149
32569
|
} catch (err) {
|
|
32150
32570
|
console.error(`trellis-mcp-bridge: failed to list tools for MCP server "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
32151
32571
|
await closeClient(client);
|
|
32572
|
+
return void 0;
|
|
32152
32573
|
}
|
|
32153
32574
|
})
|
|
32154
32575
|
);
|
|
32576
|
+
for (const result of discovered) {
|
|
32577
|
+
if (!result) continue;
|
|
32578
|
+
registry2.add(result.name, result.client, result.tools);
|
|
32579
|
+
}
|
|
32580
|
+
for (const tool of registry2.listTools()) {
|
|
32581
|
+
const target = registry2.resolve(tool.name);
|
|
32582
|
+
if (!target) continue;
|
|
32583
|
+
const originalName = target.toolName;
|
|
32584
|
+
const serverName = target.serverName;
|
|
32585
|
+
pi.registerTool({
|
|
32586
|
+
name: tool.name,
|
|
32587
|
+
label: originalName,
|
|
32588
|
+
description: tool.description ?? `MCP tool "${originalName}" from server "${serverName}"`,
|
|
32589
|
+
parameters: toParametersSchema(tool.inputSchema),
|
|
32590
|
+
async execute(_toolCallId, params) {
|
|
32591
|
+
const result = await registry2.callTool(tool.name, params);
|
|
32592
|
+
const content = Array.isArray(result.content) ? result.content : [];
|
|
32593
|
+
return { content: toPiContent(content), details: result };
|
|
32594
|
+
}
|
|
32595
|
+
});
|
|
32596
|
+
}
|
|
32155
32597
|
}
|
|
32156
32598
|
export {
|
|
32157
32599
|
trellisMcpBridge as default,
|