mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-05 15:37:35 +08:00
feat: load uixt mcp server in mcphost
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/httprunner/httprunner/v5/mcphost"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var CmdMCPServer = &cobra.Command{
|
||||||
|
Use: "mcp-server",
|
||||||
|
Short: "Start MCP server for UI automation",
|
||||||
|
Long: `Start MCP server for UI automation, expose device driver via MCP protocol`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
mcpServer := mcphost.NewMCPServer()
|
||||||
|
return mcpServer.Start()
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
v5.0.0-beta-2505201421
|
v5.0.0-beta-2505201707
|
||||||
|
|||||||
+18
-4
@@ -61,6 +61,14 @@ func NewMCPHost(configPath string) (*MCPHost, error) {
|
|||||||
|
|
||||||
// InitServers initializes all MCP servers
|
// InitServers initializes all MCP servers
|
||||||
func (h *MCPHost) InitServers(ctx context.Context) error {
|
func (h *MCPHost) InitServers(ctx context.Context) error {
|
||||||
|
// initialize uixt MCP server
|
||||||
|
h.connections["uixt"] = &Connection{
|
||||||
|
Client: &MCPClient4XTDriver{
|
||||||
|
server: NewMCPServer(),
|
||||||
|
},
|
||||||
|
Config: nil,
|
||||||
|
}
|
||||||
|
|
||||||
for name, server := range h.config.MCPServers {
|
for name, server := range h.config.MCPServers {
|
||||||
if server.Config.IsDisabled() {
|
if server.Config.IsDisabled() {
|
||||||
continue
|
continue
|
||||||
@@ -299,27 +307,33 @@ func (h *MCPHost) GetEinoToolInfos(ctx context.Context) ([]*schema.ToolInfo, err
|
|||||||
var tools []*schema.ToolInfo
|
var tools []*schema.ToolInfo
|
||||||
for _, serverTools := range results {
|
for _, serverTools := range results {
|
||||||
if serverTools.Err != nil {
|
if serverTools.Err != nil {
|
||||||
log.Error().Err(serverTools.Err).Str("server", serverTools.ServerName).Msg("failed to get tools")
|
log.Error().Err(serverTools.Err).
|
||||||
|
Str("server", serverTools.ServerName).Msg("failed to get tools")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var toolNames []string
|
||||||
for _, tool := range serverTools.Tools {
|
for _, tool := range serverTools.Tools {
|
||||||
einoTool, err := h.GetEinoTool(ctx, serverTools.ServerName, tool.Name)
|
einoTool, err := h.GetEinoTool(ctx, serverTools.ServerName, tool.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Str("server", serverTools.ServerName).Str("tool", tool.Name).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).Str("server", serverTools.ServerName).Str("tool", tool.Name).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
|
||||||
}
|
}
|
||||||
einoToolInfo.Name = fmt.Sprintf("%s__%s", serverTools.ServerName, tool.Name)
|
einoToolInfo.Name = fmt.Sprintf("%s__%s", serverTools.ServerName, tool.Name)
|
||||||
tools = append(tools, einoToolInfo)
|
tools = append(tools, einoToolInfo)
|
||||||
|
toolNames = append(toolNames, tool.Name)
|
||||||
}
|
}
|
||||||
|
log.Debug().Str("server", serverTools.ServerName).
|
||||||
|
Strs("tools", toolNames).Msg("loaded MCP tools")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Int("count", len(tools)).Msg("eino tool infos loaded")
|
|
||||||
return tools, nil
|
return tools, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+50
-5
@@ -13,16 +13,49 @@ import (
|
|||||||
"github.com/httprunner/httprunner/v5/uixt"
|
"github.com/httprunner/httprunner/v5/uixt"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||||
|
"github.com/mark3labs/mcp-go/client"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MCPClient4XTDriver is a minimal MCP client that only implements the methods used by the host
|
||||||
|
type MCPClient4XTDriver struct {
|
||||||
|
client.MCPClient
|
||||||
|
server *MCPServer4XTDriver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MCPClient4XTDriver) ListTools(ctx context.Context, req mcp.ListToolsRequest) (*mcp.ListToolsResult, error) {
|
||||||
|
tools := c.server.ListTools()
|
||||||
|
return &mcp.ListToolsResult{Tools: tools}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MCPClient4XTDriver) CallTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
handler := c.server.GetHandler(req.Params.Name)
|
||||||
|
if handler == nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("handler for tool %s not found", req.Params.Name)), nil
|
||||||
|
}
|
||||||
|
return handler(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MCPClient4XTDriver) Initialize(ctx context.Context, req mcp.InitializeRequest) (*mcp.InitializeResult, error) {
|
||||||
|
// no need to initialize for local server
|
||||||
|
return &mcp.InitializeResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MCPClient4XTDriver) Close() error {
|
||||||
|
// no need to close for local server
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type toolCall = func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error)
|
||||||
|
|
||||||
// MCPServer4XTDriver wraps a MCPServer to expose XTDriver functionality via MCP protocol.
|
// MCPServer4XTDriver wraps a MCPServer to expose XTDriver functionality via MCP protocol.
|
||||||
type MCPServer4XTDriver struct {
|
type MCPServer4XTDriver struct {
|
||||||
mcpServer *server.MCPServer
|
mcpServer *server.MCPServer
|
||||||
driverCache sync.Map // key is serial, value is *XTDriver
|
driverCache sync.Map // key is serial, value is *XTDriver
|
||||||
tools []mcp.Tool // 本地维护的工具列表
|
tools []mcp.Tool // tools list for uixt
|
||||||
|
handlerMap map[string]toolCall // tool name to handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMCPServer creates a new MCP server for XTDriver and registers all tools.
|
// NewMCPServer creates a new MCP server for XTDriver and registers all tools.
|
||||||
@@ -33,7 +66,8 @@ func NewMCPServer() *MCPServer4XTDriver {
|
|||||||
server.WithToolCapabilities(false),
|
server.WithToolCapabilities(false),
|
||||||
)
|
)
|
||||||
s := &MCPServer4XTDriver{
|
s := &MCPServer4XTDriver{
|
||||||
mcpServer: mcpServer,
|
mcpServer: mcpServer,
|
||||||
|
handlerMap: make(map[string]toolCall),
|
||||||
}
|
}
|
||||||
s.addTools()
|
s.addTools()
|
||||||
return s
|
return s
|
||||||
@@ -56,6 +90,7 @@ func (ums *MCPServer4XTDriver) addTools() {
|
|||||||
tapXYTool := mcp.NewTool("tap_xy", tapParams...)
|
tapXYTool := mcp.NewTool("tap_xy", tapParams...)
|
||||||
ums.mcpServer.AddTool(tapXYTool, ums.handleTapXY)
|
ums.mcpServer.AddTool(tapXYTool, ums.handleTapXY)
|
||||||
ums.tools = append(ums.tools, tapXYTool)
|
ums.tools = append(ums.tools, tapXYTool)
|
||||||
|
ums.handlerMap[tapXYTool.Name] = ums.handleTapXY
|
||||||
log.Info().Str("name", tapXYTool.Name).Msg("Register tool")
|
log.Info().Str("name", tapXYTool.Name).Msg("Register tool")
|
||||||
|
|
||||||
// Swipe Tool
|
// Swipe Tool
|
||||||
@@ -67,6 +102,7 @@ func (ums *MCPServer4XTDriver) addTools() {
|
|||||||
swipeTool := mcp.NewTool("swipe", swipeParams...)
|
swipeTool := mcp.NewTool("swipe", swipeParams...)
|
||||||
ums.mcpServer.AddTool(swipeTool, ums.handleSwipe)
|
ums.mcpServer.AddTool(swipeTool, ums.handleSwipe)
|
||||||
ums.tools = append(ums.tools, swipeTool)
|
ums.tools = append(ums.tools, swipeTool)
|
||||||
|
ums.handlerMap[swipeTool.Name] = ums.handleSwipe
|
||||||
log.Info().Str("name", swipeTool.Name).Msg("Register tool")
|
log.Info().Str("name", swipeTool.Name).Msg("Register tool")
|
||||||
|
|
||||||
// ScreenShot Tool
|
// ScreenShot Tool
|
||||||
@@ -75,6 +111,7 @@ func (ums *MCPServer4XTDriver) addTools() {
|
|||||||
)
|
)
|
||||||
ums.mcpServer.AddTool(screenShotTool, ums.handleScreenShot)
|
ums.mcpServer.AddTool(screenShotTool, ums.handleScreenShot)
|
||||||
ums.tools = append(ums.tools, screenShotTool)
|
ums.tools = append(ums.tools, screenShotTool)
|
||||||
|
ums.handlerMap[screenShotTool.Name] = ums.handleScreenShot
|
||||||
log.Info().Str("name", screenShotTool.Name).Msg("Register tool")
|
log.Info().Str("name", screenShotTool.Name).Msg("Register tool")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,12 +296,12 @@ var commonToolOptions = []mcp.ToolOption{
|
|||||||
mcp.WithString("serial", mcp.Required(), mcp.Description("Device serial/udid/browser id")),
|
mcp.WithString("serial", mcp.Required(), mcp.Description("Device serial/udid/browser id")),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListTools 返回所有注册的 mcp.Tool
|
// ListTools returns all registered tools
|
||||||
func (s *MCPServer4XTDriver) ListTools() []mcp.Tool {
|
func (s *MCPServer4XTDriver) ListTools() []mcp.Tool {
|
||||||
return s.tools
|
return s.tools
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTool 根据名称返回 mcp.Tool 指针
|
// GetTool returns a pointer to the mcp.Tool with the given name
|
||||||
func (s *MCPServer4XTDriver) GetTool(name string) *mcp.Tool {
|
func (s *MCPServer4XTDriver) GetTool(name string) *mcp.Tool {
|
||||||
for i := range s.tools {
|
for i := range s.tools {
|
||||||
if s.tools[i].Name == name {
|
if s.tools[i].Name == name {
|
||||||
@@ -273,3 +310,11 @@ func (s *MCPServer4XTDriver) GetTool(name string) *mcp.Tool {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHandler returns the tool handler for the given name
|
||||||
|
func (s *MCPServer4XTDriver) GetHandler(name string) toolCall {
|
||||||
|
if s.handlerMap == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.handlerMap[name]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user