refactor: move DoAction to MCP tools call

This commit is contained in:
lilong.129
2025-05-25 08:10:57 +08:00
parent 4ff2692f02
commit 7986c4899f
8 changed files with 2220 additions and 359 deletions

View File

@@ -1,16 +1,6 @@
package uixt
import (
"encoding/json"
"fmt"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/uixt/option"
)
@@ -31,261 +21,3 @@ func (ma MobileAction) GetOptions() []option.ActionOption {
actionOptionList = append(actionOptionList, ma.ActionOptions.Options()...)
return actionOptionList
}
// TODO: merge to uixt MCP Server
func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
actionStartTime := time.Now()
defer func() {
var logger *zerolog.Event
if err != nil {
logger = log.Error().Bool("success", false).Err(err)
} else {
logger = log.Debug().Bool("success", true)
}
logger = logger.
Str("method", string(action.Method)).
Interface("params", action.Params).
Int64("elapsed(ms)", time.Since(actionStartTime).Milliseconds())
logger.Msg("exec uixt action")
}()
switch action.Method {
case option.ACTION_WebLoginNoneUI:
if len(action.Params.([]interface{})) == 4 {
driver, ok := dExt.IDriver.(*BrowserDriver)
if !ok {
return errors.New("invalid browser driver")
}
params := action.Params.([]interface{})
_, err = driver.LoginNoneUI(params[0].(string), params[1].(string), params[2].(string), params[3].(string))
return err
}
return fmt.Errorf("invalid %s params: %v", option.ACTION_WebLoginNoneUI, action.Params)
case option.ACTION_AppInstall:
if app, ok := action.Params.(string); ok {
if err = dExt.GetDevice().Install(app,
option.WithRetryTimes(action.MaxRetryTimes)); err != nil {
return errors.Wrap(err, "failed to install app")
}
}
case option.ACTION_AppUninstall:
if packageName, ok := action.Params.(string); ok {
if err = dExt.GetDevice().Uninstall(packageName); err != nil {
return errors.Wrap(err, "failed to uninstall app")
}
}
case option.ACTION_AppClear:
if packageName, ok := action.Params.(string); ok {
if err = dExt.AppClear(packageName); err != nil {
return errors.Wrap(err, "failed to clear app")
}
}
case option.ACTION_AppLaunch:
if bundleId, ok := action.Params.(string); ok {
return dExt.AppLaunch(bundleId)
}
return fmt.Errorf("invalid %s params, should be bundleId(string), got %v",
option.ACTION_AppLaunch, action.Params)
case option.ACTION_SwipeToTapApp:
if appName, ok := action.Params.(string); ok {
return dExt.SwipeToTapApp(appName, action.GetOptions()...)
}
return fmt.Errorf("invalid %s params, should be app name(string), got %v",
option.ACTION_SwipeToTapApp, action.Params)
case option.ACTION_SwipeToTapText:
if text, ok := action.Params.(string); ok {
return dExt.SwipeToTapTexts([]string{text}, action.GetOptions()...)
}
return fmt.Errorf("invalid %s params, should be app text(string), got %v",
option.ACTION_SwipeToTapText, action.Params)
case option.ACTION_SwipeToTapTexts:
if texts, ok := action.Params.([]string); ok {
return dExt.SwipeToTapTexts(texts, action.GetOptions()...)
}
if texts, err := builtin.ConvertToStringSlice(action.Params); err == nil {
return dExt.SwipeToTapTexts(texts, action.GetOptions()...)
}
return fmt.Errorf("invalid %s params: %v", option.ACTION_SwipeToTapTexts, action.Params)
case option.ACTION_AppTerminate:
if bundleId, ok := action.Params.(string); ok {
success, err := dExt.AppTerminate(bundleId)
if err != nil {
return errors.Wrap(err, "failed to terminate app")
}
if !success {
log.Warn().Str("bundleId", bundleId).Msg("app was not running")
}
return nil
}
return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params)
case option.ACTION_Home:
return dExt.Home()
case option.ACTION_SecondaryClick:
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
if len(params) != 2 {
return fmt.Errorf("invalid tap location params: %v", params)
}
x, y := params[0], params[1]
return dExt.SecondaryClick(x, y)
}
return fmt.Errorf("invalid %s params: %v", option.ACTION_SecondaryClick, action.Params)
case option.ACTION_HoverBySelector:
if selector, ok := action.Params.(string); ok {
return dExt.HoverBySelector(selector, action.GetOptions()...)
}
return fmt.Errorf("invalid %s params: %v", option.ACTION_HoverBySelector, action.Params)
case option.ACTION_TapBySelector:
if selector, ok := action.Params.(string); ok {
return dExt.TapBySelector(selector, action.GetOptions()...)
}
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapBySelector, action.Params)
case option.ACTION_SecondaryClickBySelector:
if selector, ok := action.Params.(string); ok {
return dExt.SecondaryClickBySelector(selector, action.GetOptions()...)
}
return fmt.Errorf("invalid %s params: %v", option.ACTION_SecondaryClickBySelector, action.Params)
case option.ACTION_WebCloseTab:
if param, ok := action.Params.(json.Number); ok {
paramInt64, _ := param.Int64()
return dExt.IDriver.(*BrowserDriver).CloseTab(int(paramInt64))
} else if param, ok := action.Params.(int64); ok {
return dExt.IDriver.(*BrowserDriver).CloseTab(int(param))
} else {
return dExt.IDriver.(*BrowserDriver).CloseTab(action.Params.(int))
}
// return fmt.Errorf("invalid %s params: %v", ACTION_WebCloseTab, action.Params)
case option.ACTION_SetIme:
if ime, ok := action.Params.(string); ok {
err = dExt.SetIme(ime)
if err != nil {
return errors.Wrap(err, "failed to set ime")
}
return nil
}
case option.ACTION_GetSource:
if packageName, ok := action.Params.(string); ok {
_, err = dExt.Source(option.WithProcessName(packageName))
if err != nil {
return errors.Wrap(err, "failed to set ime")
}
return nil
}
case option.ACTION_TapXY:
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
// relative x,y of window size: [0.5, 0.5]
if len(params) != 2 {
return fmt.Errorf("invalid tap location params: %v", params)
}
x, y := params[0], params[1]
return dExt.TapXY(x, y, action.GetOptions()...)
}
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapXY, action.Params)
case option.ACTION_TapAbsXY:
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
// absolute coordinates x,y of window size: [100, 300]
if len(params) != 2 {
return fmt.Errorf("invalid tap location params: %v", params)
}
x, y := params[0], params[1]
return dExt.TapAbsXY(x, y, action.GetOptions()...)
}
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapAbsXY, action.Params)
case option.ACTION_TapByOCR:
if ocrText, ok := action.Params.(string); ok {
return dExt.TapByOCR(ocrText, action.GetOptions()...)
}
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapByOCR, action.Params)
case option.ACTION_TapByCV:
actionOptions := option.NewActionOptions(action.GetOptions()...)
if len(actionOptions.ScreenShotWithUITypes) > 0 {
return dExt.TapByCV(action.GetOptions()...)
}
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapByCV, action.Params)
case option.ACTION_DoubleTapXY:
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
// relative x,y of window size: [0.5, 0.5]
if len(params) != 2 {
return fmt.Errorf("invalid tap location params: %v", params)
}
x, y := params[0], params[1]
return dExt.DoubleTap(x, y)
}
return fmt.Errorf("invalid %s params: %v", option.ACTION_DoubleTapXY, action.Params)
case option.ACTION_Swipe:
params := action.Params
swipeAction := prepareSwipeAction(dExt, params, action.GetOptions()...)
return swipeAction(dExt)
case option.ACTION_Input:
// input text on current active element
// append \n to send text with enter
// send \b\b\b to delete 3 chars
param := fmt.Sprintf("%v", action.Params)
return dExt.Input(param)
case option.ACTION_Back:
return dExt.Back()
case option.ACTION_Sleep:
if param, ok := action.Params.(json.Number); ok {
seconds, _ := param.Float64()
time.Sleep(time.Duration(seconds*1000) * time.Millisecond)
return nil
} else if param, ok := action.Params.(float64); ok {
time.Sleep(time.Duration(param*1000) * time.Millisecond)
return nil
} else if param, ok := action.Params.(int64); ok {
time.Sleep(time.Duration(param) * time.Second)
return nil
} else if sd, ok := action.Params.(SleepConfig); ok {
sleepStrict(sd.StartTime, int64(sd.Seconds*1000))
return nil
} else if param, ok := action.Params.(string); ok {
seconds, err := builtin.ConvertToFloat64(param)
if err != nil {
return errors.Wrapf(err, "invalid sleep params: %v(%T)", action.Params, action.Params)
}
time.Sleep(time.Duration(seconds*1000) * time.Millisecond)
return nil
}
return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params)
case option.ACTION_SleepMS:
if param, ok := action.Params.(json.Number); ok {
milliseconds, _ := param.Int64()
time.Sleep(time.Duration(milliseconds) * time.Millisecond)
return nil
} else if param, ok := action.Params.(int64); ok {
time.Sleep(time.Duration(param) * time.Millisecond)
return nil
} else if sd, ok := action.Params.(SleepConfig); ok {
sleepStrict(sd.StartTime, sd.Milliseconds)
return nil
}
return fmt.Errorf("invalid sleep ms params: %v(%T)", action.Params, action.Params)
case option.ACTION_SleepRandom:
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
sleepStrict(time.Now(), getSimulationDuration(params))
return nil
}
return fmt.Errorf("invalid sleep random params: %v(%T)", action.Params, action.Params)
case option.ACTION_ScreenShot:
// take screenshot
log.Info().Msg("take screenshot for current screen")
_, err := dExt.GetScreenResult(action.GetScreenShotOptions()...)
return err
case option.ACTION_ClosePopups:
return dExt.ClosePopupsHandler()
case option.ACTION_CallFunction:
if funcDesc, ok := action.Params.(string); ok {
return dExt.Call(funcDesc, action.Fn, action.GetOptions()...)
}
return fmt.Errorf("invalid function description: %v", action.Params)
case option.ACTION_AIAction:
if prompt, ok := action.Params.(string); ok {
return dExt.AIAction(prompt, action.GetOptions()...)
}
return fmt.Errorf("invalid %s params: %v", option.ACTION_AIAction, action.Params)
default:
log.Warn().Str("action", string(action.Method)).Msg("action not implemented")
return errors.Wrapf(code.InvalidCaseError,
"UI action %v not implemented", action.Method)
}
return nil
}

File diff suppressed because it is too large Load Diff

72
uixt/mcp_server_test.go Normal file
View File

@@ -0,0 +1,72 @@
package uixt
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewMCPServer(t *testing.T) {
server := NewMCPServer()
assert.NotNil(t, server)
// Check that tools are registered
tools := server.ListTools()
assert.Greater(t, len(tools), 0, "Should have at least one tool registered")
// Check specific tools exist
expectedTools := []string{
"list_available_devices",
"select_device",
"list_packages",
"launch_app",
"terminate_app",
"get_screen_size",
"press_button",
"tap_xy",
"swipe",
"drag",
"screenshot",
"home",
"back",
"input",
"sleep",
}
registeredToolNames := make(map[string]bool)
for _, tool := range tools {
registeredToolNames[tool.Name] = true
}
for _, expectedTool := range expectedTools {
assert.True(t, registeredToolNames[expectedTool], "Tool %s should be registered", expectedTool)
}
}
func TestToolInterfaces(t *testing.T) {
// Test that all tools implement the ActionTool interface correctly
tools := []ActionTool{
&ToolListAvailableDevices{},
&ToolSelectDevice{},
&ToolListPackages{},
&ToolLaunchApp{},
&ToolTerminateApp{},
&ToolGetScreenSize{},
&ToolPressButton{},
&ToolTapXY{},
&ToolSwipe{},
&ToolDrag{},
&ToolScreenShot{},
&ToolHome{},
&ToolBack{},
&ToolInput{},
&ToolSleep{},
}
for _, tool := range tools {
assert.NotEmpty(t, tool.Name(), "Tool name should not be empty")
assert.NotEmpty(t, tool.Description(), "Tool description should not be empty")
assert.NotNil(t, tool.Options(), "Tool options should not be nil")
assert.NotNil(t, tool.Implement(), "Tool implementation should not be nil")
}
}

View File

@@ -92,10 +92,125 @@ type PressButtonRequest struct {
Button types.DeviceButton `json:"button" binding:"required" desc:"The button to press. Supported buttons: BACK (android only), HOME, VOLUME_UP, VOLUME_DOWN, ENTER."`
}
// Additional requests for missing actions
type WebLoginNoneUIRequest struct {
TargetDeviceRequest
PackageName string `json:"packageName" binding:"required" desc:"Package name for the app to login"`
PhoneNumber string `json:"phoneNumber" binding:"required" desc:"Phone number for login"`
Captcha string `json:"captcha" binding:"required" desc:"Captcha code"`
Password string `json:"password" binding:"required" desc:"Password for login"`
}
type SwipeToTapAppRequest struct {
TargetDeviceRequest
AppName string `json:"appName" binding:"required" desc:"App name to find and tap"`
}
type SwipeToTapTextRequest struct {
TargetDeviceRequest
Text string `json:"text" binding:"required" desc:"Text to find and tap"`
}
type SwipeToTapTextsRequest struct {
TargetDeviceRequest
Texts []string `json:"texts" binding:"required" desc:"List of texts to find and tap"`
}
type SecondaryClickRequest struct {
TargetDeviceRequest
X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
}
type SelectorRequest struct {
TargetDeviceRequest
Selector string `json:"selector" binding:"required" desc:"CSS or XPath selector"`
}
type WebCloseTabRequest struct {
TargetDeviceRequest
TabIndex int `json:"tabIndex" binding:"required" desc:"Index of the tab to close"`
}
type SetImeRequest struct {
TargetDeviceRequest
Ime string `json:"ime" binding:"required" desc:"IME package name to set"`
}
type GetSourceRequest struct {
TargetDeviceRequest
PackageName string `json:"packageName" binding:"required" desc:"Package name to get source from"`
}
type TapAbsXYRequest struct {
TargetDeviceRequest
X float64 `json:"x" binding:"required" desc:"Absolute X coordinate in pixels"`
Y float64 `json:"y" binding:"required" desc:"Absolute Y coordinate in pixels"`
Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"`
}
type TapByOCRRequest struct {
TargetDeviceRequest
Text string `json:"text" binding:"required" desc:"OCR text to find and tap"`
}
type TapByCVRequest struct {
TargetDeviceRequest
ImagePath string `json:"imagePath" desc:"Path to reference image for CV recognition"`
}
type DoubleTapXYRequest struct {
TargetDeviceRequest
X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
}
type SwipeAdvancedRequest struct {
TargetDeviceRequest
FromX float64 `json:"fromX" binding:"required" desc:"Starting X coordinate"`
FromY float64 `json:"fromY" binding:"required" desc:"Starting Y coordinate"`
ToX float64 `json:"toX" binding:"required" desc:"Ending X coordinate"`
ToY float64 `json:"toY" binding:"required" desc:"Ending Y coordinate"`
Duration float64 `json:"duration" desc:"Swipe duration in seconds (optional)"`
PressDuration float64 `json:"pressDuration" desc:"Press duration in seconds (optional)"`
}
type SleepMSRequest struct {
TargetDeviceRequest
Milliseconds int64 `json:"milliseconds" binding:"required" desc:"Sleep duration in milliseconds"`
}
type SleepRandomRequest struct {
TargetDeviceRequest
Params []float64 `json:"params" binding:"required" desc:"Random sleep parameters [min, max] or [min1, max1, weight1, ...]"`
}
type CallFunctionRequest struct {
TargetDeviceRequest
Description string `json:"description" binding:"required" desc:"Function description"`
}
type AIActionRequest struct {
TargetDeviceRequest
Prompt string `json:"prompt" binding:"required" desc:"AI action prompt"`
}
// NewMCPOptions generates mcp.NewTool parameters from a struct type.
// It automatically generates mcp.NewTool parameters based on the struct fields and their desc tags.
func NewMCPOptions(t interface{}) (options []mcp.ToolOption) {
tType := reflect.TypeOf(t)
// Handle pointer type by getting the element type
if tType.Kind() == reflect.Ptr {
tType = tType.Elem()
}
// Ensure we have a struct type
if tType.Kind() != reflect.Struct {
log.Warn().Str("type", tType.String()).Msg("NewMCPOptions expects a struct or pointer to struct")
return options
}
for i := 0; i < tType.NumField(); i++ {
field := tType.Field(i)
jsonTag := field.Tag.Get("json")
@@ -125,6 +240,23 @@ func NewMCPOptions(t interface{}) (options []mcp.ToolOption) {
} else {
options = append(options, mcp.WithBoolean(name, mcp.Description(desc)))
}
case reflect.Slice:
// Handle slice types, especially []string and []float64
if field.Type.Elem().Kind() == reflect.String {
// Array of strings
if required {
options = append(options, mcp.WithArray(name, mcp.Required(), mcp.Description(desc)))
} else {
options = append(options, mcp.WithArray(name, mcp.Description(desc)))
}
} else if field.Type.Elem().Kind() == reflect.Float64 {
// Array of numbers
if required {
options = append(options, mcp.WithArray(name, mcp.Required(), mcp.Description(desc)))
} else {
options = append(options, mcp.WithArray(name, mcp.Description(desc)))
}
}
default:
log.Warn().Str("field_type", field.Type.String()).Msg("Unsupported field type")
}

View File

@@ -2,8 +2,10 @@ package uixt
import (
"context"
"encoding/json"
"fmt"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/uixt/ai"
"github.com/httprunner/httprunner/v5/uixt/option"
"github.com/mark3labs/mcp-go/client"
@@ -78,28 +80,755 @@ func (c *MCPClient4XTDriver) Close() error {
return nil
}
func convertActionToCallToolRequest(action MobileAction) (mcp.CallToolRequest, error) {
// req := mcp.CallToolRequest{
// Params: struct {
// Name string `json:"name"`
// Arguments map[string]any `json:"arguments,omitempty"`
// Meta *struct {
// ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
// } `json:"_meta,omitempty"`
// }{
// Name: action.Method,
// Arguments: action.Params,
// },
// }
return mcp.CallToolRequest{}, nil
}
func (dExt *XTDriver) ExecuteAction(action MobileAction) (err error) {
// convert action to call tool request
// Convert action to MCP tool call
req, err := convertActionToCallToolRequest(action)
if err != nil {
return err
return fmt.Errorf("failed to convert action to MCP tool call: %w", err)
}
// Execute via MCP tool
result, err := dExt.client.CallTool(context.Background(), req)
if err != nil {
return fmt.Errorf("MCP tool call failed: %w", err)
}
// Check if the tool execution had business logic errors
if result.IsError {
if len(result.Content) > 0 {
return fmt.Errorf("tool execution failed: %s", result.Content[0])
}
return fmt.Errorf("tool execution failed")
}
log.Debug().Str("method", string(action.Method)).Msg("executed action via MCP tool")
return nil
}
func convertActionToCallToolRequest(action MobileAction) (mcp.CallToolRequest, error) {
var arguments map[string]interface{}
switch action.Method {
case option.ACTION_WebLoginNoneUI:
if params, ok := action.Params.([]interface{}); ok && len(params) == 4 {
arguments = map[string]interface{}{
"packageName": params[0].(string),
"phoneNumber": params[1].(string),
"captcha": params[2].(string),
"password": params[3].(string),
}
} else if params, ok := action.Params.([]string); ok && len(params) == 4 {
arguments = map[string]interface{}{
"packageName": params[0],
"phoneNumber": params[1],
"captcha": params[2],
"password": params[3],
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid web login params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "web_login_none_ui",
Arguments: arguments,
},
}, nil
case option.ACTION_AppInstall:
if app, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"appUrl": app,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid app install params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "app_install",
Arguments: arguments,
},
}, nil
case option.ACTION_AppUninstall:
if packageName, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"packageName": packageName,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid app uninstall params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "app_uninstall",
Arguments: arguments,
},
}, nil
case option.ACTION_AppClear:
if packageName, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"packageName": packageName,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid app clear params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "app_clear",
Arguments: arguments,
},
}, nil
case option.ACTION_AppLaunch:
if packageName, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"packageName": packageName,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid app launch params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "launch_app",
Arguments: arguments,
},
}, nil
case option.ACTION_SwipeToTapApp:
if appName, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"appName": appName,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap app params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "swipe_to_tap_app",
Arguments: arguments,
},
}, nil
case option.ACTION_SwipeToTapText:
if text, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"text": text,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap text params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "swipe_to_tap_text",
Arguments: arguments,
},
}, nil
case option.ACTION_SwipeToTapTexts:
var texts []string
if textsSlice, ok := action.Params.([]string); ok {
texts = textsSlice
} else if textsInterface, err := builtin.ConvertToStringSlice(action.Params); err == nil {
texts = textsInterface
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap texts params: %v", action.Params)
}
arguments = map[string]interface{}{
"texts": texts,
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "swipe_to_tap_texts",
Arguments: arguments,
},
}, nil
case option.ACTION_AppTerminate:
if packageName, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"packageName": packageName,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid app terminate params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "terminate_app",
Arguments: arguments,
},
}, nil
case option.ACTION_Home:
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "home",
Arguments: map[string]interface{}{},
},
}, nil
case option.ACTION_SecondaryClick:
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil && len(params) == 2 {
arguments = map[string]interface{}{
"x": params[0],
"y": params[1],
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid secondary click params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "secondary_click",
Arguments: arguments,
},
}, nil
case option.ACTION_HoverBySelector:
if selector, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"selector": selector,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid hover by selector params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "hover_by_selector",
Arguments: arguments,
},
}, nil
case option.ACTION_TapBySelector:
if selector, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"selector": selector,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap by selector params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "tap_by_selector",
Arguments: arguments,
},
}, nil
case option.ACTION_SecondaryClickBySelector:
if selector, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"selector": selector,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid secondary click by selector params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "secondary_click_by_selector",
Arguments: arguments,
},
}, nil
case option.ACTION_WebCloseTab:
var tabIndex int
if param, ok := action.Params.(json.Number); ok {
paramInt64, _ := param.Int64()
tabIndex = int(paramInt64)
} else if param, ok := action.Params.(int64); ok {
tabIndex = int(param)
} else if param, ok := action.Params.(int); ok {
tabIndex = param
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid web close tab params: %v", action.Params)
}
arguments = map[string]interface{}{
"tabIndex": tabIndex,
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "web_close_tab",
Arguments: arguments,
},
}, nil
case option.ACTION_SetIme:
if ime, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"ime": ime,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid set ime params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "set_ime",
Arguments: arguments,
},
}, nil
case option.ACTION_GetSource:
if packageName, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"packageName": packageName,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid get source params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "get_source",
Arguments: arguments,
},
}, nil
case option.ACTION_TapXY:
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil && len(params) == 2 {
x, y := params[0], params[1]
arguments = map[string]interface{}{
"x": x,
"y": y,
}
// Add duration if available from action options
if actionOptions := action.GetOptions(); len(actionOptions) > 0 {
for _, opt := range actionOptions {
if opt != nil {
// Add options like duration
if duration := action.ActionOptions.Duration; duration > 0 {
arguments["duration"] = duration
}
}
}
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "tap_xy",
Arguments: arguments,
},
}, nil
case option.ACTION_TapAbsXY:
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil && len(params) == 2 {
x, y := params[0], params[1]
arguments = map[string]interface{}{
"x": x,
"y": y,
}
// Add duration if available
if duration := action.ActionOptions.Duration; duration > 0 {
arguments["duration"] = duration
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap abs params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "tap_abs_xy",
Arguments: arguments,
},
}, nil
case option.ACTION_TapByOCR:
if text, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"text": text,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap by OCR params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "tap_by_ocr",
Arguments: arguments,
},
}, nil
case option.ACTION_TapByCV:
// For TapByCV, the original action might not have params but relies on options
arguments = map[string]interface{}{
"imagePath": "", // Will be handled by the tool based on UI types
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "tap_by_cv",
Arguments: arguments,
},
}, nil
case option.ACTION_DoubleTapXY:
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil && len(params) == 2 {
x, y := params[0], params[1]
arguments = map[string]interface{}{
"x": x,
"y": y,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid double tap params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "double_tap_xy",
Arguments: arguments,
},
}, nil
case option.ACTION_Swipe:
// Handle different types of swipe params
switch params := action.Params.(type) {
case string:
// Direction swipe like "up", "down", "left", "right"
arguments = map[string]interface{}{
"direction": params,
}
// Add duration and press duration from options
if duration := action.ActionOptions.Duration; duration > 0 {
arguments["duration"] = duration
}
if pressDuration := action.ActionOptions.PressDuration; pressDuration > 0 {
arguments["pressDuration"] = pressDuration
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "swipe",
Arguments: arguments,
},
}, nil
default:
// Advanced swipe with coordinates
if paramSlice, err := builtin.ConvertToFloat64Slice(params); err == nil && len(paramSlice) == 4 {
arguments = map[string]interface{}{
"fromX": paramSlice[0],
"fromY": paramSlice[1],
"toX": paramSlice[2],
"toY": paramSlice[3],
}
// Add duration and press duration from options
if duration := action.ActionOptions.Duration; duration > 0 {
arguments["duration"] = duration
}
if pressDuration := action.ActionOptions.PressDuration; pressDuration > 0 {
arguments["pressDuration"] = pressDuration
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "swipe_advanced",
Arguments: arguments,
},
}, nil
}
}
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe params: %v", action.Params)
case option.ACTION_Input:
text := fmt.Sprintf("%v", action.Params)
arguments = map[string]interface{}{
"text": text,
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "input",
Arguments: arguments,
},
}, nil
case option.ACTION_Back:
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "back",
Arguments: map[string]interface{}{},
},
}, nil
case option.ACTION_Sleep:
arguments = map[string]interface{}{
"seconds": action.Params,
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "sleep",
Arguments: arguments,
},
}, nil
case option.ACTION_SleepMS:
var milliseconds int64
if param, ok := action.Params.(json.Number); ok {
milliseconds, _ = param.Int64()
} else if param, ok := action.Params.(int64); ok {
milliseconds = param
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid sleep ms params: %v", action.Params)
}
arguments = map[string]interface{}{
"milliseconds": milliseconds,
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "sleep_ms",
Arguments: arguments,
},
}, nil
case option.ACTION_SleepRandom:
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
arguments = map[string]interface{}{
"params": params,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid sleep random params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "sleep_random",
Arguments: arguments,
},
}, nil
case option.ACTION_ScreenShot:
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "screenshot",
Arguments: map[string]interface{}{},
},
}, nil
case option.ACTION_ClosePopups:
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "close_popups",
Arguments: map[string]interface{}{},
},
}, nil
case option.ACTION_CallFunction:
if description, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"description": description,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid call function params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "call_function",
Arguments: arguments,
},
}, nil
case option.ACTION_AIAction:
if prompt, ok := action.Params.(string); ok {
arguments = map[string]interface{}{
"prompt": prompt,
}
} else {
return mcp.CallToolRequest{}, fmt.Errorf("invalid AI action params: %v", action.Params)
}
return mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "ai_action",
Arguments: arguments,
},
}, nil
default:
return mcp.CallToolRequest{}, fmt.Errorf("unsupported action method: %s", action.Method)
}
_, err = dExt.client.CallTool(context.Background(), req)
return err
}