Files
registry_mcp/tool_run.go
T
2026-05-09 13:29:32 +02:00

80 lines
1.9 KiB
Go

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 <id> [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"
}