diff --git a/internal/version/VERSION b/internal/version/VERSION index c4a56dae..e73222e7 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505201326 +v5.0.0-beta-2505201421 diff --git a/server/mcp_server.go b/mcphost/mcp_server.go similarity index 90% rename from server/mcp_server.go rename to mcphost/mcp_server.go index 85dd64d3..17a2297f 100644 --- a/server/mcp_server.go +++ b/mcphost/mcp_server.go @@ -1,4 +1,4 @@ -package server +package mcphost import ( "context" @@ -12,6 +12,7 @@ import ( "github.com/httprunner/httprunner/v5/internal/version" "github.com/httprunner/httprunner/v5/uixt" "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/rs/zerolog/log" @@ -20,7 +21,8 @@ import ( // MCPServer4XTDriver wraps a MCPServer to expose XTDriver functionality via MCP protocol. type MCPServer4XTDriver struct { mcpServer *server.MCPServer - driverCache sync.Map // key is serial, value is *XTDriver + driverCache sync.Map // key is serial, value is *XTDriver + tools []mcp.Tool // 本地维护的工具列表 } // NewMCPServer creates a new MCP server for XTDriver and registers all tools. @@ -50,9 +52,10 @@ func (ums *MCPServer4XTDriver) addTools() { []mcp.ToolOption{mcp.WithDescription("Taps on the device screen at the given coordinates.")}, commonToolOptions..., ) - tapParams = append(tapParams, generateMCPOptions(TapRequest{})...) + tapParams = append(tapParams, generateMCPOptions(types.TapRequest{})...) tapXYTool := mcp.NewTool("tap_xy", tapParams...) ums.mcpServer.AddTool(tapXYTool, ums.handleTapXY) + ums.tools = append(ums.tools, tapXYTool) log.Info().Str("name", tapXYTool.Name).Msg("Register tool") // Swipe Tool @@ -60,9 +63,10 @@ func (ums *MCPServer4XTDriver) addTools() { []mcp.ToolOption{mcp.WithDescription("Swipes on the device screen from one point to another.")}, commonToolOptions..., ) - swipeParams = append(swipeParams, generateMCPOptions(DragRequest{})...) + swipeParams = append(swipeParams, generateMCPOptions(types.DragRequest{})...) swipeTool := mcp.NewTool("swipe", swipeParams...) ums.mcpServer.AddTool(swipeTool, ums.handleSwipe) + ums.tools = append(ums.tools, swipeTool) log.Info().Str("name", swipeTool.Name).Msg("Register tool") // ScreenShot Tool @@ -70,6 +74,7 @@ func (ums *MCPServer4XTDriver) addTools() { mcp.WithDescription("Takes a screenshot of the device screen and returns it as a base64 encoded string."), ) ums.mcpServer.AddTool(screenShotTool, ums.handleScreenShot) + ums.tools = append(ums.tools, screenShotTool) log.Info().Str("name", screenShotTool.Name).Msg("Register tool") } @@ -79,7 +84,7 @@ func (ums *MCPServer4XTDriver) handleTapXY(ctx context.Context, request mcp.Call if err != nil { return nil, err } - var tapReq TapRequest + var tapReq types.TapRequest if err := mapToStruct(request.Params.Arguments, &tapReq); err != nil { return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil } @@ -103,7 +108,7 @@ func (ums *MCPServer4XTDriver) handleSwipe(ctx context.Context, request mcp.Call if err != nil { return nil, err } - var swipeReq DragRequest + var swipeReq types.DragRequest if err := mapToStruct(request.Params.Arguments, &swipeReq); err != nil { return mcp.NewToolResultError("parse parameters error: " + err.Error()), nil } @@ -253,3 +258,18 @@ var commonToolOptions = []mcp.ToolOption{ mcp.WithString("platform", mcp.Required(), mcp.Description("Device platform: android/ios/browser")), mcp.WithString("serial", mcp.Required(), mcp.Description("Device serial/udid/browser id")), } + +// ListTools 返回所有注册的 mcp.Tool +func (s *MCPServer4XTDriver) ListTools() []mcp.Tool { + return s.tools +} + +// GetTool 根据名称返回 mcp.Tool 指针 +func (s *MCPServer4XTDriver) GetTool(name string) *mcp.Tool { + for i := range s.tools { + if s.tools[i].Name == name { + return &s.tools[i] + } + } + return nil +} diff --git a/server/model.go b/server/model.go index bc2adee6..4a3e8492 100644 --- a/server/model.go +++ b/server/model.go @@ -4,12 +4,6 @@ import ( "github.com/httprunner/httprunner/v5/uixt/option" ) -type TapRequest struct { - 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)"` - Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"` -} - type uploadRequest struct { X float64 `json:"x"` Y float64 `json:"y"` @@ -17,15 +11,6 @@ type uploadRequest struct { FileFormat string `json:"file_format"` } -type DragRequest struct { - FromX float64 `json:"from_x" binding:"required" desc:"Starting X-coordinate (percentage, 0.0 to 1.0)"` - FromY float64 `json:"from_y" binding:"required" desc:"Starting Y-coordinate (percentage, 0.0 to 1.0)"` - ToX float64 `json:"to_x" binding:"required" desc:"Ending X-coordinate (percentage, 0.0 to 1.0)"` - ToY float64 `json:"to_y" binding:"required" desc:"Ending Y-coordinate (percentage, 0.0 to 1.0)"` - Duration float64 `json:"duration" desc:"Swipe duration in milliseconds (optional)"` - PressDuration float64 `json:"press_duration" desc:"Press duration in milliseconds (optional)"` -} - type InputRequest struct { Text string `json:"text" binding:"required"` Frequency int `json:"frequency"` // only iOS diff --git a/server/ui.go b/server/ui.go index 186180e3..12a40f74 100644 --- a/server/ui.go +++ b/server/ui.go @@ -4,10 +4,11 @@ import ( "github.com/gin-gonic/gin" "github.com/httprunner/httprunner/v5/uixt" "github.com/httprunner/httprunner/v5/uixt/option" + "github.com/httprunner/httprunner/v5/uixt/types" ) func (r *Router) tapHandler(c *gin.Context) { - var tapReq TapRequest + var tapReq types.TapRequest if err := c.ShouldBindJSON(&tapReq); err != nil { RenderErrorValidateRequest(c, err) return @@ -30,7 +31,7 @@ func (r *Router) tapHandler(c *gin.Context) { } func (r *Router) rightClickHandler(c *gin.Context) { - var rightClickReq TapRequest + var rightClickReq types.TapRequest if err := c.ShouldBindJSON(&rightClickReq); err != nil { RenderErrorValidateRequest(c, err) return @@ -117,7 +118,7 @@ func (r *Router) scrollHandler(c *gin.Context) { } func (r *Router) doubleTapHandler(c *gin.Context) { - var tapReq TapRequest + var tapReq types.TapRequest if err := c.ShouldBindJSON(&tapReq); err != nil { RenderErrorValidateRequest(c, err) return @@ -137,7 +138,7 @@ func (r *Router) doubleTapHandler(c *gin.Context) { } func (r *Router) dragHandler(c *gin.Context) { - var dragReq DragRequest + var dragReq types.DragRequest if err := c.ShouldBindJSON(&dragReq); err != nil { RenderErrorValidateRequest(c, err) return diff --git a/server/ui_test.go b/server/ui_test.go index 9172ad80..1851eabe 100644 --- a/server/ui_test.go +++ b/server/ui_test.go @@ -8,6 +8,7 @@ import ( "net/http/httptest" "testing" + "github.com/httprunner/httprunner/v5/uixt/types" "github.com/stretchr/testify/assert" ) @@ -17,14 +18,14 @@ func TestTapHandler(t *testing.T) { tests := []struct { name string path string - tapReq TapRequest + tapReq types.TapRequest wantStatus int wantResp HttpResponse }{ { name: "tap abs xy", path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"), - tapReq: TapRequest{ + tapReq: types.TapRequest{ X: 500, Y: 800, Duration: 0, @@ -39,7 +40,7 @@ func TestTapHandler(t *testing.T) { { name: "tap relative xy", path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"), - tapReq: TapRequest{ + tapReq: types.TapRequest{ X: 0.5, Y: 0.6, Duration: 0, diff --git a/uixt/types/request.go b/uixt/types/request.go new file mode 100644 index 00000000..c9bd59bc --- /dev/null +++ b/uixt/types/request.go @@ -0,0 +1,16 @@ +package types + +type TapRequest struct { + 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)"` + Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"` +} + +type DragRequest struct { + FromX float64 `json:"from_x" binding:"required" desc:"Starting X-coordinate (percentage, 0.0 to 1.0)"` + FromY float64 `json:"from_y" binding:"required" desc:"Starting Y-coordinate (percentage, 0.0 to 1.0)"` + ToX float64 `json:"to_x" binding:"required" desc:"Ending X-coordinate (percentage, 0.0 to 1.0)"` + ToY float64 `json:"to_y" binding:"required" desc:"Ending Y-coordinate (percentage, 0.0 to 1.0)"` + Duration float64 `json:"duration" desc:"Swipe duration in milliseconds (optional)"` + PressDuration float64 `json:"press_duration" desc:"Press duration in milliseconds (optional)"` +}