mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-05 23:51:25 +08:00
feat: add argument --with-uixt to start built-in uixt MCP server
This commit is contained in:
+3
-1
@@ -15,7 +15,7 @@ var CmdMCPHost = &cobra.Command{
|
|||||||
Long: `mcphost is a command-line tool that allows you to interact with MCP tools.`,
|
Long: `mcphost is a command-line tool that allows you to interact with MCP tools.`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
// Create MCP host
|
// Create MCP host
|
||||||
host, err := mcphost.NewMCPHost(mcpConfigPath)
|
host, err := mcphost.NewMCPHost(mcpConfigPath, withUIXT)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create MCP host: %w", err)
|
return fmt.Errorf("failed to create MCP host: %w", err)
|
||||||
}
|
}
|
||||||
@@ -40,9 +40,11 @@ var CmdMCPHost = &cobra.Command{
|
|||||||
var (
|
var (
|
||||||
mcpConfigPath string
|
mcpConfigPath string
|
||||||
dumpPath string
|
dumpPath string
|
||||||
|
withUIXT bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
CmdMCPHost.Flags().StringVarP(&mcpConfigPath, "mcp-config", "c", "$HOME/.hrp/mcp.json", "path to the MCP config file")
|
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().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) {
|
func TestRunPromptWithNoToolCall(t *testing.T) {
|
||||||
host, err := NewMCPHost("./testdata/test.mcp.json")
|
host, err := NewMCPHost("./testdata/test.mcp.json", true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
chat, err := host.NewChat(context.Background())
|
chat, err := host.NewChat(context.Background())
|
||||||
@@ -21,7 +21,7 @@ func TestRunPromptWithNoToolCall(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunPromptWithToolCall(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
chat, err := host.NewChat(context.Background())
|
chat, err := host.NewChat(context.Background())
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestConvertToolsToRecordsFromFile(t *testing.T) {
|
func TestConvertToolsToRecordsFromFile(t *testing.T) {
|
||||||
hub, err := NewMCPHost("./testdata/test.mcp.json")
|
hub, err := NewMCPHost("./testdata/test.mcp.json", true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// use ExportToolsToJSON to dump tools to JSON file
|
// use ExportToolsToJSON to dump tools to JSON file
|
||||||
|
|||||||
+40
-6
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/cloudwego/eino/components/tool"
|
"github.com/cloudwego/eino/components/tool"
|
||||||
"github.com/cloudwego/eino/schema"
|
"github.com/cloudwego/eino/schema"
|
||||||
"github.com/httprunner/httprunner/v5/internal/version"
|
"github.com/httprunner/httprunner/v5/internal/version"
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt"
|
||||||
"github.com/mark3labs/mcp-go/client"
|
"github.com/mark3labs/mcp-go/client"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -24,6 +25,8 @@ type MCPHost struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
connections map[string]*Connection
|
connections map[string]*Connection
|
||||||
config *MCPConfig
|
config *MCPConfig
|
||||||
|
withUIXT bool
|
||||||
|
drivers map[string]*uixt.XTDriver
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection represents a connection to an MCP server
|
// Connection represents a connection to an MCP server
|
||||||
@@ -40,7 +43,7 @@ type MCPTools struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewMCPHost creates a new MCPHost instance
|
// NewMCPHost creates a new MCPHost instance
|
||||||
func NewMCPHost(configPath string) (*MCPHost, error) {
|
func NewMCPHost(configPath string, withUIXT bool) (*MCPHost, error) {
|
||||||
config, err := LoadMCPConfig(configPath)
|
config, err := LoadMCPConfig(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -49,6 +52,8 @@ func NewMCPHost(configPath string) (*MCPHost, error) {
|
|||||||
host := &MCPHost{
|
host := &MCPHost{
|
||||||
connections: make(map[string]*Connection),
|
connections: make(map[string]*Connection),
|
||||||
config: config,
|
config: config,
|
||||||
|
drivers: make(map[string]*uixt.XTDriver),
|
||||||
|
withUIXT: withUIXT,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize MCP servers
|
// Initialize MCP servers
|
||||||
@@ -62,11 +67,13 @@ 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
|
// initialize uixt MCP server
|
||||||
h.connections["uixt"] = &Connection{
|
if h.withUIXT {
|
||||||
Client: &MCPClient4XTDriver{
|
h.connections["uixt"] = &Connection{
|
||||||
server: NewMCPServer(),
|
Client: &MCPClient4XTDriver{
|
||||||
},
|
server: NewMCPServer(),
|
||||||
Config: nil,
|
},
|
||||||
|
Config: nil,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, server := range h.config.MCPServers {
|
for name, server := range h.config.MCPServers {
|
||||||
@@ -387,3 +394,30 @@ func handleToolError(result *mcp.CallToolResult) error {
|
|||||||
}
|
}
|
||||||
return fmt.Errorf("tool error: unknown 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
|
||||||
|
}
|
||||||
|
|||||||
+11
-11
@@ -12,20 +12,20 @@ import (
|
|||||||
|
|
||||||
func TestNewMCPHost(t *testing.T) {
|
func TestNewMCPHost(t *testing.T) {
|
||||||
// Test with valid config file
|
// Test with valid config file
|
||||||
host, err := NewMCPHost("./testdata/test.mcp.json")
|
host, err := NewMCPHost("./testdata/test.mcp.json", false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotNil(t, host)
|
assert.NotNil(t, host)
|
||||||
assert.NotNil(t, host.config)
|
assert.NotNil(t, host.config)
|
||||||
assert.NotEmpty(t, host.config.MCPServers)
|
assert.NotEmpty(t, host.config.MCPServers)
|
||||||
|
|
||||||
// Test with non-existent config file
|
// 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")
|
require.Error(t, err, "expected error when config file does not exist")
|
||||||
assert.Nil(t, host)
|
assert.Nil(t, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitServers(t *testing.T) {
|
func TestInitServers(t *testing.T) {
|
||||||
host, err := NewMCPHost("./testdata/test.mcp.json")
|
host, err := NewMCPHost("./testdata/test.mcp.json", false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify connections are established
|
// Verify connections are established
|
||||||
@@ -35,7 +35,7 @@ func TestInitServers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetClient(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test getting existing client
|
// Test getting existing client
|
||||||
@@ -50,7 +50,7 @@ func TestGetClient(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetTools(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -81,7 +81,7 @@ func TestGetTools(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetTool(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -104,7 +104,7 @@ func TestGetTool(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInvokeTool(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -132,7 +132,7 @@ func TestInvokeTool(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCallEinoTool(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -147,7 +147,7 @@ func TestCallEinoTool(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCloseServers(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify servers are connected
|
// Verify servers are connected
|
||||||
@@ -162,7 +162,7 @@ func TestCloseServers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConcurrentOperations(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test concurrent tool invocations
|
// Test concurrent tool invocations
|
||||||
@@ -193,7 +193,7 @@ func TestConcurrentOperations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDisabledServer(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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify only enabled servers are connected
|
// Verify only enabled servers are connected
|
||||||
|
|||||||
+21
-4
@@ -126,7 +126,8 @@ func (ums *MCPServer4XTDriver) handleTapXY(ctx context.Context, request mcp.Call
|
|||||||
return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil
|
return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil
|
||||||
}
|
}
|
||||||
if tapReq.Duration > 0 {
|
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 {
|
if err != nil {
|
||||||
return mcp.NewToolResultError("Tap failed: " + err.Error()), 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.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.
|
// handleSwipe handles the swipe tool call.
|
||||||
@@ -153,11 +156,15 @@ func (ums *MCPServer4XTDriver) handleSwipe(ctx context.Context, request mcp.Call
|
|||||||
if swipeReq.Duration > 0 {
|
if swipeReq.Duration > 0 {
|
||||||
actionOptions = append(actionOptions, option.WithDuration(swipeReq.Duration/1000.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 {
|
if err != nil {
|
||||||
return mcp.NewToolResultError("Swipe failed: " + err.Error()), 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.
|
// 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
|
// init device
|
||||||
var device uixt.IDevice
|
var device uixt.IDevice
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ func NewCaseRunner(testcase TestCase, hrpRunner *HRPRunner) (*CaseRunner, error)
|
|||||||
|
|
||||||
// init MCP servers
|
// init MCP servers
|
||||||
if config.MCPConfigPath != "" {
|
if config.MCPConfigPath != "" {
|
||||||
mcpHost, err := mcphost.NewMCPHost(config.MCPConfigPath)
|
mcpHost, err := mcphost.NewMCPHost(config.MCPConfigPath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("init MCP hub failed")
|
log.Error().Err(err).Msg("init MCP hub failed")
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ type Router struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) InitMCPHost(configPath string) error {
|
func (r *Router) InitMCPHost(configPath string) error {
|
||||||
mcpHost, err := mcphost.NewMCPHost(configPath)
|
mcpHost, err := mcphost.NewMCPHost(configPath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("init MCP host failed")
|
log.Error().Err(err).Msg("init MCP host failed")
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ func (dExt *XTDriver) AIAssert(assertion string, opts ...option.ActionOption) er
|
|||||||
return errors.New("LLM service is not initialized")
|
return errors.New("LLM service is not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
screenShotBase64, err := getScreenShotBufferBase64(dExt.IDriver)
|
screenShotBase64, err := GetScreenShotBufferBase64(dExt.IDriver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,8 +222,8 @@ func getScreenShotBuffer(driver IDriver) (compressedBufSource *bytes.Buffer, err
|
|||||||
return compressBufSource, nil
|
return compressBufSource, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getScreenShotBufferBase64 takes a screenshot, returns the compressed image buffer in base64 format
|
// GetScreenShotBufferBase64 takes a screenshot, returns the compressed image buffer in base64 format
|
||||||
func getScreenShotBufferBase64(driver IDriver) (compressedBufBase64 string, err error) {
|
func GetScreenShotBufferBase64(driver IDriver) (compressedBufBase64 string, err error) {
|
||||||
compressBufSource, err := getScreenShotBuffer(driver)
|
compressBufSource, err := getScreenShotBuffer(driver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
Reference in New Issue
Block a user