mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-05 15:37:35 +08:00
refactor: integrate and optimize MCP tool calling methods
This commit is contained in:
@@ -1 +1 @@
|
|||||||
v5.0.0-beta-2505272016
|
v5.0.0-beta-2505272139
|
||||||
|
|||||||
+21
@@ -448,6 +448,16 @@ func (s *StepMobile) ClosePopups(opts ...option.ActionOption) *StepMobile {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StepMobile) Call(name string, fn func(), opts ...option.ActionOption) *StepMobile {
|
||||||
|
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||||
|
Method: option.ACTION_CallFunction,
|
||||||
|
Params: name, // function description
|
||||||
|
Fn: fn,
|
||||||
|
Options: option.NewActionOptions(opts...),
|
||||||
|
})
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
// Validate switches to step validation.
|
// Validate switches to step validation.
|
||||||
func (s *StepMobile) Validate() *StepMobileUIValidation {
|
func (s *StepMobile) Validate() *StepMobileUIValidation {
|
||||||
return &StepMobileUIValidation{
|
return &StepMobileUIValidation{
|
||||||
@@ -804,6 +814,17 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// call custom function
|
||||||
|
if action.Method == option.ACTION_CallFunction {
|
||||||
|
if funcDesc, ok := action.Params.(string); ok {
|
||||||
|
err := uiDriver.Call(funcDesc, action.Fn, action.GetOptions()...)
|
||||||
|
if err != nil {
|
||||||
|
return stepResult, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
err = uiDriver.ExecuteAction(context.Background(), action)
|
err = uiDriver.ExecuteAction(context.Background(), action)
|
||||||
actionResult.Elapsed = time.Since(actionStartTime).Milliseconds()
|
actionResult.Elapsed = time.Since(actionStartTime).Milliseconds()
|
||||||
stepResult.Actions = append(stepResult.Actions, actionResult)
|
stepResult.Actions = append(stepResult.Actions, actionResult)
|
||||||
|
|||||||
@@ -281,7 +281,9 @@ func (ad *ADBDriver) AppLaunch(packageName string) (err error) {
|
|||||||
return errors.Wrap(code.MobileUILaunchAppError,
|
return errors.Wrap(code.MobileUILaunchAppError,
|
||||||
fmt.Sprintf("monkey aborted: %s", strings.TrimSpace(sOutput)))
|
fmt.Sprintf("monkey aborted: %s", strings.TrimSpace(sOutput)))
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
return postHandler(ad, option.ACTION_SetTouchInfo,
|
||||||
|
option.NewActionOptions(option.WithAntiRisk(true)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *ADBDriver) AppTerminate(packageName string) (successful bool, err error) {
|
func (ad *ADBDriver) AppTerminate(packageName string) (successful bool, err error) {
|
||||||
|
|||||||
@@ -308,3 +308,32 @@ func RegisterXTDriver(serial string, driver *XTDriver) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getXTDriverFromCache gets XTDriver from cache using device UUID
|
||||||
|
func getXTDriverFromCache(driver IDriver) *XTDriver {
|
||||||
|
// Get device info to find the corresponding XTDriver
|
||||||
|
device := driver.GetDevice()
|
||||||
|
if device == nil {
|
||||||
|
log.Warn().Msg("Cannot get device from driver for MCP hook")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get device UUID (serial/udid/connectKey/browserID)
|
||||||
|
deviceUUID := device.UUID()
|
||||||
|
if deviceUUID == "" {
|
||||||
|
log.Warn().Msg("Cannot get device UUID for MCP hook")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get XTDriver from cache using device UUID as serial
|
||||||
|
cachedDrivers := ListCachedDrivers()
|
||||||
|
for _, cached := range cachedDrivers {
|
||||||
|
if cached.Serial == deviceUUID {
|
||||||
|
return cached.Driver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warn().Str("uuid", deviceUUID).
|
||||||
|
Msg("Cannot find cached XTDriver for MCP hook")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
type MobileAction struct {
|
type MobileAction struct {
|
||||||
Method option.ActionName `json:"method,omitempty" yaml:"method,omitempty"`
|
Method option.ActionName `json:"method,omitempty" yaml:"method,omitempty"`
|
||||||
Params interface{} `json:"params,omitempty" yaml:"params,omitempty"`
|
Params interface{} `json:"params,omitempty" yaml:"params,omitempty"`
|
||||||
|
Fn func() `json:"-" yaml:"-"` // used for function action, not serialized
|
||||||
Options *option.ActionOptions `json:"options,omitempty" yaml:"options,omitempty"`
|
Options *option.ActionOptions `json:"options,omitempty" yaml:"options,omitempty"`
|
||||||
option.ActionOptions
|
option.ActionOptions
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-117
@@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||||
"github.com/httprunner/httprunner/v5/internal/config"
|
"github.com/httprunner/httprunner/v5/internal/config"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,10 +50,7 @@ func preHandler_TapAbsXY(driver IDriver, options *option.ActionOptions, rawX, ra
|
|||||||
|
|
||||||
// Call MCP action tool if anti-risk is enabled
|
// Call MCP action tool if anti-risk is enabled
|
||||||
if options.AntiRisk {
|
if options.AntiRisk {
|
||||||
callMCPActionTool(driver, option.ACTION_TapAbsXY, map[string]any{
|
// TODO
|
||||||
"x": rawX,
|
|
||||||
"y": rawY,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
x, y = options.ApplyTapOffset(rawX, rawY)
|
x, y = options.ApplyTapOffset(rawX, rawY)
|
||||||
@@ -129,6 +125,13 @@ func preHandler_Swipe(driver IDriver, actionType option.ActionName,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func postHandler(driver IDriver, actionType option.ActionName, options *option.ActionOptions) error {
|
func postHandler(driver IDriver, actionType option.ActionName, options *option.ActionOptions) error {
|
||||||
|
if options.AntiRisk && actionType == option.ACTION_SetTouchInfo {
|
||||||
|
arguments := getAntiRisk_SetTouchInfo_Arguments(driver)
|
||||||
|
if arguments != nil {
|
||||||
|
callMCPActionTool(driver, "evalpkgs", string(actionType), arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// save screenshot after action
|
// save screenshot after action
|
||||||
if options.PostMarkOperation {
|
if options.PostMarkOperation {
|
||||||
// get compressed screenshot buffer
|
// get compressed screenshot buffer
|
||||||
@@ -155,129 +158,30 @@ func postHandler(driver IDriver, actionType option.ActionName, options *option.A
|
|||||||
}
|
}
|
||||||
|
|
||||||
// callMCPActionTool calls MCP tool for the given action
|
// callMCPActionTool calls MCP tool for the given action
|
||||||
func callMCPActionTool(driver IDriver, actionType option.ActionName, arguments map[string]any) {
|
func callMCPActionTool(driver IDriver,
|
||||||
|
serverName, actionType string, arguments map[string]any) {
|
||||||
// Get XTDriver from cache
|
// Get XTDriver from cache
|
||||||
dExt := getXTDriverFromCache(driver)
|
dExt := getXTDriverFromCache(driver)
|
||||||
if dExt == nil {
|
if dExt == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define action to MCP server mapping for pre-hooks
|
dExt.CallMCPTool(context.Background(),
|
||||||
serverMapping := getPreHookServerMapping(actionType)
|
serverName, actionType, arguments)
|
||||||
if serverMapping == nil {
|
|
||||||
return // No MCP hook configured for this action
|
|
||||||
}
|
|
||||||
|
|
||||||
callMCPTool(dExt, serverMapping.ServerName, serverMapping.ToolName, arguments, actionType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MCPServerMapping defines the mapping between action and MCP server/tool
|
func getAntiRisk_SetTouchInfo_Arguments(driver IDriver) map[string]interface{} {
|
||||||
type MCPServerMapping struct {
|
var deviceModel string
|
||||||
ServerName string
|
|
||||||
ToolName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPreHookServerMapping returns MCP server mapping for pre-hooks
|
|
||||||
// TODO: You can customize these mappings according to your needs
|
|
||||||
func getPreHookServerMapping(actionType option.ActionName) *MCPServerMapping {
|
|
||||||
mappings := map[option.ActionName]*MCPServerMapping{
|
|
||||||
option.ACTION_TapAbsXY: {
|
|
||||||
ServerName: "evalpkgs",
|
|
||||||
ToolName: "log_pre_action",
|
|
||||||
},
|
|
||||||
// Add more mappings as needed
|
|
||||||
// option.ACTION_Swipe: {
|
|
||||||
// ServerName: "monitor",
|
|
||||||
// ToolName: "start_timer",
|
|
||||||
// },
|
|
||||||
}
|
|
||||||
return mappings[actionType]
|
|
||||||
}
|
|
||||||
|
|
||||||
// getXTDriverFromCache gets XTDriver from cache using device UUID
|
|
||||||
func getXTDriverFromCache(driver IDriver) *XTDriver {
|
|
||||||
// Get device info to find the corresponding XTDriver
|
|
||||||
device := driver.GetDevice()
|
device := driver.GetDevice()
|
||||||
if device == nil {
|
if adbDevice, ok := device.(*AndroidDevice); ok {
|
||||||
log.Warn().Msg("Cannot get device from driver for MCP hook")
|
var err error
|
||||||
return nil
|
deviceModel, err = adbDevice.Model()
|
||||||
}
|
|
||||||
|
|
||||||
// Get device UUID (serial/udid/connectKey/browserID)
|
|
||||||
deviceUUID := device.UUID()
|
|
||||||
if deviceUUID == "" {
|
|
||||||
log.Warn().Msg("Cannot get device UUID for MCP hook")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get XTDriver from cache using device UUID as serial
|
|
||||||
cachedDrivers := ListCachedDrivers()
|
|
||||||
for _, cached := range cachedDrivers {
|
|
||||||
if cached.Serial == deviceUUID {
|
|
||||||
return cached.Driver
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Warn().Str("uuid", deviceUUID).
|
|
||||||
Msg("Cannot find cached XTDriver for MCP hook")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// callMCPTool calls the specified MCP tool
|
|
||||||
func callMCPTool(dExt *XTDriver, serverName, toolName string, arguments map[string]any, actionType option.ActionName) {
|
|
||||||
// Get MCP client
|
|
||||||
mcpClient, exists := dExt.GetMCPClient(serverName)
|
|
||||||
if !exists {
|
|
||||||
log.Debug().Str("server", serverName).Msg("MCP server not found for hook")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create context with timeout
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Prepare arguments
|
|
||||||
if arguments == nil {
|
|
||||||
arguments = make(map[string]any)
|
|
||||||
}
|
|
||||||
// Add action type and hook type to arguments
|
|
||||||
arguments["action_type"] = string(actionType)
|
|
||||||
|
|
||||||
// Call MCP tool
|
|
||||||
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: toolName,
|
|
||||||
Arguments: arguments,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := mcpClient.CallTool(ctx, req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).
|
return nil
|
||||||
Str("server", serverName).
|
}
|
||||||
Str("tool", toolName).
|
|
||||||
Msg("MCP hook call failed")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.IsError {
|
return map[string]interface{}{
|
||||||
log.Debug().
|
"deviceModel": deviceModel,
|
||||||
Str("server", serverName).
|
|
||||||
Str("tool", toolName).
|
|
||||||
Interface("content", result.Content).
|
|
||||||
Msg("MCP hook returned error")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().
|
|
||||||
Str("server", serverName).
|
|
||||||
Str("tool", toolName).
|
|
||||||
Str("action", string(actionType)).
|
|
||||||
Msg("MCP hook called successfully")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ const (
|
|||||||
ACTION_UninstallApp ActionName = "uninstall_app"
|
ACTION_UninstallApp ActionName = "uninstall_app"
|
||||||
ACTION_DownloadApp ActionName = "download_app"
|
ACTION_DownloadApp ActionName = "download_app"
|
||||||
ACTION_Finished ActionName = "finished"
|
ACTION_Finished ActionName = "finished"
|
||||||
|
ACTION_CallFunction ActionName = "call_function"
|
||||||
|
|
||||||
|
// anti-risk actions
|
||||||
|
ACTION_SetTouchInfo ActionName = "set_touch_info"
|
||||||
|
ACTION_SetTouchInfoList ActionName = "set_touch_info_list"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
+58
@@ -160,3 +160,61 @@ func (dExt *XTDriver) GetMCPClient(serverName string) (client.MCPClient, bool) {
|
|||||||
client, exists := dExt.loadedMCPClients[serverName]
|
client, exists := dExt.loadedMCPClients[serverName]
|
||||||
return client, exists
|
return client, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CallMCPTool calls the specified MCP tool
|
||||||
|
func (dExt *XTDriver) CallMCPTool(ctx context.Context,
|
||||||
|
serverName, toolName string, arguments map[string]any) (result *mcp.CallToolResult, err error) {
|
||||||
|
// Get MCP client
|
||||||
|
|
||||||
|
mcpClient, exists := dExt.GetMCPClient(serverName)
|
||||||
|
if !exists {
|
||||||
|
log.Warn().Str("server", serverName).Msg("MCP server not found")
|
||||||
|
return nil, fmt.Errorf("MCP server %s not found", serverName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare arguments
|
||||||
|
if arguments == nil {
|
||||||
|
arguments = make(map[string]any)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("server", serverName).Str("tool", toolName).
|
||||||
|
Interface("arguments", arguments).Msg("call MCP tool")
|
||||||
|
|
||||||
|
// Call MCP tool
|
||||||
|
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: toolName,
|
||||||
|
Arguments: arguments,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = mcpClient.CallTool(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).
|
||||||
|
Str("server", serverName).
|
||||||
|
Str("tool", toolName).
|
||||||
|
Msg("MCP hook call failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.IsError {
|
||||||
|
log.Debug().
|
||||||
|
Str("server", serverName).
|
||||||
|
Str("tool", toolName).
|
||||||
|
Interface("content", result.Content).
|
||||||
|
Msg("MCP hook returned error")
|
||||||
|
return nil, fmt.Errorf("MCP hook returned error")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("server", serverName).
|
||||||
|
Str("tool", toolName).
|
||||||
|
Msg("MCP hook called successfully")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user