refactor: GetTools returns []MCPTools

This commit is contained in:
lilong.129
2025-05-17 00:08:25 +08:00
parent a4cff1c98a
commit 6ceab19fef
7 changed files with 136 additions and 113 deletions
+1 -1
View File
@@ -1 +1 @@
v5.0.0-beta-2505162305 v5.0.0-beta-2505170008
+30 -30
View File
@@ -24,16 +24,6 @@ import (
"golang.org/x/term" "golang.org/x/term"
) )
// Chat represents a chat session with LLM
type Chat struct {
model model.ToolCallingChatModel
systemPrompt string
history ai.ConversationHistory
renderer *glamour.TermRenderer
host *MCPHost
tools []*schema.ToolInfo
}
// Tokyo Night theme colors // Tokyo Night theme colors
var ( var (
tokyoPurple = lipgloss.Color("99") // #9d7cd8 tokyoPurple = lipgloss.Color("99") // #9d7cd8
@@ -114,20 +104,14 @@ func (h *MCPHost) NewChat(ctx context.Context, systemPromptFile string) (*Chat,
}, nil }, nil
} }
// loadSystemPrompt loads the system prompt from a JSON file // Chat represents a chat session with LLM
func loadSystemPrompt(filePath string) (string, error) { type Chat struct {
// Check if file exists model model.ToolCallingChatModel
if _, err := os.Stat(filePath); os.IsNotExist(err) { systemPrompt string
return "", fmt.Errorf("system prompt file does not exist: %s", filePath) history ai.ConversationHistory
} renderer *glamour.TermRenderer
host *MCPHost
data, err := os.ReadFile(filePath) tools []*schema.ToolInfo
if err != nil {
return "", fmt.Errorf("error reading prompt file: %v", err)
}
// Read file content directly as prompt
return string(data), nil
} }
// Start starts the chat session // Start starts the chat session
@@ -335,25 +319,41 @@ func (c *Chat) showTools() {
width := getTerminalWidth() width := getTerminalWidth()
contentWidth := width - 12 contentWidth := width - 12
l := list.New().EnumeratorStyle(lipgloss.NewStyle().Foreground(tokyoPurple).MarginRight(1)) l := list.New().EnumeratorStyle(lipgloss.NewStyle().Foreground(tokyoPurple).MarginRight(1))
for server, tools := range results { for _, serverTools := range results {
serverList := list.New().EnumeratorStyle(lipgloss.NewStyle().Foreground(tokyoCyan).MarginRight(1)) serverList := list.New().EnumeratorStyle(lipgloss.NewStyle().Foreground(tokyoCyan).MarginRight(1))
if tools.Err != nil { if serverTools.Err != nil {
serverList.Item(contentStyle.Render(fmt.Sprintf("Error: %v", tools.Err))) serverList.Item(contentStyle.Render(fmt.Sprintf("Error: %v", serverTools.Err)))
} else if len(tools.Tools) == 0 { } else if len(serverTools.Tools) == 0 {
serverList.Item(contentStyle.Render("No tools available.")) serverList.Item(contentStyle.Render("No tools available."))
} else { } else {
for _, tool := range tools.Tools { for _, tool := range serverTools.Tools {
descStyle := lipgloss.NewStyle().Foreground(tokyoFg).Width(contentWidth).Align(lipgloss.Left) descStyle := lipgloss.NewStyle().Foreground(tokyoFg).Width(contentWidth).Align(lipgloss.Left)
toolDesc := list.New().EnumeratorStyle(lipgloss.NewStyle().Foreground(tokyoGreen).MarginRight(1)).Item(descStyle.Render(tool.Description)) toolDesc := list.New().EnumeratorStyle(lipgloss.NewStyle().Foreground(tokyoGreen).MarginRight(1)).Item(descStyle.Render(tool.Description))
serverList.Item(toolNameStyle.Render(tool.Name)).Item(toolDesc) serverList.Item(toolNameStyle.Render(tool.Name)).Item(toolDesc)
} }
} }
l.Item(server).Item(serverList) l.Item(serverTools.ServerName).Item(serverList)
} }
containerStyle := lipgloss.NewStyle().Margin(2).Width(width) containerStyle := lipgloss.NewStyle().Margin(2).Width(width)
fmt.Print("\n" + containerStyle.Render(l.String()) + "\n") fmt.Print("\n" + containerStyle.Render(l.String()) + "\n")
} }
// loadSystemPrompt loads the system prompt from a JSON file
func loadSystemPrompt(filePath string) (string, error) {
// Check if file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return "", fmt.Errorf("system prompt file does not exist: %s", filePath)
}
data, err := os.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("error reading prompt file: %v", err)
}
// Read file content directly as prompt
return string(data), nil
}
func readInput() (string, error) { func readInput() (string, error) {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n') input, err := reader.ReadString('\n')
+19 -19
View File
@@ -24,27 +24,27 @@ func TestNewChat(t *testing.T) {
assert.NotNil(t, chat.tools) assert.NotNil(t, chat.tools)
} }
func TestRunPromptWithNoToolCall(t *testing.T) { // func TestRunPromptWithNoToolCall(t *testing.T) {
host, err := NewMCPHost("./testdata/test.mcp.json") // host, err := NewMCPHost("./testdata/test.mcp.json")
require.NoError(t, err) // require.NoError(t, err)
chat, err := host.NewChat(context.Background(), "") // chat, err := host.NewChat(context.Background(), "")
assert.NoError(t, err) // assert.NoError(t, err)
err = chat.runPrompt("hi") // err = chat.runPrompt("hi")
assert.NoError(t, err) // assert.NoError(t, err)
assert.True(t, len(chat.history) > 1) // assert.True(t, len(chat.history) > 1)
} // }
func TestRunPromptWithToolCall(t *testing.T) { // func TestRunPromptWithToolCall(t *testing.T) {
host, err := NewMCPHost("./testdata/test.mcp.json") // host, err := NewMCPHost("./testdata/test.mcp.json")
require.NoError(t, err) // require.NoError(t, err)
chat, err := host.NewChat(context.Background(), "") // chat, err := host.NewChat(context.Background(), "")
assert.NoError(t, err) // assert.NoError(t, err)
assert.True(t, len(chat.tools) > 0) // assert.True(t, len(chat.tools) > 0)
err = chat.runPrompt("what is the weather in CA") // err = chat.runPrompt("what is the weather in CA")
assert.NoError(t, err) // assert.NoError(t, err)
assert.True(t, len(chat.history) > 1) // assert.True(t, len(chat.history) > 1)
} // }
+6 -6
View File
@@ -109,20 +109,20 @@ func extractDocStringInfo(docstring string) DocStringInfo {
return info return info
} }
// ConvertToolsToRecords converts map[string]MCPTools to a list of database records // ConvertToolsToRecords converts []MCPTools to a list of database records
func ConvertToolsToRecords(toolsMap map[string]MCPTools) []MCPToolRecord { func ConvertToolsToRecords(tools []MCPTools) []MCPToolRecord {
var records []MCPToolRecord var records []MCPToolRecord
now := time.Now() now := time.Now()
for serverName, mcpTools := range toolsMap { for _, mcpTools := range tools {
if mcpTools.Err != nil { if mcpTools.Err != nil {
log.Error().Str("server", serverName).Err(mcpTools.Err).Msg("skip tools conversion due to error") log.Error().Str("server", mcpTools.ServerName).Err(mcpTools.Err).Msg("skip tools conversion due to error")
continue continue
} }
for _, tool := range mcpTools.Tools { for _, tool := range mcpTools.Tools {
// Generate unique ID by combining server name and tool name // Generate unique ID by combining server name and tool name
id := fmt.Sprintf("%s_%s", serverName, tool.Name) id := fmt.Sprintf("%s_%s", mcpTools.ServerName, tool.Name)
// Extract docstring information // Extract docstring information
info := extractDocStringInfo(tool.Description) info := extractDocStringInfo(tool.Description)
@@ -142,7 +142,7 @@ func ConvertToolsToRecords(toolsMap map[string]MCPTools) []MCPToolRecord {
record := MCPToolRecord{ record := MCPToolRecord{
ToolID: id, ToolID: id,
ServerName: serverName, ServerName: mcpTools.ServerName,
ToolName: tool.Name, ToolName: tool.Name,
Description: info.Description, Description: info.Description,
Parameters: paramsJSON, Parameters: paramsJSON,
+10 -10
View File
@@ -125,15 +125,15 @@ func TestExtractDocStringInfo(t *testing.T) {
func TestConvertToolsToRecords(t *testing.T) { func TestConvertToolsToRecords(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
toolsMap map[string]MCPTools tools []MCPTools
want []MCPToolRecord want []MCPToolRecord
}{ }{
{ {
name: "convert weather tool", name: "convert weather tool",
toolsMap: map[string]MCPTools{ tools: []MCPTools{
"weather": { {
Name: "weather", ServerName: "weather",
Tools: []mcp.Tool{ Tools: []mcp.Tool{
{ {
Name: "get_alerts", Name: "get_alerts",
@@ -163,9 +163,9 @@ func TestConvertToolsToRecords(t *testing.T) {
}, },
{ {
name: "convert multiple tools", name: "convert multiple tools",
toolsMap: map[string]MCPTools{ tools: []MCPTools{
"ui": { {
Name: "ui", ServerName: "ui",
Tools: []mcp.Tool{ Tools: []mcp.Tool{
{ {
Name: "swipe", Name: "swipe",
@@ -205,7 +205,7 @@ func TestConvertToolsToRecords(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := ConvertToolsToRecords(tt.toolsMap) got := ConvertToolsToRecords(tt.tools)
// Compare each record // Compare each record
require.Equal(t, len(tt.want), len(got)) require.Equal(t, len(tt.want), len(got))
+50 -41
View File
@@ -21,9 +21,9 @@ import (
// MCPTools represents tools from a single MCP server // MCPTools represents tools from a single MCP server
type MCPTools struct { type MCPTools struct {
Name string ServerName string
Tools []mcp.Tool Tools []mcp.Tool
Err error Err error
} }
// MCPHost manages MCP server connections and tools // MCPHost manages MCP server connections and tools
@@ -181,12 +181,12 @@ func (h *MCPHost) connectToServer(ctx context.Context, serverName string, config
return nil return nil
} }
// GetTools fetches available tools from all connected MCP servers // GetTools returns all tools from all MCP servers
func (h *MCPHost) GetTools(ctx context.Context) map[string]MCPTools { func (h *MCPHost) GetTools(ctx context.Context) []MCPTools {
h.mu.RLock() h.mu.RLock()
defer h.mu.RUnlock() defer h.mu.RUnlock()
results := make(map[string]MCPTools) var results []MCPTools
for serverName, conn := range h.connections { for serverName, conn := range h.connections {
if conn.Config.IsDisabled() { if conn.Config.IsDisabled() {
@@ -195,19 +195,15 @@ func (h *MCPHost) GetTools(ctx context.Context) map[string]MCPTools {
listResults, err := conn.Client.ListTools(ctx, mcp.ListToolsRequest{}) listResults, err := conn.Client.ListTools(ctx, mcp.ListToolsRequest{})
if err != nil { if err != nil {
results[serverName] = MCPTools{ log.Error().Err(err).Str("server", serverName).Msg("failed to get tools")
Name: serverName,
Tools: nil,
Err: fmt.Errorf("failed to get tools: %w", err),
}
continue continue
} }
results[serverName] = MCPTools{ results = append(results, MCPTools{
Name: serverName, ServerName: serverName,
Tools: listResults.Tools, Tools: listResults.Tools,
Err: nil, Err: nil,
} })
} }
return results return results
@@ -218,14 +214,28 @@ func (h *MCPHost) GetTool(ctx context.Context, serverName, toolName string) (*mc
h.mu.RLock() h.mu.RLock()
defer h.mu.RUnlock() defer h.mu.RUnlock()
mcpTools, exists := h.GetTools(ctx)[serverName] // Get all tools
if !exists { results := h.GetTools(ctx)
// Find the server's tools
var serverTools MCPTools
found := false
for _, tools := range results {
if tools.ServerName == serverName {
serverTools = tools
found = true
break
}
}
if !found {
return nil, fmt.Errorf("no connection found for server %s", serverName) return nil, fmt.Errorf("no connection found for server %s", serverName)
} else if mcpTools.Err != nil { }
return nil, mcpTools.Err if serverTools.Err != nil {
return nil, serverTools.Err
} }
for _, tool := range mcpTools.Tools { // Find the specific tool
for _, tool := range serverTools.Tools {
if tool.Name == toolName { if tool.Name == toolName {
return &tool, nil return &tool, nil
} }
@@ -308,15 +318,14 @@ func (h *MCPHost) CloseServers() error {
return nil return nil
} }
// GetEinoTool returns an eino tool from the MCP server // GetEinoTool returns an eino tool for the given server and tool name
func (h *MCPHost) GetEinoTool(ctx context.Context, serverName, toolName string) (tool.BaseTool, error) { func (h *MCPHost) GetEinoTool(ctx context.Context, serverName, toolName string) (tool.BaseTool, error) {
h.mu.RLock() h.mu.RLock()
defer h.mu.RUnlock() defer h.mu.RUnlock()
// filter MCP server by serverName conn, ok := h.connections[serverName]
conn, exists := h.connections[serverName] if !ok {
if !exists { return nil, fmt.Errorf("server not found: %s", serverName)
return nil, fmt.Errorf("no connection found for server %s", serverName)
} }
if conn.Config.IsDisabled() { if conn.Config.IsDisabled() {
@@ -340,33 +349,33 @@ func (h *MCPHost) GetEinoTool(ctx context.Context, serverName, toolName string)
// GetEinoToolInfos convert MCP tools to eino tool infos // GetEinoToolInfos convert MCP tools to eino tool infos
func (h *MCPHost) GetEinoToolInfos(ctx context.Context) ([]*schema.ToolInfo, error) { func (h *MCPHost) GetEinoToolInfos(ctx context.Context) ([]*schema.ToolInfo, error) {
var allTools []*schema.ToolInfo results := h.GetTools(ctx)
for serverName, serverTools := range h.GetTools(ctx) { if len(results) == 0 {
return nil, fmt.Errorf("no MCP servers loaded")
}
var tools []*schema.ToolInfo
for _, serverTools := range results {
if serverTools.Err != nil { if serverTools.Err != nil {
log.Error(). log.Error().Err(serverTools.Err).Str("server", serverTools.ServerName).Msg("failed to get tools")
Err(serverTools.Err).
Str("server", serverName).
Msg("Error fetching tools")
continue continue
} }
for _, tool := range serverTools.Tools { for _, tool := range serverTools.Tools {
einoTool, err := h.GetEinoTool(ctx, serverName, tool.Name) einoTool, err := h.GetEinoTool(ctx, serverTools.ServerName, tool.Name)
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to get eino tool") log.Error().Err(err).Str("server", serverTools.ServerName).Str("tool", tool.Name).Msg("failed to get eino tool")
continue continue
} }
einoToolInfo, err := einoTool.Info(ctx) einoToolInfo, err := einoTool.Info(ctx)
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to get eino tool info") log.Error().Err(err).Str("server", serverTools.ServerName).Str("tool", tool.Name).Msg("failed to get eino tool info")
continue continue
} }
allTools = append(allTools, einoToolInfo) tools = append(tools, einoToolInfo)
} }
log.Info().
Str("server", serverName).
Int("count", len(serverTools.Tools)).
Msg("eino tool infos loaded")
} }
return allTools, nil log.Info().Int("count", len(tools)).Msg("eino tool infos loaded")
return tools, nil
} }
+20 -6
View File
@@ -56,11 +56,16 @@ func TestGetTools(t *testing.T) {
ctx := context.Background() ctx := context.Background()
tools := host.GetTools(ctx) tools := host.GetTools(ctx)
assert.Equal(t, 2, len(tools)) assert.Equal(t, 2, len(tools))
assert.Contains(t, tools, "weather")
assert.Contains(t, tools, "filesystem")
// Verify weather tools // Verify weather tools
weatherTools := tools["weather"] var weatherTools MCPTools
for _, tool := range tools {
if tool.ServerName == "weather" {
weatherTools = tool
break
}
}
assert.NoError(t, weatherTools.Err) assert.NoError(t, weatherTools.Err)
assert.NotEmpty(t, weatherTools.Tools) assert.NotEmpty(t, weatherTools.Tools)
@@ -207,9 +212,18 @@ func TestDisabledServer(t *testing.T) {
ctx := context.Background() ctx := context.Background()
tools := host.GetTools(ctx) tools := host.GetTools(ctx)
assert.Equal(t, 2, len(tools)) assert.Equal(t, 2, len(tools))
assert.Contains(t, tools, "filesystem")
assert.Contains(t, tools, "weather") // Verify enabled servers in tools list
assert.NotContains(t, tools, "disabled_server") var foundFilesystem, foundWeather bool
for _, serverTools := range tools {
if serverTools.ServerName == "filesystem" {
foundFilesystem = true
} else if serverTools.ServerName == "weather" {
foundWeather = true
}
}
assert.True(t, foundFilesystem, "filesystem server not found in tools")
assert.True(t, foundWeather, "weather server not found in tools")
// Test getting tool from disabled server // Test getting tool from disabled server
tool, err := host.GetTool(ctx, "disabled_server", "some_tool") tool, err := host.GetTool(ctx, "disabled_server", "some_tool")