feat: add MCP tools registration to LLM service

- Add RegisterTools method to ILLMService interface
- Create shared MCP to eino tool converter
- Auto-register built-in uixt tools in XTDriver initialization
- Refactor MCPHost to use shared converter
- Add comprehensive test coverage for tool conversion

This enables doubao-1.5-thinking-vision-pro model to access
MCP tools through function calling mechanism.
This commit is contained in:
lilong.129
2025-06-09 22:18:03 +08:00
parent dd52faef57
commit 39acadb0a7
8 changed files with 221 additions and 24 deletions

View File

@@ -1 +1 @@
v5.0.0-beta-2506092052
v5.0.0-beta-2506092219

View File

@@ -17,6 +17,7 @@ import (
"github.com/cloudwego/eino/schema"
"github.com/httprunner/httprunner/v5/internal/version"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/ai"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
"github.com/pkg/errors"
@@ -413,7 +414,7 @@ func (h *MCPHost) GetEinoToolInfos(ctx context.Context) ([]*schema.ToolInfo, err
return nil, fmt.Errorf("no MCP servers loaded")
}
var tools []*schema.ToolInfo
var allTools []*schema.ToolInfo
for _, serverTools := range results {
if serverTools.Err != nil {
log.Error().Err(serverTools.Err).
@@ -421,29 +422,13 @@ func (h *MCPHost) GetEinoToolInfos(ctx context.Context) ([]*schema.ToolInfo, err
continue
}
var toolNames []string
for _, tool := range serverTools.Tools {
einoTool, err := h.GetEinoTool(ctx, serverTools.ServerName, tool.Name)
if err != nil {
log.Error().Err(err).Str("server", serverTools.ServerName).
Str("tool", tool.Name).Msg("failed to get eino tool")
continue
}
einoToolInfo, err := einoTool.Info(ctx)
if err != nil {
log.Error().Err(err).Str("server", serverTools.ServerName).
Str("tool", tool.Name).Msg("failed to get eino tool info")
continue
}
einoToolInfo.Name = fmt.Sprintf("%s__%s", serverTools.ServerName, tool.Name)
tools = append(tools, einoToolInfo)
toolNames = append(toolNames, tool.Name)
}
log.Debug().Str("server", serverTools.ServerName).
Strs("tools", toolNames).Msg("loaded MCP tools")
// convert MCP tools to eino tools
einoTools := ai.ConvertMCPToolsToEinoToolInfos(
serverTools.Tools, serverTools.ServerName)
allTools = append(allTools, einoTools...)
}
return tools, nil
return allTools, nil
}
// parseHeaders parses header strings into a map

View File

@@ -101,7 +101,7 @@ func TestStartToGoal(t *testing.T) {
`
testCase := &hrp.TestCase{
Config: hrp.NewConfig("run ui action with start to goal").
Config: hrp.NewConfig("连连看小游戏自动化测试").
SetLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428),
TestSteps: []hrp.IStep{
hrp.NewStep("启动抖音「连了又连」小游戏").

View File

@@ -3,6 +3,7 @@ package ai
import (
"context"
"github.com/cloudwego/eino/schema"
"github.com/httprunner/httprunner/v5/uixt/option"
)
@@ -10,6 +11,8 @@ import (
type ILLMService interface {
Call(ctx context.Context, opts *PlanningOptions) (*PlanningResult, error)
Assert(ctx context.Context, opts *AssertOptions) (*AssertionResult, error)
// RegisterTools registers tools for function calling
RegisterTools(tools []*schema.ToolInfo) error
}
func NewLLMService(modelType option.LLMServiceType) (ILLMService, error) {
@@ -49,3 +52,12 @@ func (c *combinedLLMService) Call(ctx context.Context, opts *PlanningOptions) (*
func (c *combinedLLMService) Assert(ctx context.Context, opts *AssertOptions) (*AssertionResult, error) {
return c.asserter.Assert(ctx, opts)
}
// RegisterTools registers tools for function calling
func (c *combinedLLMService) RegisterTools(tools []*schema.ToolInfo) error {
// Only register tools to planner since asserter doesn't need tools
if planner, ok := c.planner.(*Planner); ok {
return planner.RegisterTools(tools)
}
return nil
}

100
uixt/ai/converter.go Normal file
View File

@@ -0,0 +1,100 @@
package ai
import (
"fmt"
"github.com/cloudwego/eino/schema"
"github.com/mark3labs/mcp-go/mcp"
)
// ConvertMCPToolToEinoToolInfo converts an MCP tool to eino ToolInfo
func ConvertMCPToolToEinoToolInfo(mcpTool mcp.Tool, namePrefix string) *schema.ToolInfo {
// Create eino ToolInfo from MCP tool
toolName := mcpTool.Name
if namePrefix != "" {
toolName = fmt.Sprintf("%s__%s", namePrefix, mcpTool.Name)
}
toolInfo := &schema.ToolInfo{
Name: toolName,
Desc: mcpTool.Description,
}
// Convert input schema
if mcpTool.InputSchema.Properties != nil {
params := make(map[string]*schema.ParameterInfo)
for propName, propValue := range mcpTool.InputSchema.Properties {
if propMap, ok := propValue.(map[string]interface{}); ok {
paramInfo := &schema.ParameterInfo{}
if propType, exists := propMap["type"]; exists {
if typeStr, ok := propType.(string); ok {
switch typeStr {
case "string":
paramInfo.Type = schema.String
case "number":
paramInfo.Type = schema.Number
case "integer":
paramInfo.Type = schema.Integer
case "boolean":
paramInfo.Type = schema.Boolean
case "array":
paramInfo.Type = schema.Array
case "object":
paramInfo.Type = schema.Object
default:
paramInfo.Type = schema.String // default to string
}
}
}
if description, exists := propMap["description"]; exists {
if descStr, ok := description.(string); ok {
paramInfo.Desc = descStr
}
}
if enum, exists := propMap["enum"]; exists {
if enumSlice, ok := enum.([]interface{}); ok {
var enumStrings []string
for _, enumVal := range enumSlice {
if enumStr, ok := enumVal.(string); ok {
enumStrings = append(enumStrings, enumStr)
}
}
paramInfo.Enum = enumStrings
}
}
// Check if this parameter is required
for _, requiredField := range mcpTool.InputSchema.Required {
if requiredField == propName {
paramInfo.Required = true
break
}
}
params[propName] = paramInfo
}
}
if len(params) > 0 {
toolInfo.ParamsOneOf = schema.NewParamsOneOfByParams(params)
}
}
return toolInfo
}
// ConvertMCPToolsToEinoToolInfos converts multiple MCP tools to eino ToolInfos
func ConvertMCPToolsToEinoToolInfos(mcpTools []mcp.Tool, namePrefix string) []*schema.ToolInfo {
var einoTools []*schema.ToolInfo
for _, mcpTool := range mcpTools {
einoTool := ConvertMCPToolToEinoToolInfo(mcpTool, namePrefix)
if einoTool != nil {
einoTools = append(einoTools, einoTool)
}
}
return einoTools
}

84
uixt/ai/converter_test.go Normal file
View File

@@ -0,0 +1,84 @@
package ai
import (
"testing"
"github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
)
func TestConvertMCPToolToEinoToolInfo(t *testing.T) {
// Create a mock MCP tool
mcpTool := mcp.Tool{
Name: "test_tool",
Description: "Test tool description",
InputSchema: mcp.ToolInputSchema{
Type: "object",
Properties: map[string]interface{}{
"param1": map[string]interface{}{
"type": "string",
"description": "Test parameter 1",
},
"param2": map[string]interface{}{
"type": "number",
"description": "Test parameter 2",
},
},
Required: []string{"param1"},
},
}
// Convert to eino ToolInfo using shared converter
einoTool := ConvertMCPToolToEinoToolInfo(mcpTool, "uixt")
// Verify the conversion
assert.NotNil(t, einoTool)
assert.Equal(t, "uixt__test_tool", einoTool.Name)
assert.Equal(t, "Test tool description", einoTool.Desc)
assert.NotNil(t, einoTool.ParamsOneOf)
}
func TestConvertMCPToolWithoutParams(t *testing.T) {
// Create a mock MCP tool without parameters
mcpTool := mcp.Tool{
Name: "simple_tool",
Description: "Simple tool without parameters",
InputSchema: mcp.ToolInputSchema{
Type: "object",
},
}
// Convert to eino ToolInfo using shared converter
einoTool := ConvertMCPToolToEinoToolInfo(mcpTool, "uixt")
// Verify the conversion
assert.NotNil(t, einoTool)
assert.Equal(t, "uixt__simple_tool", einoTool.Name)
assert.Equal(t, "Simple tool without parameters", einoTool.Desc)
}
func TestConvertMCPToolsToEinoToolInfos(t *testing.T) {
// Create multiple mock MCP tools
mcpTools := []mcp.Tool{
{
Name: "tool1",
Description: "First tool",
InputSchema: mcp.ToolInputSchema{Type: "object"},
},
{
Name: "tool2",
Description: "Second tool",
InputSchema: mcp.ToolInputSchema{Type: "object"},
},
}
// Convert to eino ToolInfos using shared converter
einoTools := ConvertMCPToolsToEinoToolInfos(mcpTools, "test_server")
// Verify the conversion
assert.Len(t, einoTools, 2)
assert.Equal(t, "test_server__tool1", einoTools[0].Name)
assert.Equal(t, "test_server__tool2", einoTools[1].Name)
assert.Equal(t, "First tool", einoTools[0].Desc)
assert.Equal(t, "Second tool", einoTools[1].Desc)
}

View File

@@ -77,6 +77,15 @@ func (p *Planner) RegisterTools(tools []*schema.ToolInfo) error {
if err != nil {
return errors.Wrap(err, "failed to register tools")
}
var toolNames []string
for _, tool := range tools {
toolNames = append(toolNames, tool.Name)
}
log.Debug().Strs("tools", toolNames).
Str("model", string(p.modelConfig.ModelType)).
Msg("registered tools to model")
p.model = toolCallingModel
return nil
}

View File

@@ -38,6 +38,13 @@ func NewXTDriver(driver IDriver, opts ...option.AIServiceOption) (*XTDriver, err
if err != nil {
return nil, errors.Wrap(err, "init llm service failed")
}
// Register uixt MCP tools to LLM service
mcpTools := driverExt.client.Server.ListTools()
einoTools := ai.ConvertMCPToolsToEinoToolInfos(mcpTools, "uixt")
if err := driverExt.LLMService.RegisterTools(einoTools); err != nil {
log.Warn().Err(err).Msg("failed to register uixt tools")
}
}
return driverExt, nil