Skip to content

Commit 9def375

Browse files
committed
feat: MCP client — kode connects to external MCP servers
kode can now connect to any MCP server over stdio and expose its tools to the agent. Configured via mcp_servers in kode.json, same format as Claude Code's mcpServers. New package: internal/mcpclient - Client — spawns MCP server subprocess, JSON-RPC 2.0 over pipes - Discover() — initialize + tools/list handshake - CallTool() — tools/call with text result extraction - ToolAdapter — wraps MCP tool as kode.Tool (Name, Description, Schema, Call) - Tool names prefixed as <server>__<tool> to avoid collisions - 17 tests: discovery, calling, errors, timeouts, close, env config Config: - New mcp_servers map in FileConfig and ResolvedConfig - Supports command, args, and env overrides (same format as Claude Code ~/.claude/settings.json) Wired into all commands: - kode run, kode repl, kode continue, kode serve, kode mcp, kode subagent - Cleanup via defer on exit Docs: - Rewrote docs/MCP.md covering both server and client directions - Configuration examples: Playwright, Fetch, GitHub MCP servers - Tool naming, lifecycle, logging, config reference 15/15 packages, race clean.
1 parent 2c556e4 commit 9def375

11 files changed

Lines changed: 1182 additions & 35 deletions

File tree

cmd/kode/main.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/BackendStack21/kode/internal/config"
1919
"github.com/BackendStack21/kode/internal/danger"
2020
"github.com/BackendStack21/kode/internal/llm"
21+
"github.com/BackendStack21/kode/internal/mcpclient"
2122
"github.com/BackendStack21/kode/internal/render"
2223
"github.com/BackendStack21/kode/internal/session"
2324
"github.com/BackendStack21/kode/internal/skills"
@@ -717,6 +718,17 @@ func run(args []string) error {
717718
var sandboxCleanup func() error
718719
tools := builtinTools(resolved.Dangerous, sm, nil)
719720

721+
// MCP server tools
722+
var mcpCleanup func()
723+
if len(resolved.MCPServers) > 0 {
724+
cl, err := loadMCPTools(resolved.MCPServers, &tools)
725+
if err != nil {
726+
return fmt.Errorf("mcp: %w", err)
727+
}
728+
mcpCleanup = cl
729+
defer mcpCleanup()
730+
}
731+
720732
if resolved.Sandbox {
721733
cleanup, err := setupSandbox(tools, sbCfg)
722734
if err != nil {
@@ -1010,6 +1022,53 @@ func builtinTools(dc danger.DangerousConfig, sm *skills.SkillManager, approver d
10101022
return tools
10111023
}
10121024

1025+
// loadMCPTools connects to configured MCP servers and appends their tools
1026+
// to the tool slice. Returns a cleanup function that closes all connections.
1027+
// The passed-in tool slice pointer is extended with ToolAdapters.
1028+
func loadMCPTools(servers map[string]mcpclient.ServerConfig, tools *[]kode.Tool) (func(), error) {
1029+
var cleaners []func()
1030+
for name, cfg := range servers {
1031+
client, err := mcpclient.New(name, cfg)
1032+
if err != nil {
1033+
// Clean up any servers we already started
1034+
for _, c := range cleaners {
1035+
c()
1036+
}
1037+
return nil, fmt.Errorf("mcp server %q: %w", name, err)
1038+
}
1039+
1040+
defs, err := client.Discover(context.Background())
1041+
if err != nil {
1042+
client.Close()
1043+
for _, c := range cleaners {
1044+
c()
1045+
}
1046+
return nil, fmt.Errorf("mcp server %q: discover: %w", name, err)
1047+
}
1048+
1049+
for _, def := range defs {
1050+
*tools = append(*tools, &mcpclient.ToolAdapter{
1051+
Client: client,
1052+
ToolName: def.Name,
1053+
Desc: def.Description,
1054+
ParamSchema: def.InputSchema,
1055+
})
1056+
}
1057+
1058+
cleaners = append(cleaners, func() {
1059+
if err := client.Close(); err != nil {
1060+
fmt.Fprintf(os.Stderr, "kode: warning: mcp client %q close: %v\n", name, err)
1061+
}
1062+
})
1063+
}
1064+
1065+
return func() {
1066+
for _, c := range cleaners {
1067+
c()
1068+
}
1069+
}, nil
1070+
}
1071+
10131072
// getVersion returns the version string. Resolution order:
10141073
// 1. ldflags override (-X main.version=v0.2.1)
10151074
// 2. VCS tag from debug.ReadBuildInfo (when built with go install)
@@ -1341,6 +1400,17 @@ func continueCmd(args []string) error {
13411400
tools := builtinTools(resolved.Dangerous, sm, nil)
13421401
var sandboxCleanup func() error
13431402

1403+
// MCP server tools
1404+
var mcpCleanup func()
1405+
if len(resolved.MCPServers) > 0 {
1406+
cl, err := loadMCPTools(resolved.MCPServers, &tools)
1407+
if err != nil {
1408+
return fmt.Errorf("mcp: %w", err)
1409+
}
1410+
mcpCleanup = cl
1411+
defer mcpCleanup()
1412+
}
1413+
13441414
systemMessage := resolved.System
13451415
if systemMessage == "" {
13461416
systemMessage = defaultSystem

cmd/kode/mcp.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ Flags:
8181
// Build tools
8282
toolSet := builtinTools(resolved.Dangerous, sm, nil)
8383

84+
// MCP server tools — connect and discover before sandbox
85+
var mcpCleanup func()
86+
if len(resolved.MCPServers) > 0 {
87+
cl, err := loadMCPTools(resolved.MCPServers, &toolSet)
88+
if err != nil {
89+
return fmt.Errorf("mcp: %w", err)
90+
}
91+
mcpCleanup = cl
92+
defer mcpCleanup()
93+
}
94+
8495
// Sandbox setup (must happen after tools are created)
8596
var sandboxCleanup func() error
8697
if resolved.Sandbox {

cmd/kode/repl.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ func replCmd(args []string) error {
7878
tools := builtinTools(resolved.Dangerous, sm, nil)
7979
var sandboxCleanup func() error
8080

81+
// MCP server tools
82+
var mcpCleanup func()
83+
if len(resolved.MCPServers) > 0 {
84+
cl, err := loadMCPTools(resolved.MCPServers, &tools)
85+
if err != nil {
86+
return fmt.Errorf("mcp: %w", err)
87+
}
88+
mcpCleanup = cl
89+
defer mcpCleanup()
90+
}
91+
8192
if resolved.Sandbox {
8293
sbCfg := sandboxConfig{
8394
Image: resolved.SandboxImage,

cmd/kode/serve.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ Flags:
9898

9999
// ── Agent Builder ──────────────────────────────────────────────────────
100100

101-
func newServeAgent(resolved config.ResolvedConfig, system string, sendFn func(v any) error) (*kode.Agent, func() error, *wsApprover, error) {
101+
func newServeAgent(resolved config.ResolvedConfig, system string, sendFn func(v any) error) (*kode.Agent, func() error, func(), *wsApprover, error) {
102102
var sm *skills.SkillManager
103103
if resolved.Skills.Learn {
104104
sm = skills.NewSkillManager(
@@ -114,6 +114,16 @@ func newServeAgent(resolved config.ResolvedConfig, system string, sendFn func(v
114114
tools := builtinTools(resolved.Dangerous, sm, approver)
115115
var sandboxCleanup func() error
116116

117+
// MCP server tools
118+
var mcpCleanup func()
119+
if len(resolved.MCPServers) > 0 {
120+
cl, err := loadMCPTools(resolved.MCPServers, &tools)
121+
if err != nil {
122+
return nil, nil, nil, nil, fmt.Errorf("mcp: %w", err)
123+
}
124+
mcpCleanup = cl
125+
}
126+
117127
if resolved.Sandbox {
118128
cfg := sandboxConfig{
119129
Image: resolved.SandboxImage,
@@ -127,7 +137,7 @@ func newServeAgent(resolved config.ResolvedConfig, system string, sendFn func(v
127137
}
128138
cleanup, err := setupSandbox(tools, cfg)
129139
if err != nil {
130-
return nil, nil, nil, fmt.Errorf("sandbox: %w", err)
140+
return nil, nil, nil, nil, fmt.Errorf("sandbox: %w", err)
131141
}
132142
sandboxCleanup = cleanup
133143
}
@@ -148,10 +158,10 @@ func newServeAgent(resolved config.ResolvedConfig, system string, sendFn func(v
148158
MemoryConfig: resolved.Memory,
149159
})
150160
if err != nil {
151-
return nil, nil, nil, err
161+
return nil, nil, nil, nil, err
152162
}
153163

154-
return agent, sandboxCleanup, approver, nil
164+
return agent, sandboxCleanup, mcpCleanup, approver, nil
155165
}
156166

157167
// ── WebSocket Types ────────────────────────────────────────────────────
@@ -175,7 +185,7 @@ func handleWebSocket(store *session.Store, resources *resource.Registry, resolve
175185

176186
// Create ONE agent per WebSocket connection — provides buffer
177187
// continuity across turns within the same session.
178-
agent, sandboxCleanup, approver, err := newServeAgent(resolved, system, func(v any) error {
188+
agent, sandboxCleanup, mcpCleanup, approver, err := newServeAgent(resolved, system, func(v any) error {
179189
writeWSJSON(conn, v)
180190
return nil
181191
})
@@ -187,6 +197,9 @@ func handleWebSocket(store *session.Store, resources *resource.Registry, resolve
187197
if sandboxCleanup != nil {
188198
defer sandboxCleanup()
189199
}
200+
if mcpCleanup != nil {
201+
defer mcpCleanup()
202+
}
190203

191204
ctx, cancel := signal.NotifyContext(r.Context(), os.Interrupt)
192205
defer cancel()

cmd/kode/subagent.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,17 @@ func subagentCmd(args []string) error {
288288
tools := builtinTools(resolved.Dangerous, sm, nil)
289289
var sandboxCleanup func() error
290290

291+
// MCP server tools
292+
var mcpCleanup func()
293+
if len(resolved.MCPServers) > 0 {
294+
cl, err := loadMCPTools(resolved.MCPServers, &tools)
295+
if err != nil {
296+
return fmt.Errorf("mcp: %w", err)
297+
}
298+
mcpCleanup = cl
299+
defer mcpCleanup()
300+
}
301+
291302
if resolved.Sandbox {
292303
sbCfg := sandboxConfig{
293304
Image: resolved.SandboxImage,

0 commit comments

Comments
 (0)