Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions internal/mcp_handlers/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func RegisterTools(
registerTool(s, tools.getSelectorByParametersTool)
registerTool(s, tools.getAllSelectorsTool)
registerTool(s, tools.updateHealingResultTool)
registerTool(s, tools.saveSelectorPathTool)
}

func (ti *McpTool) getLastReportTool() (*mcp.Tool, ToolHandler[any, any]) {
Expand Down Expand Up @@ -371,3 +372,153 @@ func (ti *McpTool) updateHealingResultTool() (*mcp.Tool, ToolHandler[map[string]
}, nil, nil
}
}

func (ti *McpTool) saveSelectorPathTool() (*mcp.Tool, ToolHandler[map[string]any, any]) {
return &mcp.Tool{
Name: "saveSelectorPath",
Description: "Saves the path to a selector in Healenium. This endpoint associates healing results with their declaring classes for a specific selector.",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"id": {
Type: "string",
Description: "Unique identifier of the selector (MD5 hash format)",
Pattern: "^[0-9a-fA-F]{32}$",
},
"data": {
Type: "array",
Description: "List of records containing healing result IDs and their declaring classes (must contain at least one record)",
Items: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"healingResultId": {
Type: "integer",
Description: "Healing result ID",
},
"declaringClass": {
Type: "string",
Description: "Class path for selector (e.g., 'com.example.tests.LoginTest')",
},
},
Required: []string{"healingResultId", "declaringClass"},
},
},
},
Required: []string{"id", "data"},
},
},
func(ctx context.Context, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
// Extract and validate id
id, ok := args["id"].(string)
if !ok || id == "" {
return nil, nil, fmt.Errorf("id is required and must be a non-empty string")
}

// Validate that id is a valid 32-character hex string
if !uid.IsValidHexString(id, 32) {
return nil, nil, fmt.Errorf(
"invalid id: must be a 32-character hexadecimal string (MD5 hash format)",
)
}

// Extract and validate data array
dataRaw, ok := args["data"]
if !ok {
return nil, nil, fmt.Errorf("data is required")
}

dataArray, ok := dataRaw.([]any)
if !ok {
return nil, nil, fmt.Errorf("data must be an array")
}

if len(dataArray) == 0 {
return nil, nil, fmt.Errorf("data array must contain at least one record")
}

// Process and validate each data record
processedData := make([]map[string]any, 0, len(dataArray))
for i, item := range dataArray {
itemMap, ok := item.(map[string]any)
if !ok {
return nil, nil, fmt.Errorf("data[%d] must be an object", i)
}

// Extract and validate healingResultId
var healingResultId uint64
switch v := itemMap["healingResultId"].(type) {
case float64:
if v < 0 || v != float64(uint64(v)) {
return nil, nil, fmt.Errorf(
"data[%d].healingResultId must be a non-negative integer (got %v)",
i,
v,
)
Comment thread
AleksandrParamonoff marked this conversation as resolved.
}
healingResultId = uint64(v)
case uint64:
healingResultId = v
default:
return nil, nil, fmt.Errorf(
"data[%d].healingResultId is required and must be an unsigned integer",
i,
)
}

// Extract and validate declaringClass
declaringClass, ok := itemMap["declaringClass"].(string)
if !ok || declaringClass == "" {
return nil, nil, fmt.Errorf(
"data[%d].declaringClass is required and must be a non-empty string",
i,
)
}

processedData = append(processedData, map[string]any{
"healingResultId": healingResultId,
"declaringClass": declaringClass,
})
}

// Build request body
requestBody := map[string]any{
"id": id,
"data": processedData,
}

// Create a new HTTP POST request
resp, err := ti.client.R().
SetContext(ctx).
SetBody(requestBody).
Post("/healenium/selector/save/path")
if err != nil {
return nil, nil, fmt.Errorf(
"failed to save selector path at /healenium/selector/save/path: %w",
err,
)
}

// Check HTTP status code
if !resp.IsSuccess() {
return nil, nil, fmt.Errorf(
"failed to save selector path: received status code %d, body: %s",
resp.StatusCode(),
resp.String(),
)
}

// Create success message
successMessage := fmt.Sprintf(
"Selector path saved successfully for id %s with %d record(s) (HTTP %d)",
id,
len(processedData),
resp.StatusCode(),
)

return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: successMessage},
},
}, nil, nil
}
}
Loading
Loading