mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-28 02:51:42 +08:00
feat: add argument --with-uixt to start built-in uixt MCP server
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
v5.0.0-beta-2505201803
|
||||
v5.0.0-beta-2505202236
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user