agent-trellis 0.7.0 → 0.8.2
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 +48 -153
- 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 +6 -1
- package/dist/adapters/mcpPlan.js +106 -8
- package/dist/adapters/pi.js +22 -3
- package/dist/adapters/symlinkPlan.d.ts +7 -0
- package/dist/adapters/symlinkPlan.js +16 -7
- package/dist/adapters/zcode.d.ts +21 -0
- package/dist/adapters/zcode.js +234 -0
- package/dist/cli.js +145 -29
- package/dist/commands/doctor.d.ts +7 -2
- package/dist/commands/doctor.js +40 -2
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.js +29 -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 +77 -14
- package/dist/commands/mcp.js +169 -29
- package/dist/commands/mcpGateway.d.ts +6 -0
- package/dist/commands/mcpGateway.js +35 -14
- package/dist/commands/mcpImport.d.ts +46 -0
- package/dist/commands/mcpImport.js +359 -0
- package/dist/commands/memory.d.ts +17 -3
- package/dist/commands/memory.js +27 -8
- package/dist/commands/migrate.d.ts +16 -5
- package/dist/commands/migrate.js +93 -17
- package/dist/commands/onboard.d.ts +74 -2
- package/dist/commands/onboard.js +469 -71
- package/dist/commands/rollback.js +28 -2
- package/dist/commands/secretsAudit.js +4 -1
- package/dist/commands/skill.d.ts +52 -6
- package/dist/commands/skill.js +114 -15
- package/dist/commands/sync.js +5 -1
- package/dist/core/adapter.d.ts +11 -1
- package/dist/core/canonical.d.ts +15 -7
- package/dist/core/canonical.js +101 -13
- 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 +8 -0
- package/dist/lib/agentBridge.d.ts +169 -0
- package/dist/lib/agentBridge.js +367 -0
- package/dist/lib/agentBridgeProvider.d.ts +14 -0
- package/dist/lib/agentBridgeProvider.js +95 -0
- package/dist/lib/backup.d.ts +32 -0
- package/dist/lib/backup.js +20 -1
- package/dist/lib/builtinSkills.d.ts +2 -0
- package/dist/lib/builtinSkills.js +4 -0
- package/dist/lib/chatAgents.d.ts +42 -0
- package/dist/lib/chatAgents.js +59 -0
- package/dist/lib/cliMetadata.d.ts +1 -0
- package/dist/lib/cliMetadata.js +14 -0
- package/dist/lib/envVarNames.d.ts +3 -0
- package/dist/lib/envVarNames.js +19 -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 +2 -0
- package/dist/lib/mcpMigrateRead.js +22 -1
- 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/realHomeSnapshot.js +5 -0
- package/dist/lib/runtimeControlProvider.d.ts +16 -0
- package/dist/lib/runtimeControlProvider.js +170 -0
- package/dist/lib/secretEnv.d.ts +2 -1
- package/dist/lib/secretEnv.js +6 -2
- 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 -49
- package/dist/lib/terminalPicker.js +80 -218
- package/dist/lib/zcodeOwnership.d.ts +17 -0
- package/dist/lib/zcodeOwnership.js +37 -0
- package/dist/pi-bridge/bundle.js +476 -37
- 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/codex.d.ts +7 -0
- package/dist/probes/codex.js +37 -5
- package/dist/probes/kimi-code.d.ts +12 -0
- package/dist/probes/kimi-code.js +55 -0
- package/dist/probes/zcode.d.ts +32 -0
- package/dist/probes/zcode.js +140 -0
- package/dist/sdk.d.ts +1 -1
- package/docs/architecture.md +230 -28
- package/docs/assets/brand-v3/trellis-mark-v3-a.png +0 -0
- package/docs/assets/brand-v3/trellis-mark-v3-a.svg +10 -0
- package/docs/assets/poster-series-v3/README.md +60 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.svg +51 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.svg +51 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.svg +48 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.svg +48 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.svg +56 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.svg +56 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.svg +47 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.svg +47 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.svg +51 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.svg +51 -0
- package/docs/getting-started.md +496 -42
- package/docs/overview.md +134 -0
- package/docs/overview.zh-CN.md +102 -0
- package/docs/research.md +59 -0
- package/docs/roadmap.md +11 -6
- package/docs/video/README.md +76 -0
- package/docs/video/build/audio/01-opening.aiff +0 -0
- package/docs/video/build/audio/01-opening.mp3 +0 -0
- package/docs/video/build/audio/02-pain.aiff +0 -0
- package/docs/video/build/audio/02-pain.mp3 +0 -0
- package/docs/video/build/audio/03-solution.aiff +0 -0
- package/docs/video/build/audio/03-solution.mp3 +0 -0
- package/docs/video/build/audio/04-install.aiff +0 -0
- package/docs/video/build/audio/04-install.mp3 +0 -0
- package/docs/video/build/audio/04-setup.mp3 +0 -0
- package/docs/video/build/audio/05-dryrun.mp3 +0 -0
- package/docs/video/build/audio/05-init.aiff +0 -0
- package/docs/video/build/audio/05-init.mp3 +0 -0
- package/docs/video/build/audio/05-interactive.mp3 +0 -0
- package/docs/video/build/audio/06-dryrun.aiff +0 -0
- package/docs/video/build/audio/06-dryrun.mp3 +0 -0
- package/docs/video/build/audio/06-onboard.mp3 +0 -0
- package/docs/video/build/audio/06-run.mp3 +0 -0
- package/docs/video/build/audio/07-onboard.aiff +0 -0
- package/docs/video/build/audio/07-onboard.mp3 +0 -0
- package/docs/video/build/audio/07-verify.mp3 +0 -0
- package/docs/video/build/audio/08-closing.mp3 +0 -0
- package/docs/video/build/audio/08-doctor.aiff +0 -0
- package/docs/video/build/audio/08-doctor.mp3 +0 -0
- package/docs/video/build/audio/09-rollback.aiff +0 -0
- package/docs/video/build/audio/09-rollback.mp3 +0 -0
- package/docs/video/build/audio/10-principles.aiff +0 -0
- package/docs/video/build/audio/10-principles.mp3 +0 -0
- package/docs/video/build/audio/11-closing.aiff +0 -0
- package/docs/video/build/audio/11-closing.mp3 +0 -0
- package/docs/video/build/frames/01-opening-0.png +0 -0
- package/docs/video/build/frames/02-pain-1.png +0 -0
- package/docs/video/build/frames/02-pain-2.png +0 -0
- package/docs/video/build/frames/03-solution-3.png +0 -0
- package/docs/video/build/frames/04-install-4.png +0 -0
- package/docs/video/build/frames/04-setup-4.png +0 -0
- package/docs/video/build/frames/04-setup-5.png +0 -0
- package/docs/video/build/frames/05-dryrun-6.png +0 -0
- package/docs/video/build/frames/05-init-5.png +0 -0
- package/docs/video/build/frames/05-interactive-5.png +0 -0
- package/docs/video/build/frames/05-interactive-6.png +0 -0
- package/docs/video/build/frames/06-dryrun-6.png +0 -0
- package/docs/video/build/frames/06-dryrun-7.png +0 -0
- package/docs/video/build/frames/06-onboard-7.png +0 -0
- package/docs/video/build/frames/06-onboard-8.png +0 -0
- package/docs/video/build/frames/06-run-7.png +0 -0
- package/docs/video/build/frames/06-run-8.png +0 -0
- package/docs/video/build/frames/07-onboard-10.png +0 -0
- package/docs/video/build/frames/07-onboard-7.png +0 -0
- package/docs/video/build/frames/07-onboard-8.png +0 -0
- package/docs/video/build/frames/07-onboard-9.png +0 -0
- package/docs/video/build/frames/07-verify-10.png +0 -0
- package/docs/video/build/frames/07-verify-8.png +0 -0
- package/docs/video/build/frames/07-verify-9.png +0 -0
- package/docs/video/build/frames/08-closing-10.png +0 -0
- package/docs/video/build/frames/08-closing-11.png +0 -0
- package/docs/video/build/frames/08-doctor-11.png +0 -0
- package/docs/video/build/frames/08-doctor-9.png +0 -0
- package/docs/video/build/frames/09-rollback-10.png +0 -0
- package/docs/video/build/frames/09-rollback-12.png +0 -0
- package/docs/video/build/frames/10-principles-11.png +0 -0
- package/docs/video/build/frames/10-principles-13.png +0 -0
- package/docs/video/build/frames/11-closing-12.png +0 -0
- package/docs/video/build/frames/11-closing-14.png +0 -0
- package/docs/video/build/segments/01-opening.mp4 +0 -0
- package/docs/video/build/segments/01-opening.txt +3 -0
- package/docs/video/build/segments/02-pain.mp4 +0 -0
- package/docs/video/build/segments/02-pain.txt +5 -0
- package/docs/video/build/segments/03-solution.mp4 +0 -0
- package/docs/video/build/segments/03-solution.txt +3 -0
- package/docs/video/build/segments/04-install.mp4 +0 -0
- package/docs/video/build/segments/04-install.txt +3 -0
- package/docs/video/build/segments/04-setup.mp4 +0 -0
- package/docs/video/build/segments/04-setup.txt +3 -0
- package/docs/video/build/segments/05-dryrun.mp4 +0 -0
- package/docs/video/build/segments/05-dryrun.txt +3 -0
- package/docs/video/build/segments/05-init.mp4 +0 -0
- package/docs/video/build/segments/05-init.txt +3 -0
- package/docs/video/build/segments/05-interactive.mp4 +0 -0
- package/docs/video/build/segments/05-interactive.txt +5 -0
- package/docs/video/build/segments/06-dryrun.mp4 +0 -0
- package/docs/video/build/segments/06-dryrun.txt +3 -0
- package/docs/video/build/segments/06-onboard.mp4 +0 -0
- package/docs/video/build/segments/06-onboard.txt +5 -0
- package/docs/video/build/segments/06-run.mp4 +0 -0
- package/docs/video/build/segments/06-run.txt +3 -0
- package/docs/video/build/segments/07-onboard.mp4 +0 -0
- package/docs/video/build/segments/07-onboard.txt +5 -0
- package/docs/video/build/segments/07-verify.mp4 +0 -0
- package/docs/video/build/segments/07-verify.txt +5 -0
- package/docs/video/build/segments/08-closing.mp4 +0 -0
- package/docs/video/build/segments/08-closing.txt +3 -0
- package/docs/video/build/segments/08-doctor.mp4 +0 -0
- package/docs/video/build/segments/08-doctor.txt +3 -0
- package/docs/video/build/segments/09-rollback.mp4 +0 -0
- package/docs/video/build/segments/09-rollback.txt +3 -0
- package/docs/video/build/segments/10-principles.mp4 +0 -0
- package/docs/video/build/segments/10-principles.txt +3 -0
- package/docs/video/build/segments/11-closing.mp4 +0 -0
- package/docs/video/build/segments/11-closing.txt +3 -0
- package/docs/video/build/segments/final.txt +8 -0
- package/docs/video/build-ai/audio/01-hook.mp3 +0 -0
- package/docs/video/build-ai/audio/02-pain.mp3 +0 -0
- package/docs/video/build-ai/audio/03-solution.mp3 +0 -0
- package/docs/video/build-ai/audio/04-how.mp3 +0 -0
- package/docs/video/build-ai/audio/05-safe.mp3 +0 -0
- package/docs/video/build-ai/audio/06-closing.mp3 +0 -0
- package/docs/video/build-ai/clips/01-hook.mp4 +0 -0
- package/docs/video/build-ai/clips/02-pain.mp4 +0 -0
- package/docs/video/build-ai/clips/03-solution.mp4 +0 -0
- package/docs/video/build-ai/clips/04-how.mp4 +0 -0
- package/docs/video/build-ai/clips/05-safe.mp4 +0 -0
- package/docs/video/build-ai/clips/06-closing.mp4 +0 -0
- package/docs/video/build-ai/first-frame-02-pain.png +0 -0
- package/docs/video/build-ai/first-frame-03-solution.png +0 -0
- package/docs/video/build-ai/first-frame-04-how.png +0 -0
- package/docs/video/build-ai/first-frame-05-safe.png +0 -0
- package/docs/video/build-ai/segments/01-hook.caption.txt +2 -0
- package/docs/video/build-ai/segments/01-hook.mp4 +0 -0
- package/docs/video/build-ai/segments/02-pain.mp4 +0 -0
- package/docs/video/build-ai/segments/03-solution.mp4 +0 -0
- package/docs/video/build-ai/segments/04-how.mp4 +0 -0
- package/docs/video/build-ai/segments/05-safe.mp4 +0 -0
- package/docs/video/build-ai/segments/06-closing.mp4 +0 -0
- package/docs/video/build-ai/segments/cap-01-hook.png +0 -0
- package/docs/video/build-ai/segments/cap-02-pain.png +0 -0
- package/docs/video/build-ai/segments/cap-03-solution.png +0 -0
- package/docs/video/build-ai/segments/cap-04-how.png +0 -0
- package/docs/video/build-ai/segments/cap-05-safe.png +0 -0
- package/docs/video/build-ai/segments/cap-06-closing.png +0 -0
- package/docs/video/build-ai/segments/final.txt +6 -0
- package/docs/video/build-posters/audio/01-hook.mp3 +0 -0
- package/docs/video/build-posters/audio/02-pain.mp3 +0 -0
- package/docs/video/build-posters/audio/03-solution.mp3 +0 -0
- package/docs/video/build-posters/audio/04-how.mp3 +0 -0
- package/docs/video/build-posters/audio/05-safe.mp3 +0 -0
- package/docs/video/build-posters/audio/06-closing.mp3 +0 -0
- package/docs/video/build-posters/cap-01-hook.png +0 -0
- package/docs/video/build-posters/cap-02-pain.png +0 -0
- package/docs/video/build-posters/cap-03-solution.png +0 -0
- package/docs/video/build-posters/cap-04-how.png +0 -0
- package/docs/video/build-posters/cap-05-safe.png +0 -0
- package/docs/video/build-posters/cap-06-closing.png +0 -0
- package/docs/video/build-posters/card-01-hook.png +0 -0
- package/docs/video/build-posters/card-06-closing.png +0 -0
- package/docs/video/build-posters/segments/01-hook.mp4 +0 -0
- package/docs/video/build-posters/segments/02-pain.mp4 +0 -0
- package/docs/video/build-posters/segments/03-solution.mp4 +0 -0
- package/docs/video/build-posters/segments/04-how.mp4 +0 -0
- package/docs/video/build-posters/segments/05-safe.mp4 +0 -0
- package/docs/video/build-posters/segments/06-closing.mp4 +0 -0
- package/docs/video/build-posters/segments/final.txt +6 -0
- package/docs/video/generate-video-ai.mjs +334 -0
- package/docs/video/generate-video-posters.mjs +204 -0
- package/docs/video/generate-video.mjs +426 -0
- package/docs/video/outline.zh-CN.md +102 -0
- package/docs/video/trellis-demo-video-ai.mp4 +0 -0
- package/docs/video/trellis-demo-video.mp4 +0 -0
- package/docs/video/voiceover.zh-CN.md +49 -0
- package/package.json +6 -2
- 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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config for the GUI's in-app chat feature: which CLI agents can be chatted
|
|
3
|
+
* with from the desktop app, keyed by an arbitrary string id (not `AgentId`
|
|
4
|
+
* — chat targets are not necessarily fully-managed Agents with adapters,
|
|
5
|
+
* sync, and probes; they're just "a non-interactive CLI I can spawn and
|
|
6
|
+
* stream from"). Kept as its own file + config path, sibling to
|
|
7
|
+
* `agent-bridge.yaml`, so delegation's `ALL_AGENTS` constraint (needed for
|
|
8
|
+
* the cross-Agent-delegation use case) never has to bend for chat's needs.
|
|
9
|
+
*
|
|
10
|
+
* Configuration lives at `~/.trellis/mcp/chat-agents.yaml`. Its absence
|
|
11
|
+
* means zero targets, not an error — the same "optional, additive" shape
|
|
12
|
+
* as `agent-bridge.yaml`.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { parse as parseYaml } from "yaml";
|
|
17
|
+
const OUTPUT_FORMATS = ["text", "json", "stream-json"];
|
|
18
|
+
const STREAM_PROTOCOLS = ["claude", "zcode"];
|
|
19
|
+
export function chatAgentsPath(homeDir) {
|
|
20
|
+
return join(homeDir, ".trellis", "mcp", "chat-agents.yaml");
|
|
21
|
+
}
|
|
22
|
+
export function loadChatAgentConfig(homeDir) {
|
|
23
|
+
const path = chatAgentsPath(homeDir);
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
return { targets: {} };
|
|
26
|
+
const parsed = (parseYaml(readFileSync(path, "utf-8")) ?? {});
|
|
27
|
+
const targets = {};
|
|
28
|
+
for (const [id, raw] of Object.entries(parsed.targets ?? {})) {
|
|
29
|
+
if (!raw)
|
|
30
|
+
continue;
|
|
31
|
+
if (!raw.label || !raw.label.trim()) {
|
|
32
|
+
throw new Error(`chat-agents.yaml: target "${id}" must declare a non-empty "label"`);
|
|
33
|
+
}
|
|
34
|
+
if (!raw.command || !Array.isArray(raw.args) || raw.args.length === 0) {
|
|
35
|
+
throw new Error(`chat-agents.yaml: target "${id}" must declare "command" and a non-empty "args"`);
|
|
36
|
+
}
|
|
37
|
+
if (raw.resumeArgs !== undefined && (!Array.isArray(raw.resumeArgs) || raw.resumeArgs.length === 0)) {
|
|
38
|
+
throw new Error(`chat-agents.yaml: target "${id}" has an invalid "resumeArgs"`);
|
|
39
|
+
}
|
|
40
|
+
if (raw.outputFormat !== undefined && !OUTPUT_FORMATS.includes(raw.outputFormat)) {
|
|
41
|
+
throw new Error(`chat-agents.yaml: target "${id}" has unknown outputFormat "${raw.outputFormat}"`);
|
|
42
|
+
}
|
|
43
|
+
if (raw.streamProtocol !== undefined && !STREAM_PROTOCOLS.includes(raw.streamProtocol)) {
|
|
44
|
+
throw new Error(`chat-agents.yaml: target "${id}" has unknown streamProtocol "${raw.streamProtocol}"`);
|
|
45
|
+
}
|
|
46
|
+
targets[id] = {
|
|
47
|
+
label: raw.label,
|
|
48
|
+
command: raw.command,
|
|
49
|
+
args: raw.args,
|
|
50
|
+
...(raw.resumeArgs ? { resumeArgs: raw.resumeArgs } : {}),
|
|
51
|
+
...(raw.outputFormat ? { outputFormat: raw.outputFormat } : {}),
|
|
52
|
+
...(raw.streamProtocol ? { streamProtocol: raw.streamProtocol } : {}),
|
|
53
|
+
...(raw.timeoutMs ? { timeoutMs: raw.timeoutMs } : {}),
|
|
54
|
+
...(raw.tags ? { tags: raw.tags } : {}),
|
|
55
|
+
...(raw.persona ? { persona: raw.persona } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return { targets };
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const TRELLIS_VERSION: string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Read the package version relative to the compiled/source CLI entrypoint.
|
|
4
|
+
* `src/lib` and `dist/lib` are both exactly two levels below package.json,
|
|
5
|
+
* so the same URL works under tsx and from an installed package.
|
|
6
|
+
*/
|
|
7
|
+
function loadPackageVersion() {
|
|
8
|
+
const manifest = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
9
|
+
if (typeof manifest.version !== "string" || manifest.version.length === 0) {
|
|
10
|
+
throw new Error("agent-trellis package.json does not declare a version");
|
|
11
|
+
}
|
|
12
|
+
return manifest.version;
|
|
13
|
+
}
|
|
14
|
+
export const TRELLIS_VERSION = loadPackageVersion();
|
|
@@ -44,6 +44,9 @@ export declare function declaredEnvNames(def: McpServerDef): string[];
|
|
|
44
44
|
* different command, not this extractor's job.
|
|
45
45
|
*/
|
|
46
46
|
export declare function extractJsonEnvVarNames(content: string): string[];
|
|
47
|
+
/** ZCode stores the same server value shape under `mcp.servers` rather than
|
|
48
|
+
* the top-level `mcpServers` map used by Claude Code and Kiro. */
|
|
49
|
+
export declare function extractZcodeEnvVarNames(content: string): string[];
|
|
47
50
|
/** Codex's config is TOML. The shapes audit needs: every `env_vars = [...]`
|
|
48
51
|
* line's quoted string contents, plus a `bearer_token_env_var = "VAR"`
|
|
49
52
|
* line's bare name (a name directly, not a `${VAR}` template — Codex's
|
package/dist/lib/envVarNames.js
CHANGED
|
@@ -71,6 +71,25 @@ export function extractJsonEnvVarNames(content) {
|
|
|
71
71
|
}
|
|
72
72
|
return names;
|
|
73
73
|
}
|
|
74
|
+
/** ZCode stores the same server value shape under `mcp.servers` rather than
|
|
75
|
+
* the top-level `mcpServers` map used by Claude Code and Kiro. */
|
|
76
|
+
export function extractZcodeEnvVarNames(content) {
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(content);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
const names = [];
|
|
85
|
+
for (const server of Object.values(parsed.mcp?.servers ?? {})) {
|
|
86
|
+
for (const value of Object.values(server.env ?? {}))
|
|
87
|
+
names.push(...extractTemplateVarNames(value));
|
|
88
|
+
for (const value of Object.values(server.headers ?? {}))
|
|
89
|
+
names.push(...extractTemplateVarNames(value));
|
|
90
|
+
}
|
|
91
|
+
return names;
|
|
92
|
+
}
|
|
74
93
|
const ENV_VARS_LINE_RE = /^\s*env_vars\s*=\s*\[(.*)\]\s*$/;
|
|
75
94
|
const BEARER_TOKEN_ENV_VAR_LINE_RE = /^\s*bearer_token_env_var\s*=\s*"([^"]*)"\s*$/;
|
|
76
95
|
const QUOTED_STRING_RE = /"((?:[^"\\]|\\.)*)"/g;
|
|
@@ -23,6 +23,8 @@ export interface GatewayBackend {
|
|
|
23
23
|
listTools(): Promise<AggregatedTool[]>;
|
|
24
24
|
/** Invoke one aggregated tool; implementations route it to its owner. */
|
|
25
25
|
callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
26
|
+
/** Sanitized connection state for user/Agent remediation. */
|
|
27
|
+
listStatus?(): readonly UpstreamStatus[];
|
|
26
28
|
/** Release everything this backend holds. Must be safe to call twice. */
|
|
27
29
|
close(): Promise<void>;
|
|
28
30
|
}
|
|
@@ -32,6 +34,17 @@ export interface UpstreamSpec {
|
|
|
32
34
|
name: string;
|
|
33
35
|
def: McpServerDef;
|
|
34
36
|
}
|
|
37
|
+
export type UpstreamStatusKind = "ready" | "auth-required" | "unavailable" | "timeout" | "degraded";
|
|
38
|
+
export interface UpstreamStatus {
|
|
39
|
+
name: string;
|
|
40
|
+
transport: "stdio" | "http" | "sse";
|
|
41
|
+
status: UpstreamStatusKind;
|
|
42
|
+
detail?: string;
|
|
43
|
+
remediation?: string;
|
|
44
|
+
remediationKind?: "oauth" | "environment" | "install" | "endpoint" | "router";
|
|
45
|
+
requiresHuman?: boolean;
|
|
46
|
+
requiresSessionRestart?: boolean;
|
|
47
|
+
}
|
|
35
48
|
export interface LocalBackendOptions {
|
|
36
49
|
secretsPolicy: SecretsPolicy;
|
|
37
50
|
clientInfo: McpClientInfo;
|
|
@@ -64,12 +77,27 @@ export declare class LocalBackend implements GatewayBackend {
|
|
|
64
77
|
private readonly warn;
|
|
65
78
|
private readonly registry;
|
|
66
79
|
private readonly connected;
|
|
80
|
+
private readonly statuses;
|
|
67
81
|
private closed;
|
|
68
82
|
private constructor();
|
|
69
83
|
static connect(upstreams: readonly UpstreamSpec[], options: LocalBackendOptions): Promise<LocalBackend>;
|
|
70
84
|
private addUpstream;
|
|
71
85
|
listTools(): Promise<AggregatedTool[]>;
|
|
72
86
|
callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
87
|
+
listStatus(): readonly UpstreamStatus[];
|
|
73
88
|
close(): Promise<void>;
|
|
74
89
|
private closeUpstream;
|
|
75
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Attaches a stored OAuth token, refreshing it first if it has expired.
|
|
93
|
+
*
|
|
94
|
+
* Returns the definition unchanged when nothing is stored for this
|
|
95
|
+
* server: the overwhelmingly common case is a remote server that uses no
|
|
96
|
+
* OAuth at all, or one whose credential lives in `headers` already, and
|
|
97
|
+
* neither should be disturbed.
|
|
98
|
+
*
|
|
99
|
+
* An explicit `Authorization` header in canonical wins. Someone who wrote
|
|
100
|
+
* one meant it, and silently overriding it with a stored token would be
|
|
101
|
+
* the kind of invisible precedence that is impossible to debug.
|
|
102
|
+
*/
|
|
103
|
+
export declare function withOAuthHeader(name: string, def: McpServerDef, homeDir: string): Promise<McpServerDef>;
|
|
@@ -32,6 +32,7 @@ export class LocalBackend {
|
|
|
32
32
|
warn;
|
|
33
33
|
registry = new McpToolRegistry();
|
|
34
34
|
connected = new Set();
|
|
35
|
+
statuses = new Map();
|
|
35
36
|
closed = false;
|
|
36
37
|
constructor(warn) {
|
|
37
38
|
this.warn = warn;
|
|
@@ -43,6 +44,8 @@ export class LocalBackend {
|
|
|
43
44
|
return backend;
|
|
44
45
|
}
|
|
45
46
|
async addUpstream({ name, def }, options) {
|
|
47
|
+
const transport = def.transport === "http" || def.transport === "sse" ? def.transport : "stdio";
|
|
48
|
+
this.statuses.set(name, { name, transport, status: "unavailable", detail: "connecting" });
|
|
46
49
|
let effectiveDef = def;
|
|
47
50
|
if (options.homeDir && def.url) {
|
|
48
51
|
try {
|
|
@@ -53,6 +56,7 @@ export class LocalBackend {
|
|
|
53
56
|
// dropped — the same isolation a failed connection gets, never a
|
|
54
57
|
// browser and never a blocked startup.
|
|
55
58
|
this.warn(`trellis-mcp-gateway: skipping MCP server "${name}": ${errorText(err)}`);
|
|
59
|
+
this.statuses.set(name, failureStatus(name, def, err));
|
|
56
60
|
return;
|
|
57
61
|
}
|
|
58
62
|
}
|
|
@@ -62,6 +66,7 @@ export class LocalBackend {
|
|
|
62
66
|
}
|
|
63
67
|
catch (err) {
|
|
64
68
|
this.warn(`trellis-mcp-gateway: failed to connect to MCP server "${name}": ${errorText(err)}`);
|
|
69
|
+
this.statuses.set(name, failureStatus(name, def, err));
|
|
65
70
|
return;
|
|
66
71
|
}
|
|
67
72
|
this.connected.add(client);
|
|
@@ -73,12 +78,28 @@ export class LocalBackend {
|
|
|
73
78
|
catch (err) {
|
|
74
79
|
this.warn(`trellis-mcp-gateway: failed to list tools for MCP server "${name}": ${errorText(err)}`);
|
|
75
80
|
await this.closeUpstream(client);
|
|
81
|
+
this.statuses.set(name, failureStatus(name, def, err));
|
|
76
82
|
return;
|
|
77
83
|
}
|
|
78
84
|
const { skipped } = this.registry.add(name, client, tools);
|
|
79
85
|
for (const collided of skipped) {
|
|
80
86
|
this.warn(`trellis-mcp-gateway: tool "${collided}" from server "${name}" collides with an already-registered tool and was skipped`);
|
|
81
87
|
}
|
|
88
|
+
if (tools.length === 0 && name === "mcp-router") {
|
|
89
|
+
this.statuses.set(name, {
|
|
90
|
+
name,
|
|
91
|
+
transport,
|
|
92
|
+
status: "degraded",
|
|
93
|
+
detail: "router connected but exposed no downstream tools",
|
|
94
|
+
remediationKind: "router",
|
|
95
|
+
requiresHuman: true,
|
|
96
|
+
remediation: "check the MCP router's provider login/configuration and reconnect it, then restart the Agent session",
|
|
97
|
+
requiresSessionRestart: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
this.statuses.set(name, { name, transport, status: "ready", detail: `connected; ${tools.length} tool(s) available` });
|
|
102
|
+
}
|
|
82
103
|
}
|
|
83
104
|
async listTools() {
|
|
84
105
|
return this.registry.listTools();
|
|
@@ -86,6 +107,9 @@ export class LocalBackend {
|
|
|
86
107
|
async callTool(name, args) {
|
|
87
108
|
return this.registry.callTool(name, args);
|
|
88
109
|
}
|
|
110
|
+
listStatus() {
|
|
111
|
+
return [...this.statuses.values()];
|
|
112
|
+
}
|
|
89
113
|
async close() {
|
|
90
114
|
if (this.closed)
|
|
91
115
|
return;
|
|
@@ -105,6 +129,53 @@ export class LocalBackend {
|
|
|
105
129
|
}
|
|
106
130
|
}
|
|
107
131
|
}
|
|
132
|
+
function failureStatus(name, def, error) {
|
|
133
|
+
const detail = errorText(error).slice(0, 300);
|
|
134
|
+
const auth = def.url !== undefined && /401|403|unauthorized|forbidden|authentication|required|login/i.test(detail);
|
|
135
|
+
const timeout = /timed out|timeout/i.test(detail);
|
|
136
|
+
if (auth) {
|
|
137
|
+
return {
|
|
138
|
+
name,
|
|
139
|
+
transport: def.transport === "sse" ? "sse" : "http",
|
|
140
|
+
status: "auth-required",
|
|
141
|
+
detail,
|
|
142
|
+
remediationKind: "oauth",
|
|
143
|
+
requiresHuman: true,
|
|
144
|
+
requiresSessionRestart: true,
|
|
145
|
+
remediation: `run \`trellis mcp auth ${name}\` in a human terminal, then restart the Agent session`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (timeout) {
|
|
149
|
+
return {
|
|
150
|
+
name,
|
|
151
|
+
transport: def.transport === "sse" ? "sse" : def.url ? "http" : "stdio",
|
|
152
|
+
status: "timeout",
|
|
153
|
+
detail,
|
|
154
|
+
remediationKind: def.url ? "endpoint" : "environment",
|
|
155
|
+
requiresHuman: true,
|
|
156
|
+
requiresSessionRestart: true,
|
|
157
|
+
remediation: def.url
|
|
158
|
+
? `check the endpoint/network; if it requires OAuth, run \`trellis mcp auth ${name}\``
|
|
159
|
+
: `check that \`${def.command ?? "the MCP command"}\` is installed and responsive`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const envNames = [...(def.env ?? []), ...Object.keys(def.envAliases ?? {})];
|
|
163
|
+
const missingExecutable = /ENOENT|not found|no such file/i.test(detail);
|
|
164
|
+
return {
|
|
165
|
+
name,
|
|
166
|
+
transport: def.transport === "sse" ? "sse" : def.url ? "http" : "stdio",
|
|
167
|
+
status: "unavailable",
|
|
168
|
+
detail,
|
|
169
|
+
remediationKind: def.url ? "endpoint" : (missingExecutable ? "install" : "environment"),
|
|
170
|
+
requiresHuman: true,
|
|
171
|
+
requiresSessionRestart: true,
|
|
172
|
+
remediation: def.url
|
|
173
|
+
? `check the MCP endpoint and credentials; run \`trellis mcp auth ${name}\` if it uses OAuth`
|
|
174
|
+
: envNames.length > 0
|
|
175
|
+
? `check the configured environment variables (${envNames.join(", ")}) and rerun \`trellis mcp sync\``
|
|
176
|
+
: `check that \`${def.command ?? "the MCP command"}\` is installed and responsive`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
108
179
|
/**
|
|
109
180
|
* Attaches a stored OAuth token, refreshing it first if it has expired.
|
|
110
181
|
*
|
|
@@ -117,7 +188,7 @@ export class LocalBackend {
|
|
|
117
188
|
* one meant it, and silently overriding it with a stored token would be
|
|
118
189
|
* the kind of invisible precedence that is impossible to debug.
|
|
119
190
|
*/
|
|
120
|
-
async function withOAuthHeader(name, def, homeDir) {
|
|
191
|
+
export async function withOAuthHeader(name, def, homeDir) {
|
|
121
192
|
const hasExplicitAuth = Object.keys(def.headers ?? {}).some((key) => key.toLowerCase() === "authorization");
|
|
122
193
|
if (hasExplicitAuth)
|
|
123
194
|
return def;
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* Install-then-manage (trellis-managed-agents design.md D5): selecting a
|
|
3
3
|
* not-yet-present agent into the managed set is itself the authorization
|
|
4
4
|
* to install it, but never silently — one confirmation, then a real
|
|
5
|
-
* `npm install -g <package>` child process. Kiro
|
|
6
|
-
*
|
|
5
|
+
* `npm install -g <package>` child process. Kiro and Kimi Code use official
|
|
6
|
+
* install scripts rather than an npm package and are refused before this
|
|
7
|
+
* module is ever reached.
|
|
7
8
|
*/
|
|
8
9
|
import type { AgentId } from "../core/types.js";
|
|
9
10
|
/** Only agents with a real `npm install -g <pkg>` command — Kiro's own
|
|
10
11
|
* `INSTALL_HINTS` entry is a download URL, not a package, and is never
|
|
11
12
|
* looked up here. */
|
|
12
|
-
export declare const NPM_INSTALLABLE: Record<Exclude<AgentId, "kiro">, string>;
|
|
13
|
+
export declare const NPM_INSTALLABLE: Record<Exclude<AgentId, "kiro" | "kimi-code" | "zcode">, string>;
|
|
13
14
|
export interface ConfirmAndInstallOptions {
|
|
14
15
|
/** Test/real seam, same pattern as onboard's `promptForAgent` — a real
|
|
15
16
|
* terminal confirmation by default. */
|
|
@@ -19,8 +20,9 @@ export interface ConfirmAndInstallOptions {
|
|
|
19
20
|
}
|
|
20
21
|
export interface ConfirmAndInstallResult {
|
|
21
22
|
installed: boolean;
|
|
22
|
-
/** False
|
|
23
|
-
* caller refuses
|
|
23
|
+
/** False when the agent has no supported npm installation path — the
|
|
24
|
+
* caller refuses it with its official installation hint instead of
|
|
25
|
+
* prompting. */
|
|
24
26
|
installable: boolean;
|
|
25
27
|
}
|
|
26
28
|
export declare function confirmAndInstall(agent: AgentId, opts?: ConfirmAndInstallOptions): Promise<ConfirmAndInstallResult>;
|
package/dist/lib/installAgent.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Install-then-manage (trellis-managed-agents design.md D5): selecting a
|
|
3
3
|
* not-yet-present agent into the managed set is itself the authorization
|
|
4
4
|
* to install it, but never silently — one confirmation, then a real
|
|
5
|
-
* `npm install -g <package>` child process. Kiro
|
|
6
|
-
*
|
|
5
|
+
* `npm install -g <package>` child process. Kiro and Kimi Code use official
|
|
6
|
+
* install scripts rather than an npm package and are refused before this
|
|
7
|
+
* module is ever reached.
|
|
7
8
|
*/
|
|
8
9
|
import { execFileSync } from "node:child_process";
|
|
9
10
|
import { createInterface } from "node:readline/promises";
|
|
@@ -31,7 +32,7 @@ function runInstallReal(pkg) {
|
|
|
31
32
|
execFileSync("npm", ["install", "-g", pkg], { stdio: "inherit" });
|
|
32
33
|
}
|
|
33
34
|
export async function confirmAndInstall(agent, opts = {}) {
|
|
34
|
-
if (agent === "kiro") {
|
|
35
|
+
if (agent === "kiro" || agent === "kimi-code" || agent === "zcode") {
|
|
35
36
|
return { installed: false, installable: false };
|
|
36
37
|
}
|
|
37
38
|
const pkg = NPM_INSTALLABLE[agent];
|
package/dist/lib/mcpConnect.js
CHANGED
|
@@ -19,6 +19,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
|
19
19
|
import { resolveSecretEnv } from "./secretEnv.js";
|
|
20
20
|
import { extractTemplateVarNames } from "./envVarNames.js";
|
|
21
21
|
export const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
|
|
22
|
+
const FAILED_TRANSPORT_CLEANUP_TIMEOUT_MS = 1_000;
|
|
22
23
|
/**
|
|
23
24
|
* A hanging server (process alive, protocol response never sent) leaves
|
|
24
25
|
* `promise` permanently unsettled — indistinguishable from "still
|
|
@@ -55,7 +56,10 @@ export async function connectWithCleanup(client, transport, timeoutMs, message)
|
|
|
55
56
|
}
|
|
56
57
|
catch (err) {
|
|
57
58
|
try {
|
|
58
|
-
|
|
59
|
+
// A failed HTTP transport can itself hang while closing. Cleanup is
|
|
60
|
+
// best-effort and must not turn one bad upstream into a Gateway-wide
|
|
61
|
+
// startup timeout; preserve the original connect error below.
|
|
62
|
+
await withTimeout(Promise.resolve().then(() => transport.close()), FAILED_TRANSPORT_CLEANUP_TIMEOUT_MS, "transport cleanup timed out");
|
|
59
63
|
}
|
|
60
64
|
catch {
|
|
61
65
|
// best-effort; the original connect failure is what matters
|
|
@@ -37,6 +37,8 @@ export interface McpMigrateReadResult {
|
|
|
37
37
|
export declare function resolvedEnvTextMap(def: Pick<McpServerDef, "env" | "envAliases" | "staticEnv">): Record<string, string>;
|
|
38
38
|
export declare function readClaudeCodeMcpDefs(homeDir: string): McpMigrateReadResult;
|
|
39
39
|
export declare function readKiroMcpDefs(homeDir: string): McpMigrateReadResult;
|
|
40
|
+
export declare function readKimiCodeMcpDefs(homeDir: string): McpMigrateReadResult;
|
|
41
|
+
export declare function readZcodeMcpDefs(homeDir: string): McpMigrateReadResult;
|
|
40
42
|
export interface CodexMcpEntryRich {
|
|
41
43
|
name: string;
|
|
42
44
|
enabled: boolean;
|
|
@@ -15,6 +15,8 @@ import { readFileSync } from "node:fs";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { readJsonFile } from "./probeCommon.js";
|
|
17
17
|
import { readServerEnvTable } from "./tomlSection.js";
|
|
18
|
+
import { resolveZcodeProfile } from "../probes/zcode.js";
|
|
19
|
+
import { codexEnv } from "../probes/codex.js";
|
|
18
20
|
const EMPTY_RESULT = { entries: [], unsupported: [] };
|
|
19
21
|
/**
|
|
20
22
|
* Trellis's own `env` (self-referencing name) vs. `envAliases`
|
|
@@ -108,6 +110,25 @@ export function readClaudeCodeMcpDefs(homeDir) {
|
|
|
108
110
|
export function readKiroMcpDefs(homeDir) {
|
|
109
111
|
return readRichJsonMcpDefs(join(homeDir, ".kiro", "settings", "mcp.json"));
|
|
110
112
|
}
|
|
113
|
+
export function readKimiCodeMcpDefs(homeDir) {
|
|
114
|
+
return readRichJsonMcpDefs(join(homeDir, ".kimi-code", "mcp.json"));
|
|
115
|
+
}
|
|
116
|
+
export function readZcodeMcpDefs(homeDir) {
|
|
117
|
+
const profile = resolveZcodeProfile(homeDir);
|
|
118
|
+
if (!profile)
|
|
119
|
+
return EMPTY_RESULT;
|
|
120
|
+
const parsed = readJsonFile(profile.configPath);
|
|
121
|
+
const entries = [];
|
|
122
|
+
const unsupported = [];
|
|
123
|
+
for (const [name, raw] of Object.entries(parsed?.mcp?.servers ?? {})) {
|
|
124
|
+
const def = fromRichJsonServerDef(raw);
|
|
125
|
+
if (def)
|
|
126
|
+
entries.push({ name, def });
|
|
127
|
+
else
|
|
128
|
+
unsupported.push({ name, reason: "stdio entry has no command — malformed, skipped" });
|
|
129
|
+
}
|
|
130
|
+
return { entries, unsupported };
|
|
131
|
+
}
|
|
111
132
|
/**
|
|
112
133
|
* Pure conversion half — separated from the subprocess/file-read effects
|
|
113
134
|
* below so the stdio/non-stdio/malformed decision logic is unit-testable
|
|
@@ -199,7 +220,7 @@ export function readCodexMcpDefs(homeDir) {
|
|
|
199
220
|
// `[]`, not the real machine's servers), so this must be scoped to
|
|
200
221
|
// `homeDir` explicitly — `src/probes/codex.ts`'s own equivalent call
|
|
201
222
|
// has this same gap, unaddressed there; not touched by this change.
|
|
202
|
-
raw = execFileSync("codex", ["mcp", "list", "--json"], { encoding: "utf-8", timeout: 5_000, env:
|
|
223
|
+
raw = execFileSync("codex", ["mcp", "list", "--json"], { encoding: "utf-8", timeout: 5_000, env: codexEnv(homeDir) });
|
|
203
224
|
}
|
|
204
225
|
catch {
|
|
205
226
|
return EMPTY_RESULT;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import type { CallToolResult, GetPromptResult, Implementation, Prompt, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { AgentId } from "../core/types.js";
|
|
4
|
+
import type { GatewayBackend } from "./gatewayBackend.js";
|
|
5
|
+
export interface RuntimeContext {
|
|
6
|
+
agentId: AgentId;
|
|
7
|
+
homeDir: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TrellisProvider {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
listTools?(context: RuntimeContext): Promise<Tool[]> | Tool[];
|
|
12
|
+
callTool(name: string, args: unknown, context: RuntimeContext): Promise<CallToolResult>;
|
|
13
|
+
listResources?(context: RuntimeContext): Promise<Resource[]> | Resource[];
|
|
14
|
+
readResource?(uri: URL, context: RuntimeContext): Promise<ReadResourceResult> | ReadResourceResult;
|
|
15
|
+
listPrompts?(context: RuntimeContext): Promise<Prompt[]> | Prompt[];
|
|
16
|
+
getPrompt?(name: string, args: Record<string, string> | undefined, context: RuntimeContext): Promise<GetPromptResult> | GetPromptResult;
|
|
17
|
+
close?(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare class BuiltinRegistry {
|
|
20
|
+
private readonly providers;
|
|
21
|
+
private readonly warn;
|
|
22
|
+
private readonly toolOwners;
|
|
23
|
+
private readonly originalToolOwners;
|
|
24
|
+
private readonly exposedToOriginal;
|
|
25
|
+
private readonly resourceOwners;
|
|
26
|
+
private readonly promptOwners;
|
|
27
|
+
constructor(providers: readonly TrellisProvider[], warn?: (message: string) => void);
|
|
28
|
+
listTools(context: RuntimeContext): Promise<Tool[]>;
|
|
29
|
+
callTool(name: string, args: unknown, context: RuntimeContext): Promise<CallToolResult>;
|
|
30
|
+
listResources(context: RuntimeContext): Promise<Resource[]>;
|
|
31
|
+
readResource(uri: URL, context: RuntimeContext): Promise<ReadResourceResult>;
|
|
32
|
+
listPrompts(context: RuntimeContext): Promise<Prompt[]>;
|
|
33
|
+
getPrompt(name: string, args: Record<string, string> | undefined, context: RuntimeContext): Promise<GetPromptResult>;
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** Adapts the existing gateway tool backend without duplicating its routing
|
|
37
|
+
* or upstream connection logic. */
|
|
38
|
+
export declare class UpstreamProvider implements TrellisProvider {
|
|
39
|
+
private readonly backend;
|
|
40
|
+
readonly id = "upstream";
|
|
41
|
+
constructor(backend: GatewayBackend);
|
|
42
|
+
listTools(): Promise<Tool[]>;
|
|
43
|
+
callTool(name: string, args: unknown): Promise<CallToolResult>;
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export declare function createRuntimeServer(serverInfo: Implementation, context: RuntimeContext, registry: BuiltinRegistry): Server;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { allocateExposedToolNames } from "./mcpToolRegistry.js";
|
|
4
|
+
function compactBuiltinToolName(name) {
|
|
5
|
+
return name.startsWith("trellis.") ? name.slice("trellis.".length).replaceAll(".", "_") : name;
|
|
6
|
+
}
|
|
7
|
+
function providerError(provider, error) {
|
|
8
|
+
return `trellis-mcp-runtime: provider "${provider}" failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
9
|
+
}
|
|
10
|
+
export class BuiltinRegistry {
|
|
11
|
+
providers;
|
|
12
|
+
warn;
|
|
13
|
+
toolOwners = new Map();
|
|
14
|
+
originalToolOwners = new Map();
|
|
15
|
+
exposedToOriginal = new Map();
|
|
16
|
+
resourceOwners = new Map();
|
|
17
|
+
promptOwners = new Map();
|
|
18
|
+
constructor(providers, warn = console.error) {
|
|
19
|
+
this.providers = providers;
|
|
20
|
+
this.warn = warn;
|
|
21
|
+
}
|
|
22
|
+
async listTools(context) {
|
|
23
|
+
const candidates = [];
|
|
24
|
+
this.toolOwners.clear();
|
|
25
|
+
this.originalToolOwners.clear();
|
|
26
|
+
this.exposedToOriginal.clear();
|
|
27
|
+
for (const provider of this.providers) {
|
|
28
|
+
if (!provider.listTools)
|
|
29
|
+
continue;
|
|
30
|
+
try {
|
|
31
|
+
for (const tool of await provider.listTools(context)) {
|
|
32
|
+
candidates.push({ provider, tool });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
this.warn(providerError(provider.id, err));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const exposedNames = allocateExposedToolNames(candidates.map(({ provider, tool }) => ({
|
|
40
|
+
serverName: provider.id,
|
|
41
|
+
toolName: provider.id === "upstream" ? tool.name : compactBuiltinToolName(tool.name),
|
|
42
|
+
})));
|
|
43
|
+
return candidates.map(({ provider, tool }, index) => {
|
|
44
|
+
const exposedName = exposedNames[index];
|
|
45
|
+
this.toolOwners.set(exposedName, provider);
|
|
46
|
+
this.exposedToOriginal.set(exposedName, tool.name);
|
|
47
|
+
if (!this.originalToolOwners.has(tool.name))
|
|
48
|
+
this.originalToolOwners.set(tool.name, provider);
|
|
49
|
+
const title = exposedName === tool.name || tool.title !== undefined
|
|
50
|
+
? tool.title
|
|
51
|
+
: `${provider.id} / ${tool.name}`;
|
|
52
|
+
return { ...tool, name: exposedName, ...(title === undefined ? {} : { title }) };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async callTool(name, args, context) {
|
|
56
|
+
let provider = this.toolOwners.get(name);
|
|
57
|
+
let originalName = this.exposedToOriginal.get(name) ?? name;
|
|
58
|
+
if (!provider) {
|
|
59
|
+
await this.listTools(context);
|
|
60
|
+
provider = this.toolOwners.get(name);
|
|
61
|
+
originalName = this.exposedToOriginal.get(name) ?? name;
|
|
62
|
+
// Keep direct programmatic calls using the logical pre-normalization
|
|
63
|
+
// name working, while never advertising an invalid alias to an Agent.
|
|
64
|
+
if (!provider)
|
|
65
|
+
provider = this.originalToolOwners.get(name);
|
|
66
|
+
}
|
|
67
|
+
if (!provider)
|
|
68
|
+
return { content: [{ type: "text", text: `unknown Trellis tool "${name}"` }], isError: true };
|
|
69
|
+
try {
|
|
70
|
+
return await provider.callTool(originalName, args, context);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return { content: [{ type: "text", text: providerError(provider.id, err) }], isError: true };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async listResources(context) {
|
|
77
|
+
const resources = [];
|
|
78
|
+
this.resourceOwners.clear();
|
|
79
|
+
for (const provider of this.providers) {
|
|
80
|
+
if (!provider.listResources)
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
for (const resource of await provider.listResources(context)) {
|
|
84
|
+
if (this.resourceOwners.has(resource.uri)) {
|
|
85
|
+
this.warn(`trellis-mcp-runtime: duplicate resource "${resource.uri}" from provider "${provider.id}"`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
this.resourceOwners.set(resource.uri, provider);
|
|
89
|
+
resources.push(resource);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
this.warn(providerError(provider.id, err));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return resources;
|
|
97
|
+
}
|
|
98
|
+
async readResource(uri, context) {
|
|
99
|
+
const key = uri.toString();
|
|
100
|
+
let provider = this.resourceOwners.get(key);
|
|
101
|
+
if (!provider) {
|
|
102
|
+
await this.listResources(context);
|
|
103
|
+
provider = this.resourceOwners.get(key);
|
|
104
|
+
}
|
|
105
|
+
if (!provider?.readResource)
|
|
106
|
+
throw new Error(`unknown Trellis resource "${key}"`);
|
|
107
|
+
return provider.readResource(uri, context);
|
|
108
|
+
}
|
|
109
|
+
async listPrompts(context) {
|
|
110
|
+
const prompts = [];
|
|
111
|
+
this.promptOwners.clear();
|
|
112
|
+
for (const provider of this.providers) {
|
|
113
|
+
if (!provider.listPrompts)
|
|
114
|
+
continue;
|
|
115
|
+
try {
|
|
116
|
+
for (const prompt of await provider.listPrompts(context)) {
|
|
117
|
+
if (this.promptOwners.has(prompt.name)) {
|
|
118
|
+
this.warn(`trellis-mcp-runtime: duplicate prompt "${prompt.name}" from provider "${provider.id}"`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
this.promptOwners.set(prompt.name, provider);
|
|
122
|
+
prompts.push(prompt);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
this.warn(providerError(provider.id, err));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return prompts;
|
|
130
|
+
}
|
|
131
|
+
async getPrompt(name, args, context) {
|
|
132
|
+
let provider = this.promptOwners.get(name);
|
|
133
|
+
if (!provider) {
|
|
134
|
+
await this.listPrompts(context);
|
|
135
|
+
provider = this.promptOwners.get(name);
|
|
136
|
+
}
|
|
137
|
+
if (!provider?.getPrompt)
|
|
138
|
+
throw new Error(`unknown Trellis prompt "${name}"`);
|
|
139
|
+
return provider.getPrompt(name, args, context);
|
|
140
|
+
}
|
|
141
|
+
async close() {
|
|
142
|
+
await Promise.all(this.providers.map(async (provider) => {
|
|
143
|
+
try {
|
|
144
|
+
await provider.close?.();
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
this.warn(providerError(provider.id, err));
|
|
148
|
+
}
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Adapts the existing gateway tool backend without duplicating its routing
|
|
153
|
+
* or upstream connection logic. */
|
|
154
|
+
export class UpstreamProvider {
|
|
155
|
+
backend;
|
|
156
|
+
id = "upstream";
|
|
157
|
+
constructor(backend) {
|
|
158
|
+
this.backend = backend;
|
|
159
|
+
}
|
|
160
|
+
async listTools() {
|
|
161
|
+
return (await this.backend.listTools());
|
|
162
|
+
}
|
|
163
|
+
async callTool(name, args) {
|
|
164
|
+
return (await this.backend.callTool(name, args));
|
|
165
|
+
}
|
|
166
|
+
async close() {
|
|
167
|
+
await this.backend.close();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function createRuntimeServer(serverInfo, context, registry) {
|
|
171
|
+
const server = new Server(serverInfo, {
|
|
172
|
+
capabilities: {
|
|
173
|
+
tools: { listChanged: true },
|
|
174
|
+
resources: { listChanged: true },
|
|
175
|
+
prompts: { listChanged: true },
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: await registry.listTools(context) }));
|
|
179
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => registry.callTool(request.params.name, request.params.arguments, context));
|
|
180
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: await registry.listResources(context) }));
|
|
181
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => registry.readResource(new URL(request.params.uri), context));
|
|
182
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: await registry.listPrompts(context) }));
|
|
183
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => registry.getPrompt(request.params.name, request.params.arguments, context));
|
|
184
|
+
return server;
|
|
185
|
+
}
|