Files
kandev/apps/backend/cmd/mock-agent/mcp_client.go
T

120 lines
3.2 KiB
Go

package main
import (
"context"
"fmt"
"strings"
"sync"
acp "github.com/coder/acp-go-sdk"
mcpclient "github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
)
var (
mcpClients = map[string]*mcpclient.Client{}
mcpClientsMu sync.Mutex
)
// getMCPClient returns (or creates) an initialized MCP client for the named server.
func getMCPClient(serverName string) (*mcpclient.Client, error) {
mcpClientsMu.Lock()
defer mcpClientsMu.Unlock()
if c, ok := mcpClients[serverName]; ok {
return c, nil
}
srv, ok := mcpServers[serverName]
if !ok {
return nil, fmt.Errorf("unknown MCP server: %s", serverName)
}
c, err := mcpclient.NewSSEMCPClient(srv.URL)
if err != nil {
return nil, fmt.Errorf("create SSE client for %s: %w", serverName, err)
}
ctx := context.Background()
if err := c.Start(ctx); err != nil {
return nil, fmt.Errorf("start MCP client %s: %w", serverName, err)
}
initReq := mcp.InitializeRequest{}
initReq.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initReq.Params.ClientInfo = mcp.Implementation{
Name: "mock-agent",
Version: "1.0",
}
if _, err := c.Initialize(ctx, initReq); err != nil {
_ = c.Close()
return nil, fmt.Errorf("initialize MCP client %s: %w", serverName, err)
}
mcpClients[serverName] = c
return c, nil
}
// callMCPTool calls a tool on the named MCP server and returns the result text.
func callMCPTool(serverName, toolName string, args map[string]any) (string, error) {
return callMCPToolCtx(context.Background(), serverName, toolName, args)
}
// callMCPToolCtx calls a tool on the named MCP server with a caller-provided context.
// Use this when the caller needs to impose a timeout on the MCP call.
func callMCPToolCtx(ctx context.Context, serverName, toolName string, args map[string]any) (string, error) {
c, err := getMCPClient(serverName)
if err != nil {
return "", err
}
req := mcp.CallToolRequest{}
req.Params.Name = toolName
req.Params.Arguments = args
result, err := c.CallTool(ctx, req)
if err != nil {
return "", fmt.Errorf("call tool %s/%s: %w", serverName, toolName, err)
}
return extractMCPResultText(result), nil
}
// extractMCPResultText extracts text from an MCP CallToolResult.
func extractMCPResultText(result *mcp.CallToolResult) string {
var parts []string
for _, c := range result.Content {
if tc, ok := c.(mcp.TextContent); ok {
parts = append(parts, tc.Text)
}
}
if len(parts) == 0 {
return ""
}
return strings.Join(parts, "\n")
}
// registerACPMcpServers adds SSE MCP servers from an ACP NewSessionRequest
// to the global mcpServers map so callMCPTool can reach them.
func registerACPMcpServers(servers []acp.McpServer) {
for _, s := range servers {
if s.Sse != nil && s.Sse.Name != "" && s.Sse.Url != "" {
if mcpServers == nil {
mcpServers = make(map[string]mcpServerDef)
}
mcpServers[s.Sse.Name] = mcpServerDef{URL: s.Sse.Url, Type: "sse"}
_, _ = fmt.Fprintf(logOutput, "mock-agent: registered ACP MCP server %s at %s\n", s.Sse.Name, s.Sse.Url)
}
}
}
// closeMCPClients closes all open MCP clients (called on shutdown).
func closeMCPClients() {
mcpClientsMu.Lock()
defer mcpClientsMu.Unlock()
for _, c := range mcpClients {
_ = c.Close()
}
}