package tools import ( "context" "fmt" "strings" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "forge.lclr.dev/AI/xdebug-mcp/internal/cache" "forge.lclr.dev/AI/xdebug-mcp/internal/cachegrind" ) // CalleesTool returns the MCP tool definition for get_callees. func CalleesTool() mcp.Tool { return mcp.NewTool("get_callees", mcp.WithDescription("List functions called by a given function in an Xdebug profiling file, sorted by call cost descending."), mcp.WithString("file_path", mcp.Required(), mcp.Description("Absolute or relative path to the cachegrind file"), ), mcp.WithString("function_name", mcp.Required(), mcp.Description("Exact function name or substring to search for"), ), mcp.WithNumber("top_n", mcp.Description("Maximum number of callees to return (default: 10)"), ), ) } // CalleesHandler returns the MCP handler for get_callees. func CalleesHandler(c *cache.Cache) server.ToolHandlerFunc { return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { filePath := req.GetString("file_path", "") if filePath == "" { return mcp.NewToolResultError("file_path is required"), nil } name := req.GetString("function_name", "") if name == "" { return mcp.NewToolResultError("function_name is required"), nil } topN := req.GetInt("top_n", 10) if topN <= 0 { topN = 10 } p, err := loadProfile(filePath, c) if err != nil { return mcp.NewToolResultError(err.Error()), nil } return mcp.NewToolResultText(Callees(p, name, topN)), nil } } // Callees formats the callees of name in p. Exported for testing. func Callees(p *cachegrind.Profile, name string, topN int) string { fns, errMsg := findFunctions(p, name) if errMsg != "" { return errMsg } var sb strings.Builder if len(fns) > 1 { fmt.Fprintf(&sb, "Warning: %d functions match %q — showing all\n\n", len(fns), name) } for _, fn := range fns { fmt.Fprintf(&sb, "Callees of %q [%s]\n", fn.Name, fn.File) if len(fn.Calls) == 0 { fmt.Fprintf(&sb, " no outgoing calls recorded\n\n") continue } callees := sortedCalls(fn.Calls) if topN < len(callees) { callees = callees[:topN] } fmt.Fprintf(&sb, " calls %d function(s):\n\n", len(fn.Calls)) for i, call := range callees { fmt.Fprintf(&sb, " %3d. %-60s calls=%d %s\n", i+1, call.Callee.Name, call.Count, formatCosts(call.Costs, p.Events)) fmt.Fprintf(&sb, " %s\n", call.Callee.File) } fmt.Fprintln(&sb) } return sb.String() }