package main import ( "bytes" "context" "encoding/json" "os" "os/exec" "github.com/mark3labs/mcp-go/mcp" ) type runArgs struct { ID string `json:"id"` Args []string `json:"args,omitempty"` } func runTool() mcp.Tool { return mcp.NewTool("fn_run", mcp.WithDescription("Execute a registry function/pipeline via `fn run [args...]`. Dispatches by language: Go (go run/test), Python (.venv), Bash, TypeScript (tsx). Mutating side-effects possible. Off by default — server must be launched with --enable-run."), mcp.WithString("id", mcp.Required(), mcp.Description("Registry ID or function name."), ), mcp.WithArray("args", mcp.Description("Positional args appended to fn run."), mcp.Items(map[string]any{"type": "string"}), ), ) } func (d *deps) handleRun(ctx context.Context, _ mcp.CallToolRequest, args runArgs) (*mcp.CallToolResult, error) { if args.ID == "" { return mcp.NewToolResultError("id is required"), nil } bin := d.fnBin() cmdArgs := append([]string{"run", args.ID}, args.Args...) cmd := exec.CommandContext(ctx, bin, cmdArgs...) cmd.Dir = d.root cmd.Env = os.Environ() var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr runErr := cmd.Run() exit := 0 if cmd.ProcessState != nil { exit = cmd.ProcessState.ExitCode() } out := map[string]any{ "id": args.ID, "args": args.Args, "exit_code": exit, "stdout": truncate(stdout.String(), 100_000), "stderr": truncate(stderr.String(), 100_000), } if runErr != nil && exit == 0 { out["error"] = runErr.Error() } b, _ := json.MarshalIndent(out, "", " ") return mcp.NewToolResultText(string(b)), nil } func (d *deps) fnBin() string { if v := os.Getenv("FN_BIN"); v != "" { return v } candidate := d.root + "/fn" if _, err := os.Stat(candidate); err == nil { return candidate } if path, err := exec.LookPath("fn"); err == nil { return path } return "fn" }