Simplify MCP loading and test setup without behavior changes

This commit is contained in:
Spark 2026-02-13 00:05:40 +05:30
parent 431039a9dd
commit 7ea70da672
3 changed files with 159 additions and 179 deletions

View file

@ -38,42 +38,50 @@ func LoadMCPTools(ctx context.Context, cfg config.MCPToolsConfig, workspace stri
errs := make([]error, 0) errs := make([]error, 0)
for _, serverCfg := range cfg.Servers { for _, serverCfg := range cfg.Servers {
serverTools, err := loadMCPServerTools(ctx, serverCfg, workspace, usedNames)
loaded = append(loaded, serverTools...)
if err != nil {
errs = append(errs, err)
}
}
return loaded, errors.Join(errs...)
}
func loadMCPServerTools(ctx context.Context, serverCfg config.MCPServerConfig, workspace string, usedNames map[string]int) ([]Tool, error) {
if !serverCfg.Enabled { if !serverCfg.Enabled {
continue return nil, nil
} }
client := newMCPClient(serverCfg, workspace) client := newMCPClient(serverCfg, workspace)
startupTimeout := durationFromMS(serverCfg.StartupTimeoutMS, defaultMCPStartupTimeout) startupTimeout := durationFromMS(serverCfg.StartupTimeoutMS, defaultMCPStartupTimeout)
connectCtx, cancel := context.WithTimeout(ctx, startupTimeout) connectCtx, cancel := context.WithTimeout(ctx, startupTimeout)
defer cancel()
remoteTools, err := client.ListTools(connectCtx) remoteTools, err := client.ListTools(connectCtx)
cancel()
if err != nil { if err != nil {
errs = append(errs, fmt.Errorf("mcp server %q discovery failed: %w", serverCfg.Name, err)) return nil, fmt.Errorf("mcp server %q discovery failed: %w", serverCfg.Name, err)
continue
} }
callTimeout := durationFromMS(serverCfg.CallTimeoutMS, defaultMCPCallTimeout)
loaded := make([]Tool, 0, len(remoteTools))
for _, rt := range remoteTools { for _, rt := range remoteTools {
if rt == nil || strings.TrimSpace(rt.Name) == "" { if rt == nil || strings.TrimSpace(rt.Name) == "" {
continue continue
} }
localName := buildLocalToolName(serverCfg, rt.Name, usedNames)
description := buildMCPToolDescription(serverCfg.Name, rt.Name, rt.Description)
parameters := normalizeMCPInputSchema(rt.InputSchema)
loaded = append(loaded, &MCPTool{ loaded = append(loaded, &MCPTool{
localName: localName, localName: buildLocalToolName(serverCfg, rt.Name, usedNames),
remoteName: rt.Name, remoteName: rt.Name,
description: description, description: buildMCPToolDescription(serverCfg.Name, rt.Name, rt.Description),
parameters: parameters, parameters: normalizeMCPInputSchema(rt.InputSchema),
callTimeout: durationFromMS(serverCfg.CallTimeoutMS, defaultMCPCallTimeout), callTimeout: callTimeout,
client: client, client: client,
}) })
} }
}
return loaded, errors.Join(errs...) return loaded, nil
} }
type MCPTool struct { type MCPTool struct {
@ -193,16 +201,38 @@ func (c *mcpClient) buildTransport() (mcp.Transport, error) {
switch transport { switch transport {
case "command": case "command":
return c.buildCommandTransport()
case "streamable_http":
endpoint, err := c.requiredServerURL("streamable_http")
if err != nil {
return nil, err
}
return &mcp.StreamableClientTransport{
Endpoint: endpoint,
}, nil
case "sse":
endpoint, err := c.requiredServerURL("sse")
if err != nil {
return nil, err
}
return &mcp.SSEClientTransport{
Endpoint: endpoint,
}, nil
default:
return nil, fmt.Errorf("mcp server %q: unsupported transport %q", c.cfg.Name, c.cfg.Transport)
}
}
func (c *mcpClient) buildCommandTransport() (mcp.Transport, error) {
command := strings.TrimSpace(c.cfg.Command) command := strings.TrimSpace(c.cfg.Command)
if command == "" { if command == "" {
return nil, fmt.Errorf("mcp server %q: command is required for command transport", c.cfg.Name) return nil, fmt.Errorf("mcp server %q: command is required for command transport", c.cfg.Name)
} }
cmd := exec.Command(command, c.cfg.Args...)
cmd := exec.Command(command, c.cfg.Args...)
if wd := resolvePath(c.cfg.WorkingDir, c.workspace); wd != "" { if wd := resolvePath(c.cfg.WorkingDir, c.workspace); wd != "" {
cmd.Dir = wd cmd.Dir = wd
} }
if len(c.cfg.Env) > 0 { if len(c.cfg.Env) > 0 {
cmd.Env = mergeEnv(os.Environ(), c.cfg.Env) cmd.Env = mergeEnv(os.Environ(), c.cfg.Env)
} }
@ -213,23 +243,14 @@ func (c *mcpClient) buildTransport() (mcp.Transport, error) {
} }
tr.TerminateDuration = durationFromMS(c.cfg.TerminateTimeoutMS, defaultMCPTerminateWait) tr.TerminateDuration = durationFromMS(c.cfg.TerminateTimeoutMS, defaultMCPTerminateWait)
return tr, nil return tr, nil
case "streamable_http": }
if strings.TrimSpace(c.cfg.URL) == "" {
return nil, fmt.Errorf("mcp server %q: url is required for streamable_http transport", c.cfg.Name) func (c *mcpClient) requiredServerURL(transport string) (string, error) {
} endpoint := strings.TrimSpace(c.cfg.URL)
return &mcp.StreamableClientTransport{ if endpoint == "" {
Endpoint: c.cfg.URL, return "", fmt.Errorf("mcp server %q: url is required for %s transport", c.cfg.Name, transport)
}, nil
case "sse":
if strings.TrimSpace(c.cfg.URL) == "" {
return nil, fmt.Errorf("mcp server %q: url is required for sse transport", c.cfg.Name)
}
return &mcp.SSEClientTransport{
Endpoint: c.cfg.URL,
}, nil
default:
return nil, fmt.Errorf("mcp server %q: unsupported transport %q", c.cfg.Name, c.cfg.Transport)
} }
return endpoint, nil
} }
func formatMCPCallToolResult(result *mcp.CallToolResult) (string, error) { func formatMCPCallToolResult(result *mcp.CallToolResult) (string, error) {

View file

@ -35,10 +35,7 @@ func TestMCPExternalPopularFilesystemCommand(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel() defer cancel()
cfg := config.MCPToolsConfig{ tools := loadExternalMCPTools(t, ctx, config.MCPServerConfig{
Enabled: true,
Servers: []config.MCPServerConfig{
{
Name: "filesystem", Name: "filesystem",
Enabled: true, Enabled: true,
Transport: "command", Transport: "command",
@ -47,23 +44,10 @@ func TestMCPExternalPopularFilesystemCommand(t *testing.T) {
ToolPrefix: "mcp_fs", ToolPrefix: "mcp_fs",
StartupTimeoutMS: 30000, StartupTimeoutMS: 30000,
CallTimeoutMS: 30000, CallTimeoutMS: 30000,
}, })
},
}
tools, err := LoadMCPTools(ctx, cfg, "") listAllowedDirs := requireToolByName(t, tools, "mcp_fs_list_allowed_directories")
if err != nil { readFile := requireToolByName(t, tools, "mcp_fs_read_file")
t.Fatalf("LoadMCPTools() error: %v", err)
}
listAllowedDirs := findToolByName(tools, "mcp_fs_list_allowed_directories")
if listAllowedDirs == nil {
t.Fatalf("missing tool mcp_fs_list_allowed_directories; got %v", toolNames(tools))
}
readFile := findToolByName(tools, "mcp_fs_read_file")
if readFile == nil {
t.Fatalf("missing tool mcp_fs_read_file; got %v", toolNames(tools))
}
out, err := listAllowedDirs.Execute(ctx, map[string]interface{}{}) out, err := listAllowedDirs.Execute(ctx, map[string]interface{}{})
if err != nil { if err != nil {
@ -89,10 +73,7 @@ func TestMCPExternalPopularMemoryCommand(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel() defer cancel()
cfg := config.MCPToolsConfig{ tools := loadExternalMCPTools(t, ctx, config.MCPServerConfig{
Enabled: true,
Servers: []config.MCPServerConfig{
{
Name: "memory", Name: "memory",
Enabled: true, Enabled: true,
Transport: "command", Transport: "command",
@ -102,19 +83,9 @@ func TestMCPExternalPopularMemoryCommand(t *testing.T) {
ToolPrefix: "mcp_memory", ToolPrefix: "mcp_memory",
StartupTimeoutMS: 30000, StartupTimeoutMS: 30000,
CallTimeoutMS: 30000, CallTimeoutMS: 30000,
}, })
},
}
tools, err := LoadMCPTools(ctx, cfg, "") readGraph := requireToolByName(t, tools, "mcp_memory_read_graph")
if err != nil {
t.Fatalf("LoadMCPTools() error: %v", err)
}
readGraph := findToolByName(tools, "mcp_memory_read_graph")
if readGraph == nil {
t.Fatalf("missing tool mcp_memory_read_graph; got %v", toolNames(tools))
}
out, err := readGraph.Execute(ctx, map[string]interface{}{}) out, err := readGraph.Execute(ctx, map[string]interface{}{})
if err != nil { if err != nil {
@ -135,10 +106,7 @@ func TestMCPExternalPopularEverythingSSE(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel() defer cancel()
cfg := config.MCPToolsConfig{ tools := loadExternalMCPTools(t, ctx, config.MCPServerConfig{
Enabled: true,
Servers: []config.MCPServerConfig{
{
Name: "everything", Name: "everything",
Enabled: true, Enabled: true,
Transport: "sse", Transport: "sse",
@ -146,28 +114,17 @@ func TestMCPExternalPopularEverythingSSE(t *testing.T) {
ToolPrefix: "mcp_every", ToolPrefix: "mcp_every",
StartupTimeoutMS: 30000, StartupTimeoutMS: 30000,
CallTimeoutMS: 30000, CallTimeoutMS: 30000,
}, })
},
}
tools, err := LoadMCPTools(ctx, cfg, "") echoTool := requireToolByName(t, tools, "mcp_every_echo")
if err != nil {
t.Fatalf("LoadMCPTools() error: %v", err)
}
echoTool := findToolByName(tools, "mcp_every_echo")
if echoTool == nil {
t.Fatalf("missing tool mcp_every_echo; got %v", toolNames(tools))
}
out, err := echoTool.Execute(ctx, map[string]interface{}{"message": "hello from sse"}) out, err := echoTool.Execute(ctx, map[string]interface{}{"message": "hello from sse"})
if err != nil { if err != nil {
t.Fatalf("Execute(echo) error: %v", err) t.Fatalf("Execute(echo) error: %v", err)
} }
if !strings.Contains(out, "Echo: hello from sse") { if !strings.Contains(out, "hello from sse") {
t.Fatalf("unexpected echo output: %s", out) t.Fatalf("unexpected echo output: %s", out)
} }
} }
func TestMCPExternalPopularEverythingStreamableHTTP(t *testing.T) { func TestMCPExternalPopularEverythingStreamableHTTP(t *testing.T) {
@ -180,10 +137,7 @@ func TestMCPExternalPopularEverythingStreamableHTTP(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel() defer cancel()
cfg := config.MCPToolsConfig{ tools := loadExternalMCPTools(t, ctx, config.MCPServerConfig{
Enabled: true,
Servers: []config.MCPServerConfig{
{
Name: "everything-http", Name: "everything-http",
Enabled: true, Enabled: true,
Transport: "streamable_http", Transport: "streamable_http",
@ -191,19 +145,9 @@ func TestMCPExternalPopularEverythingStreamableHTTP(t *testing.T) {
ToolPrefix: "mcp_http", ToolPrefix: "mcp_http",
StartupTimeoutMS: 30000, StartupTimeoutMS: 30000,
CallTimeoutMS: 30000, CallTimeoutMS: 30000,
}, })
},
}
tools, err := LoadMCPTools(ctx, cfg, "") echoTool := requireToolByName(t, tools, "mcp_http_echo")
if err != nil {
t.Fatalf("LoadMCPTools() error: %v", err)
}
echoTool := findToolByName(tools, "mcp_http_echo")
if echoTool == nil {
t.Fatalf("missing tool mcp_http_echo; got %v", toolNames(tools))
}
out, err := echoTool.Execute(ctx, map[string]interface{}{"message": "hello from streamable-http"}) out, err := echoTool.Execute(ctx, map[string]interface{}{"message": "hello from streamable-http"})
if err != nil { if err != nil {
@ -212,7 +156,6 @@ func TestMCPExternalPopularEverythingStreamableHTTP(t *testing.T) {
if !strings.Contains(out, "hello from streamable-http") { if !strings.Contains(out, "hello from streamable-http") {
t.Fatalf("unexpected echo output: %s", out) t.Fatalf("unexpected echo output: %s", out)
} }
} }
func requireExternalMCPTests(t *testing.T) { func requireExternalMCPTests(t *testing.T) {
@ -225,6 +168,31 @@ func requireExternalMCPTests(t *testing.T) {
} }
} }
func loadExternalMCPTools(t *testing.T, ctx context.Context, server config.MCPServerConfig) []Tool {
t.Helper()
cfg := config.MCPToolsConfig{
Enabled: true,
Servers: []config.MCPServerConfig{server},
}
tools, err := LoadMCPTools(ctx, cfg, "")
if err != nil {
t.Fatalf("LoadMCPTools() error: %v", err)
}
return tools
}
func requireToolByName(t *testing.T, tools []Tool, name string) Tool {
t.Helper()
tool := findToolByName(tools, name)
if tool == nil {
t.Fatalf("missing tool %s; got %v", name, toolNames(tools))
}
return tool
}
func findToolByName(tools []Tool, name string) Tool { func findToolByName(tools []Tool, name string) Tool {
for _, tool := range tools { for _, tool := range tools {
if tool.Name() == name { if tool.Name() == name {
@ -282,5 +250,4 @@ func startEverythingServer(t *testing.T, port int, mode string) {
_ = cmd.Process.Kill() _ = cmd.Process.Kill()
_, _ = cmd.Process.Wait() _, _ = cmd.Process.Wait()
}) })
} }

View file

@ -169,36 +169,28 @@ func TestLoadMCPTools_InvalidServerAggregatesError(t *testing.T) {
} }
func TestBuildTransport_CommandTerminateDefaults(t *testing.T) { func TestBuildTransport_CommandTerminateDefaults(t *testing.T) {
client := newMCPClient(config.MCPServerConfig{ assertCommandTransportTerminateDuration(t, config.MCPServerConfig{
Name: "default-terminate", Name: "default-terminate",
Enabled: true, Enabled: true,
Transport: "command", Transport: "command",
Command: "test-command", Command: "test-command",
}, "") }, defaultMCPTerminateWait)
tr, err := client.buildTransport()
if err != nil {
t.Fatalf("buildTransport() error: %v", err)
}
cmdTr, ok := tr.(*mcp.CommandTransport)
if !ok {
t.Fatalf("buildTransport() returned %T, want *mcp.CommandTransport", tr)
}
if cmdTr.TerminateDuration != defaultMCPTerminateWait {
t.Fatalf("TerminateDuration = %v, want %v", cmdTr.TerminateDuration, defaultMCPTerminateWait)
}
} }
func TestBuildTransport_CommandTerminateOverride(t *testing.T) { func TestBuildTransport_CommandTerminateOverride(t *testing.T) {
client := newMCPClient(config.MCPServerConfig{ assertCommandTransportTerminateDuration(t, config.MCPServerConfig{
Name: "override-terminate", Name: "override-terminate",
Enabled: true, Enabled: true,
Transport: "command", Transport: "command",
Command: "test-command", Command: "test-command",
TerminateTimeoutMS: 2500, TerminateTimeoutMS: 2500,
}, "") }, 2500*time.Millisecond)
}
func assertCommandTransportTerminateDuration(t *testing.T, cfg config.MCPServerConfig, want time.Duration) {
t.Helper()
client := newMCPClient(cfg, "")
tr, err := client.buildTransport() tr, err := client.buildTransport()
if err != nil { if err != nil {
t.Fatalf("buildTransport() error: %v", err) t.Fatalf("buildTransport() error: %v", err)
@ -208,8 +200,8 @@ func TestBuildTransport_CommandTerminateOverride(t *testing.T) {
if !ok { if !ok {
t.Fatalf("buildTransport() returned %T, want *mcp.CommandTransport", tr) t.Fatalf("buildTransport() returned %T, want *mcp.CommandTransport", tr)
} }
if cmdTr.TerminateDuration != 2500*time.Millisecond { if cmdTr.TerminateDuration != want {
t.Fatalf("TerminateDuration = %v, want %v", cmdTr.TerminateDuration, 2500*time.Millisecond) t.Fatalf("TerminateDuration = %v, want %v", cmdTr.TerminateDuration, want)
} }
} }