feat: add argument --with-uixt to start built-in uixt MCP server

This commit is contained in:
lilong.129
2025-05-20 22:36:46 +08:00
parent 037e69315e
commit 0c20fe7b02
11 changed files with 84 additions and 31 deletions

View File

@@ -15,7 +15,7 @@ var CmdMCPHost = &cobra.Command{
Long: `mcphost is a command-line tool that allows you to interact with MCP tools.`,
RunE: func(cmd *cobra.Command, args []string) error {
// Create MCP host
host, err := mcphost.NewMCPHost(mcpConfigPath)
host, err := mcphost.NewMCPHost(mcpConfigPath, withUIXT)
if err != nil {
return fmt.Errorf("failed to create MCP host: %w", err)
}
@@ -40,9 +40,11 @@ var CmdMCPHost = &cobra.Command{
var (
mcpConfigPath string
dumpPath string
withUIXT bool
)
func init() {
CmdMCPHost.Flags().StringVarP(&mcpConfigPath, "mcp-config", "c", "$HOME/.hrp/mcp.json", "path to the MCP config file")
CmdMCPHost.Flags().StringVar(&dumpPath, "dump", "", "path to save the exported tools JSON file")
CmdMCPHost.Flags().BoolVar(&withUIXT, "with-uixt", false, "start built-in uixt MCP server")
}

View File

@@ -1 +1 @@
v5.0.0-beta-2505201803
v5.0.0-beta-2505202236

View File

@@ -9,7 +9,7 @@ import (
)
func TestRunPromptWithNoToolCall(t *testing.T) {
host, err := NewMCPHost("./testdata/test.mcp.json")
host, err := NewMCPHost("./testdata/test.mcp.json", true)
require.NoError(t, err)
chat, err := host.NewChat(context.Background())
@@ -21,7 +21,7 @@ func TestRunPromptWithNoToolCall(t *testing.T) {
}
func TestRunPromptWithToolCall(t *testing.T) {
host, err := NewMCPHost("./testdata/test.mcp.json")
host, err := NewMCPHost("./testdata/test.mcp.json", true)
require.NoError(t, err)
chat, err := host.NewChat(context.Background())

View File

@@ -13,7 +13,7 @@ import (
)
func TestConvertToolsToRecordsFromFile(t *testing.T) {
hub, err := NewMCPHost("./testdata/test.mcp.json")
hub, err := NewMCPHost("./testdata/test.mcp.json", true)
require.NoError(t, err)
// use ExportToolsToJSON to dump tools to JSON file

View File

@@ -13,6 +13,7 @@ import (
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/httprunner/httprunner/v5/internal/version"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
"github.com/pkg/errors"
@@ -24,6 +25,8 @@ type MCPHost struct {
mu sync.RWMutex
connections map[string]*Connection
config *MCPConfig
withUIXT bool
drivers map[string]*uixt.XTDriver
}
// Connection represents a connection to an MCP server
@@ -40,7 +43,7 @@ type MCPTools struct {
}
// NewMCPHost creates a new MCPHost instance
func NewMCPHost(configPath string) (*MCPHost, error) {
func NewMCPHost(configPath string, withUIXT bool) (*MCPHost, error) {
config, err := LoadMCPConfig(configPath)
if err != nil {
return nil, err
@@ -49,6 +52,8 @@ func NewMCPHost(configPath string) (*MCPHost, error) {
host := &MCPHost{
connections: make(map[string]*Connection),
config: config,
drivers: make(map[string]*uixt.XTDriver),
withUIXT: withUIXT,
}
// Initialize MCP servers
@@ -62,11 +67,13 @@ func NewMCPHost(configPath string) (*MCPHost, error) {
// InitServers initializes all MCP servers
func (h *MCPHost) InitServers(ctx context.Context) error {
// initialize uixt MCP server
h.connections["uixt"] = &Connection{
Client: &MCPClient4XTDriver{
server: NewMCPServer(),
},
Config: nil,
if h.withUIXT {
h.connections["uixt"] = &Connection{
Client: &MCPClient4XTDriver{
server: NewMCPServer(),
},
Config: nil,
}
}
for name, server := range h.config.MCPServers {
@@ -387,3 +394,30 @@ func handleToolError(result *mcp.CallToolResult) error {
}
return fmt.Errorf("tool error: unknown error")
}
// ScreenshotBase64 get screenshot base64 for the given platform and serial
func (h *MCPHost) ScreenshotBase64(ctx context.Context, platform, serial string) (string, error) {
driver, err := h.GetOrCreateDriver(platform, serial)
if err != nil {
return "", err
}
return uixt.GetScreenShotBufferBase64(driver)
}
// GetOrCreateDriver get or create a driver for the given platform and serial
func (h *MCPHost) GetOrCreateDriver(platform, serial string) (*uixt.XTDriver, error) {
h.mu.Lock()
defer h.mu.Unlock()
cacheKey := fmt.Sprintf("%s_%s", platform, serial)
if driver, ok := h.drivers[cacheKey]; ok {
return driver, nil
}
driverExt, err := initDriverExt(platform, serial)
if err != nil {
return nil, err
}
// store driver in cache
h.drivers[cacheKey] = driverExt
return driverExt, nil
}

View File

@@ -12,20 +12,20 @@ import (
func TestNewMCPHost(t *testing.T) {
// Test with valid config file
host, err := NewMCPHost("./testdata/test.mcp.json")
host, err := NewMCPHost("./testdata/test.mcp.json", false)
require.NoError(t, err)
assert.NotNil(t, host)
assert.NotNil(t, host.config)
assert.NotEmpty(t, host.config.MCPServers)
// Test with non-existent config file
host, err = NewMCPHost("./testdata/non_existent.json")
host, err = NewMCPHost("./testdata/non_existent.json", false)
require.Error(t, err, "expected error when config file does not exist")
assert.Nil(t, host)
}
func TestInitServers(t *testing.T) {
host, err := NewMCPHost("./testdata/test.mcp.json")
host, err := NewMCPHost("./testdata/test.mcp.json", false)
require.NoError(t, err)
// Verify connections are established
@@ -35,7 +35,7 @@ func TestInitServers(t *testing.T) {
}
func TestGetClient(t *testing.T) {
host, err := NewMCPHost("./testdata/test.mcp.json")
host, err := NewMCPHost("./testdata/test.mcp.json", false)
require.NoError(t, err)
// Test getting existing client
@@ -50,7 +50,7 @@ func TestGetClient(t *testing.T) {
}
func TestGetTools(t *testing.T) {
host, err := NewMCPHost("./testdata/test.mcp.json")
host, err := NewMCPHost("./testdata/test.mcp.json", false)
require.NoError(t, err)
ctx := context.Background()
@@ -81,7 +81,7 @@ func TestGetTools(t *testing.T) {
}
func TestGetTool(t *testing.T) {
host, err := NewMCPHost("./testdata/test.mcp.json")
host, err := NewMCPHost("./testdata/test.mcp.json", false)
require.NoError(t, err)
ctx := context.Background()
@@ -104,7 +104,7 @@ func TestGetTool(t *testing.T) {
}
func TestInvokeTool(t *testing.T) {
host, err := NewMCPHost("./testdata/test.mcp.json")
host, err := NewMCPHost("./testdata/test.mcp.json", false)
require.NoError(t, err)
ctx := context.Background()
@@ -132,7 +132,7 @@ func TestInvokeTool(t *testing.T) {
}
func TestCallEinoTool(t *testing.T) {
hub, err := NewMCPHost("./testdata/test.mcp.json")
hub, err := NewMCPHost("./testdata/test.mcp.json", false)
require.NoError(t, err)
ctx := context.Background()
@@ -147,7 +147,7 @@ func TestCallEinoTool(t *testing.T) {
}
func TestCloseServers(t *testing.T) {
host, err := NewMCPHost("./testdata/test.mcp.json")
host, err := NewMCPHost("./testdata/test.mcp.json", false)
require.NoError(t, err)
// Verify servers are connected
@@ -162,7 +162,7 @@ func TestCloseServers(t *testing.T) {
}
func TestConcurrentOperations(t *testing.T) {
host, err := NewMCPHost("./testdata/test.mcp.json")
host, err := NewMCPHost("./testdata/test.mcp.json", false)
require.NoError(t, err)
// Test concurrent tool invocations
@@ -193,7 +193,7 @@ func TestConcurrentOperations(t *testing.T) {
}
func TestDisabledServer(t *testing.T) {
host, err := NewMCPHost("./testdata/test.mcp.json")
host, err := NewMCPHost("./testdata/test.mcp.json", false)
require.NoError(t, err)
// Verify only enabled servers are connected

View File

@@ -126,7 +126,8 @@ func (ums *MCPServer4XTDriver) handleTapXY(ctx context.Context, request mcp.Call
return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil
}
if tapReq.Duration > 0 {
err := driverExt.Drag(tapReq.X, tapReq.Y, tapReq.X, tapReq.Y, option.WithDuration(tapReq.Duration))
err := driverExt.Drag(tapReq.X, tapReq.Y, tapReq.X, tapReq.Y,
option.WithDuration(tapReq.Duration))
if err != nil {
return mcp.NewToolResultError("Tap failed: " + err.Error()), nil
}
@@ -136,7 +137,9 @@ func (ums *MCPServer4XTDriver) handleTapXY(ctx context.Context, request mcp.Call
return mcp.NewToolResultError("Tap failed: " + err.Error()), nil
}
}
return mcp.NewToolResultText("Tap successful."), nil
return mcp.NewToolResultText(
fmt.Sprintf("tap (%f,%f) success", tapReq.X, tapReq.Y),
), nil
}
// handleSwipe handles the swipe tool call.
@@ -153,11 +156,15 @@ func (ums *MCPServer4XTDriver) handleSwipe(ctx context.Context, request mcp.Call
if swipeReq.Duration > 0 {
actionOptions = append(actionOptions, option.WithDuration(swipeReq.Duration/1000.0))
}
err = driverExt.Swipe(swipeReq.FromX, swipeReq.FromY, swipeReq.ToX, swipeReq.ToY, actionOptions...)
err = driverExt.Swipe(swipeReq.FromX, swipeReq.FromY,
swipeReq.ToX, swipeReq.ToY, actionOptions...)
if err != nil {
return mcp.NewToolResultError("Swipe failed: " + err.Error()), nil
}
return mcp.NewToolResultText("Swipe successful."), nil
return mcp.NewToolResultText(
fmt.Sprintf("swipe (%f,%f)->(%f,%f) success",
swipeReq.FromX, swipeReq.FromY, swipeReq.ToX, swipeReq.ToY),
), nil
}
// handleScreenShot handles the screenshot tool call.
@@ -198,6 +205,16 @@ func (ums *MCPServer4XTDriver) setupXTDriver(_ context.Context, args map[string]
}
}
driverExt, err := initDriverExt(platform, serial)
if err != nil {
return nil, err
}
// store driver in cache
ums.driverCache.Store(cacheKey, driverExt)
return driverExt, nil
}
func initDriverExt(platform, serial string) (*uixt.XTDriver, error) {
// init device
var device uixt.IDevice
var err error

View File

@@ -318,7 +318,7 @@ func NewCaseRunner(testcase TestCase, hrpRunner *HRPRunner) (*CaseRunner, error)
// init MCP servers
if config.MCPConfigPath != "" {
mcpHost, err := mcphost.NewMCPHost(config.MCPConfigPath)
mcpHost, err := mcphost.NewMCPHost(config.MCPConfigPath, false)
if err != nil {
log.Error().Err(err).Msg("init MCP hub failed")
return nil, err

View File

@@ -25,7 +25,7 @@ type Router struct {
}
func (r *Router) InitMCPHost(configPath string) error {
mcpHost, err := mcphost.NewMCPHost(configPath)
mcpHost, err := mcphost.NewMCPHost(configPath, false)
if err != nil {
log.Error().Err(err).Msg("init MCP host failed")
return err

View File

@@ -119,7 +119,7 @@ func (dExt *XTDriver) AIAssert(assertion string, opts ...option.ActionOption) er
return errors.New("LLM service is not initialized")
}
screenShotBase64, err := getScreenShotBufferBase64(dExt.IDriver)
screenShotBase64, err := GetScreenShotBufferBase64(dExt.IDriver)
if err != nil {
return err
}

View File

@@ -222,8 +222,8 @@ func getScreenShotBuffer(driver IDriver) (compressedBufSource *bytes.Buffer, err
return compressBufSource, nil
}
// getScreenShotBufferBase64 takes a screenshot, returns the compressed image buffer in base64 format
func getScreenShotBufferBase64(driver IDriver) (compressedBufBase64 string, err error) {
// GetScreenShotBufferBase64 takes a screenshot, returns the compressed image buffer in base64 format
func GetScreenShotBufferBase64(driver IDriver) (compressedBufBase64 string, err error) {
compressBufSource, err := getScreenShotBuffer(driver)
if err != nil {
return "", err