refactor: move tool request types to option

This commit is contained in:
lilong.129
2025-05-24 23:51:58 +08:00
parent f65d8aebbd
commit 97dad38b7b
7 changed files with 85 additions and 79 deletions

View File

@@ -1 +1 @@
v5.0.0-beta-2505242332 v5.0.0-beta-2505242351

View File

@@ -5,7 +5,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/uixt" "github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/types" "github.com/httprunner/httprunner/v5/uixt/option"
) )
func (r *Router) foregroundAppHandler(c *gin.Context) { func (r *Router) foregroundAppHandler(c *gin.Context) {
@@ -22,7 +22,7 @@ func (r *Router) foregroundAppHandler(c *gin.Context) {
} }
func (r *Router) appInfoHandler(c *gin.Context) { func (r *Router) appInfoHandler(c *gin.Context) {
var appInfoReq types.AppInfoRequest var appInfoReq option.AppInfoRequest
if err := c.ShouldBindQuery(&appInfoReq); err != nil { if err := c.ShouldBindQuery(&appInfoReq); err != nil {
RenderErrorValidateRequest(c, err) RenderErrorValidateRequest(c, err)
return return
@@ -51,7 +51,7 @@ func (r *Router) appInfoHandler(c *gin.Context) {
} }
func (r *Router) clearAppHandler(c *gin.Context) { func (r *Router) clearAppHandler(c *gin.Context) {
var appClearReq types.AppClearRequest var appClearReq option.AppClearRequest
if err := c.ShouldBindJSON(&appClearReq); err != nil { if err := c.ShouldBindJSON(&appClearReq); err != nil {
RenderErrorValidateRequest(c, err) RenderErrorValidateRequest(c, err)
return return
@@ -70,7 +70,7 @@ func (r *Router) clearAppHandler(c *gin.Context) {
} }
func (r *Router) launchAppHandler(c *gin.Context) { func (r *Router) launchAppHandler(c *gin.Context) {
var appLaunchReq types.AppLaunchRequest var appLaunchReq option.AppLaunchRequest
if err := c.ShouldBindJSON(&appLaunchReq); err != nil { if err := c.ShouldBindJSON(&appLaunchReq); err != nil {
RenderErrorValidateRequest(c, err) RenderErrorValidateRequest(c, err)
return return
@@ -88,7 +88,7 @@ func (r *Router) launchAppHandler(c *gin.Context) {
} }
func (r *Router) terminalAppHandler(c *gin.Context) { func (r *Router) terminalAppHandler(c *gin.Context) {
var appTerminateReq types.AppTerminateRequest var appTerminateReq option.AppTerminateRequest
if err := c.ShouldBindJSON(&appTerminateReq); err != nil { if err := c.ShouldBindJSON(&appTerminateReq); err != nil {
RenderErrorValidateRequest(c, err) RenderErrorValidateRequest(c, err)
return return
@@ -106,7 +106,7 @@ func (r *Router) terminalAppHandler(c *gin.Context) {
} }
func (r *Router) uninstallAppHandler(c *gin.Context) { func (r *Router) uninstallAppHandler(c *gin.Context) {
var appUninstallReq types.AppUninstallRequest var appUninstallReq option.AppUninstallRequest
if err := c.ShouldBindJSON(&appUninstallReq); err != nil { if err := c.ShouldBindJSON(&appUninstallReq); err != nil {
RenderErrorValidateRequest(c, err) RenderErrorValidateRequest(c, err)
return return

View File

@@ -4,7 +4,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/httprunner/httprunner/v5/uixt" "github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/types" "github.com/httprunner/httprunner/v5/uixt/option"
) )
func (r *Router) unlockHandler(c *gin.Context) { func (r *Router) unlockHandler(c *gin.Context) {
@@ -34,7 +34,7 @@ func (r *Router) homeHandler(c *gin.Context) {
} }
func (r *Router) backspaceHandler(c *gin.Context) { func (r *Router) backspaceHandler(c *gin.Context) {
var deleteReq types.DeleteRequest var deleteReq option.DeleteRequest
if err := c.ShouldBindJSON(&deleteReq); err != nil { if err := c.ShouldBindJSON(&deleteReq); err != nil {
RenderErrorValidateRequest(c, err) RenderErrorValidateRequest(c, err)
return return
@@ -55,7 +55,7 @@ func (r *Router) backspaceHandler(c *gin.Context) {
} }
func (r *Router) keycodeHandler(c *gin.Context) { func (r *Router) keycodeHandler(c *gin.Context) {
var keycodeReq types.KeycodeRequest var keycodeReq option.KeycodeRequest
if err := c.ShouldBindJSON(&keycodeReq); err != nil { if err := c.ShouldBindJSON(&keycodeReq); err != nil {
RenderErrorValidateRequest(c, err) RenderErrorValidateRequest(c, err)
return return

View File

@@ -4,11 +4,10 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/httprunner/httprunner/v5/uixt" "github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option" "github.com/httprunner/httprunner/v5/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/types"
) )
func (r *Router) tapHandler(c *gin.Context) { func (r *Router) tapHandler(c *gin.Context) {
var tapReq types.TapRequest var tapReq option.TapRequest
if err := c.ShouldBindJSON(&tapReq); err != nil { if err := c.ShouldBindJSON(&tapReq); err != nil {
RenderErrorValidateRequest(c, err) RenderErrorValidateRequest(c, err)
return return
@@ -31,7 +30,7 @@ func (r *Router) tapHandler(c *gin.Context) {
} }
func (r *Router) rightClickHandler(c *gin.Context) { func (r *Router) rightClickHandler(c *gin.Context) {
var rightClickReq types.TapRequest var rightClickReq option.TapRequest
if err := c.ShouldBindJSON(&rightClickReq); err != nil { if err := c.ShouldBindJSON(&rightClickReq); err != nil {
RenderErrorValidateRequest(c, err) RenderErrorValidateRequest(c, err)
return return
@@ -118,7 +117,7 @@ func (r *Router) scrollHandler(c *gin.Context) {
} }
func (r *Router) doubleTapHandler(c *gin.Context) { func (r *Router) doubleTapHandler(c *gin.Context) {
var tapReq types.TapRequest var tapReq option.TapRequest
if err := c.ShouldBindJSON(&tapReq); err != nil { if err := c.ShouldBindJSON(&tapReq); err != nil {
RenderErrorValidateRequest(c, err) RenderErrorValidateRequest(c, err)
return return
@@ -138,7 +137,7 @@ func (r *Router) doubleTapHandler(c *gin.Context) {
} }
func (r *Router) dragHandler(c *gin.Context) { func (r *Router) dragHandler(c *gin.Context) {
var dragReq types.DragRequest var dragReq option.DragRequest
if err := c.ShouldBindJSON(&dragReq); err != nil { if err := c.ShouldBindJSON(&dragReq); err != nil {
RenderErrorValidateRequest(c, err) RenderErrorValidateRequest(c, err)
return return
@@ -162,7 +161,7 @@ func (r *Router) dragHandler(c *gin.Context) {
} }
func (r *Router) inputHandler(c *gin.Context) { func (r *Router) inputHandler(c *gin.Context) {
var inputReq types.InputRequest var inputReq option.InputRequest
if err := c.ShouldBindJSON(&inputReq); err != nil { if err := c.ShouldBindJSON(&inputReq); err != nil {
RenderErrorValidateRequest(c, err) RenderErrorValidateRequest(c, err)
return return

View File

@@ -8,7 +8,7 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/httprunner/httprunner/v5/uixt/types" "github.com/httprunner/httprunner/v5/uixt/option"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -18,14 +18,14 @@ func TestTapHandler(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
path string path string
tapReq types.TapRequest tapReq option.TapRequest
wantStatus int wantStatus int
wantResp HttpResponse wantResp HttpResponse
}{ }{
{ {
name: "tap abs xy", name: "tap abs xy",
path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"), path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"),
tapReq: types.TapRequest{ tapReq: option.TapRequest{
X: 500, X: 500,
Y: 800, Y: 800,
Duration: 0, Duration: 0,
@@ -40,7 +40,7 @@ func TestTapHandler(t *testing.T) {
{ {
name: "tap relative xy", name: "tap relative xy",
path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"), path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"),
tapReq: types.TapRequest{ tapReq: option.TapRequest{
X: 0.5, X: 0.5,
Y: 0.6, Y: 0.6,
Duration: 0, Duration: 0,

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect"
"strings" "strings"
"sync" "sync"
@@ -16,7 +15,6 @@ import (
"github.com/httprunner/httprunner/v5/internal/version" "github.com/httprunner/httprunner/v5/internal/version"
"github.com/httprunner/httprunner/v5/pkg/gadb" "github.com/httprunner/httprunner/v5/pkg/gadb"
"github.com/httprunner/httprunner/v5/uixt/option" "github.com/httprunner/httprunner/v5/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/types"
) )
// NewMCPServer creates a new MCP server for XTDriver and registers all tools. // NewMCPServer creates a new MCP server for XTDriver and registers all tools.
@@ -220,7 +218,7 @@ func (t *ToolListPackages) Description() string {
} }
func (t *ToolListPackages) Options() []mcp.ToolOption { func (t *ToolListPackages) Options() []mcp.ToolOption {
return generateMCPOptions(&types.TargetDeviceRequest{}) return option.NewMCPOptions(&option.TargetDeviceRequest{})
} }
func (t *ToolListPackages) Implement() toolCall { func (t *ToolListPackages) Implement() toolCall {
@@ -250,7 +248,7 @@ func (t *ToolLaunchApp) Description() string {
} }
func (t *ToolLaunchApp) Options() []mcp.ToolOption { func (t *ToolLaunchApp) Options() []mcp.ToolOption {
return generateMCPOptions(&types.AppLaunchRequest{}) return option.NewMCPOptions(&option.AppLaunchRequest{})
} }
func (t *ToolLaunchApp) Implement() toolCall { func (t *ToolLaunchApp) Implement() toolCall {
@@ -259,7 +257,7 @@ func (t *ToolLaunchApp) Implement() toolCall {
if err != nil { if err != nil {
return nil, err return nil, err
} }
var appLaunchReq types.AppLaunchRequest var appLaunchReq option.AppLaunchRequest
if err := mapToStruct(request.Params.Arguments, &appLaunchReq); err != nil { if err := mapToStruct(request.Params.Arguments, &appLaunchReq); err != nil {
return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil
} }
@@ -287,7 +285,7 @@ func (t *ToolTerminateApp) Description() string {
} }
func (t *ToolTerminateApp) Options() []mcp.ToolOption { func (t *ToolTerminateApp) Options() []mcp.ToolOption {
return generateMCPOptions(&types.AppTerminateRequest{}) return option.NewMCPOptions(&option.AppTerminateRequest{})
} }
func (t *ToolTerminateApp) Implement() toolCall { func (t *ToolTerminateApp) Implement() toolCall {
@@ -296,7 +294,7 @@ func (t *ToolTerminateApp) Implement() toolCall {
if err != nil { if err != nil {
return nil, err return nil, err
} }
var appTerminateReq types.AppTerminateRequest var appTerminateReq option.AppTerminateRequest
if err := mapToStruct(request.Params.Arguments, &appTerminateReq); err != nil { if err := mapToStruct(request.Params.Arguments, &appTerminateReq); err != nil {
return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil
} }
@@ -324,7 +322,7 @@ func (t *ToolGetScreenSize) Description() string {
} }
func (t *ToolGetScreenSize) Options() []mcp.ToolOption { func (t *ToolGetScreenSize) Options() []mcp.ToolOption {
return generateMCPOptions(&types.TargetDeviceRequest{}) return option.NewMCPOptions(&option.TargetDeviceRequest{})
} }
func (t *ToolGetScreenSize) Implement() toolCall { func (t *ToolGetScreenSize) Implement() toolCall {
@@ -356,7 +354,7 @@ func (t *ToolPressButton) Description() string {
} }
func (t *ToolPressButton) Options() []mcp.ToolOption { func (t *ToolPressButton) Options() []mcp.ToolOption {
return generateMCPOptions(&types.PressButtonRequest{}) return option.NewMCPOptions(&option.PressButtonRequest{})
} }
func (t *ToolPressButton) Implement() toolCall { func (t *ToolPressButton) Implement() toolCall {
@@ -365,7 +363,7 @@ func (t *ToolPressButton) Implement() toolCall {
if err != nil { if err != nil {
return nil, err return nil, err
} }
var pressButtonReq types.PressButtonRequest var pressButtonReq option.PressButtonRequest
if err := mapToStruct(request.Params.Arguments, &pressButtonReq); err != nil { if err := mapToStruct(request.Params.Arguments, &pressButtonReq); err != nil {
return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil
} }
@@ -389,7 +387,7 @@ func (t *ToolTapXY) Description() string {
} }
func (t *ToolTapXY) Options() []mcp.ToolOption { func (t *ToolTapXY) Options() []mcp.ToolOption {
return generateMCPOptions(&types.TapRequest{}) return option.NewMCPOptions(&option.TapRequest{})
} }
func (t *ToolTapXY) Implement() toolCall { func (t *ToolTapXY) Implement() toolCall {
@@ -398,7 +396,7 @@ func (t *ToolTapXY) Implement() toolCall {
if err != nil { if err != nil {
return mcp.NewToolResultError("Tap failed: " + err.Error()), nil return mcp.NewToolResultError("Tap failed: " + err.Error()), nil
} }
var tapReq types.TapRequest var tapReq option.TapRequest
if err := mapToStruct(request.Params.Arguments, &tapReq); err != nil { if err := mapToStruct(request.Params.Arguments, &tapReq); err != nil {
return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil
} }
@@ -426,7 +424,7 @@ func (t *ToolSwipe) Description() string {
} }
func (t *ToolSwipe) Options() []mcp.ToolOption { func (t *ToolSwipe) Options() []mcp.ToolOption {
return generateMCPOptions(&types.SwipeRequest{}) return option.NewMCPOptions(&option.SwipeRequest{})
} }
func (t *ToolSwipe) Implement() toolCall { func (t *ToolSwipe) Implement() toolCall {
@@ -435,7 +433,7 @@ func (t *ToolSwipe) Implement() toolCall {
if err != nil { if err != nil {
return mcp.NewToolResultError("Swipe failed: " + err.Error()), nil return mcp.NewToolResultError("Swipe failed: " + err.Error()), nil
} }
var swipeReq types.SwipeRequest var swipeReq option.SwipeRequest
if err := mapToStruct(request.Params.Arguments, &swipeReq); err != nil { if err := mapToStruct(request.Params.Arguments, &swipeReq); err != nil {
return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil
} }
@@ -480,7 +478,7 @@ func (t *ToolDrag) Description() string {
} }
func (t *ToolDrag) Options() []mcp.ToolOption { func (t *ToolDrag) Options() []mcp.ToolOption {
return generateMCPOptions(&types.DragRequest{}) return option.NewMCPOptions(&option.DragRequest{})
} }
func (t *ToolDrag) Implement() toolCall { func (t *ToolDrag) Implement() toolCall {
@@ -489,7 +487,7 @@ func (t *ToolDrag) Implement() toolCall {
if err != nil { if err != nil {
return nil, err return nil, err
} }
var dragReq types.DragRequest var dragReq option.DragRequest
if err := mapToStruct(request.Params.Arguments, &dragReq); err != nil { if err := mapToStruct(request.Params.Arguments, &dragReq); err != nil {
return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil
} }
@@ -521,7 +519,7 @@ func (t *ToolScreenShot) Description() string {
} }
func (t *ToolScreenShot) Options() []mcp.ToolOption { func (t *ToolScreenShot) Options() []mcp.ToolOption {
return generateMCPOptions(&types.TargetDeviceRequest{}) return option.NewMCPOptions(&option.TargetDeviceRequest{})
} }
func (t *ToolScreenShot) Implement() toolCall { func (t *ToolScreenShot) Implement() toolCall {
@@ -630,46 +628,6 @@ func NewDevice(platform, serial string) (device IDevice, err error) {
return device, nil return device, nil
} }
// generateMCPOptions generates mcp.NewTool parameters from a struct type.
// It automatically generates mcp.NewTool parameters based on the struct fields and their desc tags.
func generateMCPOptions(t interface{}) (options []mcp.ToolOption) {
tType := reflect.TypeOf(t)
for i := 0; i < tType.NumField(); i++ {
field := tType.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
name := strings.Split(jsonTag, ",")[0]
binding := field.Tag.Get("binding")
required := strings.Contains(binding, "required")
desc := field.Tag.Get("desc")
switch field.Type.Kind() {
case reflect.Float64, reflect.Float32, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if required {
options = append(options, mcp.WithNumber(name, mcp.Required(), mcp.Description(desc)))
} else {
options = append(options, mcp.WithNumber(name, mcp.Description(desc)))
}
case reflect.String:
if required {
options = append(options, mcp.WithString(name, mcp.Required(), mcp.Description(desc)))
} else {
options = append(options, mcp.WithString(name, mcp.Description(desc)))
}
case reflect.Bool:
if required {
options = append(options, mcp.WithBoolean(name, mcp.Required(), mcp.Description(desc)))
} else {
options = append(options, mcp.WithBoolean(name, mcp.Description(desc)))
}
default:
log.Warn().Str("field_type", field.Type.String()).Msg("Unsupported field type")
}
}
return options
}
// mapToStruct convert map[string]interface{} to target struct // mapToStruct convert map[string]interface{} to target struct
func mapToStruct(m map[string]interface{}, out interface{}) error { func mapToStruct(m map[string]interface{}, out interface{}) error {
b, err := json.Marshal(m) b, err := json.Marshal(m)

View File

@@ -1,4 +1,13 @@
package types package option
import (
"reflect"
"strings"
"github.com/httprunner/httprunner/v5/uixt/types"
"github.com/mark3labs/mcp-go/mcp"
"github.com/rs/zerolog/log"
)
type TargetDeviceRequest struct { type TargetDeviceRequest struct {
Platform string `json:"platform" binding:"required" desc:"Device platform: android/ios/browser"` Platform string `json:"platform" binding:"required" desc:"Device platform: android/ios/browser"`
@@ -80,5 +89,45 @@ type AppTerminateRequest struct {
type PressButtonRequest struct { type PressButtonRequest struct {
TargetDeviceRequest TargetDeviceRequest
Button DeviceButton `json:"button" binding:"required" desc:"The button to press. Supported buttons: BACK (android only), HOME, VOLUME_UP, VOLUME_DOWN, ENTER."` Button types.DeviceButton `json:"button" binding:"required" desc:"The button to press. Supported buttons: BACK (android only), HOME, VOLUME_UP, VOLUME_DOWN, ENTER."`
}
// 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)
for i := 0; i < tType.NumField(); i++ {
field := tType.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
name := strings.Split(jsonTag, ",")[0]
binding := field.Tag.Get("binding")
required := strings.Contains(binding, "required")
desc := field.Tag.Get("desc")
switch field.Type.Kind() {
case reflect.Float64, reflect.Float32, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if required {
options = append(options, mcp.WithNumber(name, mcp.Required(), mcp.Description(desc)))
} else {
options = append(options, mcp.WithNumber(name, mcp.Description(desc)))
}
case reflect.String:
if required {
options = append(options, mcp.WithString(name, mcp.Required(), mcp.Description(desc)))
} else {
options = append(options, mcp.WithString(name, mcp.Description(desc)))
}
case reflect.Bool:
if required {
options = append(options, mcp.WithBoolean(name, mcp.Required(), mcp.Description(desc)))
} else {
options = append(options, mcp.WithBoolean(name, mcp.Description(desc)))
}
default:
log.Warn().Str("field_type", field.Type.String()).Msg("Unsupported field type")
}
}
return options
} }