From aec727e00699487760e9d4e969a0611b10be72a5 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 27 Jun 2025 20:03:53 -0700 Subject: [PATCH] Initial Go MCP server implementation --- CONTEXT.md | 32 ++++++++++++ go.mod | 5 ++ main.go | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 CONTEXT.md create mode 100644 go.mod create mode 100644 main.go diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..330ddf7 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,32 @@ +# Context - gocp Project + +## CRITICAL INSTRUCTIONS - MUST FOLLOW + +1. **Simple commits**: One-line commit messages ONLY. NEVER add "Generated by Claude" footers, emojis, or any multi-line messages. Just describe what changed in one line. + +2. **Minimal comments**: Only add comments when absolutely critical for disambiguation + +3. **Never use `go build`**: Always use `go run` instead of `go build` for testing Go programs + +4. **Never change directories**: Never change directories with `cd` - always use absolute paths instead + +5. **Error handling**: Always propagate errors with proper messages, never silently handle errors + +## Project Overview +gocp is a Go MCP (Model Context Protocol) server that provides tools for building and executing Go code. It uses the go-mcp library to implement the MCP protocol. + +## Key Files +- `main.go`: MCP server implementation with build_and_run_go tool +- `go.mod`: Module definition with go-mcp dependency + +## Tool Details +- **build_and_run_go**: Executes Go code using `go run` + - Parameters: + - `code` (required): Go source code to execute + - `timeout` (optional): Timeout in seconds (default: 30) + - Returns JSON with: + - `stdout`: Standard output + - `stderr`: Standard error + - `exit_code`: Process exit code + - `error`: Error message if any + - Creates temporary directories with `gocp-*` prefix \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..31aa5fe --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/flamingcow/gocp + +go 1.21 + +require github.com/mark3labs/mcp-go v0.1.0 \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..068b14d --- /dev/null +++ b/main.go @@ -0,0 +1,140 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +type RunResult struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + ExitCode int `json:"exit_code"` + Error string `json:"error,omitempty"` +} + +func main() { + // Create MCP server + s := server.NewMCPServer( + "go-executor", + "1.0.0", + server.WithToolCapabilities(true), + ) + + // Define the build_and_run_go tool + buildAndRunTool := mcp.NewTool( + "build_and_run_go", + mcp.WithDescription("Build and execute Go code"), + mcp.WithString("code", mcp.Required(), mcp.Description("The Go source code to build and run")), + mcp.WithNumber("timeout", mcp.Description("Timeout in seconds (default: 30)")), + ) + + // Add tool handler + s.AddTool(buildAndRunTool, buildAndRunHandler) + + // Start the server + if err := s.Serve(); err != nil { + fmt.Fprintf(os.Stderr, "Server error: %v\n", err) + os.Exit(1) + } +} + +func buildAndRunHandler(ctx context.Context, args map[string]interface{}) (*mcp.CallToolResult, error) { + // Extract code parameter + code, ok := args["code"].(string) + if !ok { + return nil, fmt.Errorf("code parameter is required and must be a string") + } + + // Extract timeout parameter (optional) + timeout := 30.0 + if t, ok := args["timeout"].(float64); ok { + timeout = t + } + + // Build and run the code + stdout, stderr, exitCode, err := buildAndRunGo(code, time.Duration(timeout)*time.Second) + + // Create structured result + result := RunResult{ + Stdout: stdout, + Stderr: stderr, + ExitCode: exitCode, + } + + if err != nil { + result.Error = err.Error() + } + + // Convert to JSON + jsonData, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal result: %w", err) + } + + return mcp.NewCallToolResult( + mcp.NewTextContent(string(jsonData)), + ), nil +} + +func buildAndRunGo(code string, timeout time.Duration) (stdout, stderr string, exitCode int, err error) { + // Create temporary directory + tmpDir, err := os.MkdirTemp("", "gocp-*") + if err != nil { + return "", "", -1, fmt.Errorf("failed to create temp dir: %w", err) + } + defer os.RemoveAll(tmpDir) + + // Write code to temporary file + tmpFile := filepath.Join(tmpDir, "main.go") + if err := os.WriteFile(tmpFile, []byte(code), 0644); err != nil { + return "", "", -1, fmt.Errorf("failed to write code: %w", err) + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + // Initialize go.mod in temp directory + modCmd := exec.CommandContext(ctx, "go", "mod", "init", "temp") + modCmd.Dir = tmpDir + if err := modCmd.Run(); err != nil { + return "", "", -1, fmt.Errorf("failed to initialize go.mod: %w", err) + } + + // Run the code directly with go run + runCmd := exec.CommandContext(ctx, "go", "run", tmpFile) + runCmd.Dir = tmpDir + + // Capture stdout and stderr separately + var stdoutBuf, stderrBuf bytes.Buffer + runCmd.Stdout = &stdoutBuf + runCmd.Stderr = &stderrBuf + + // Run the command + err = runCmd.Run() + + // Get exit code + exitCode = 0 + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + exitCode = exitErr.ExitCode() + err = nil // Clear error since we got the exit code + } else if ctx.Err() == context.DeadlineExceeded { + return stdoutBuf.String(), stderrBuf.String(), -1, fmt.Errorf("execution timeout exceeded") + } else { + // Some other error occurred + return stdoutBuf.String(), stderrBuf.String(), -1, err + } + } + + return stdoutBuf.String(), stderrBuf.String(), exitCode, nil +} \ No newline at end of file