// Copyright 2026 MarketAlly. All rights reserved. // SPDX-License-Identifier: MIT // GitCaddy MCP Server - Model Context Protocol server for GitCaddy/Gitea Actions // // This standalone server implements the MCP protocol over stdio, // proxying requests to a GitCaddy instance's /api/v2/mcp endpoint. // // Usage: // // gitcaddy-mcp-server --url https://git.example.com --token YOUR_API_TOKEN // // Configure in Claude Code's settings.json: // // { // "mcpServers": { // "gitcaddy": { // "command": "gitcaddy-mcp-server", // "args": ["--url", "https://git.example.com", "--token", "YOUR_TOKEN"] // } // } // } package main import ( "bufio" "bytes" "encoding/json" "flag" "fmt" "io" "net/http" "os" "time" ) var ( giteaURL string giteaToken string debug bool ) func main() { flag.StringVar(&giteaURL, "url", "", "GitCaddy server URL (e.g., https://git.example.com)") flag.StringVar(&giteaToken, "token", "", "GitCaddy API token") flag.BoolVar(&debug, "debug", false, "Enable debug logging to stderr") flag.Parse() // Check environment variables (GITCADDY_* preferred, GITEA_* for backwards compat) if giteaURL == "" { giteaURL = os.Getenv("GITCADDY_URL") } if giteaURL == "" { giteaURL = os.Getenv("GITEA_URL") } if giteaToken == "" { giteaToken = os.Getenv("GITCADDY_TOKEN") } if giteaToken == "" { giteaToken = os.Getenv("GITEA_TOKEN") } if giteaURL == "" { fmt.Fprintln(os.Stderr, "Error: --url or GITCADDY_URL is required") os.Exit(1) } debugLog("GitCaddy MCP Server starting") debugLog("Connecting to: %s", giteaURL) // Read JSON-RPC messages from stdin, forward to Gitea, write responses to stdout reader := bufio.NewReader(os.Stdin) for { line, err := reader.ReadBytes('\n') if err != nil { if err == io.EOF { debugLog("EOF received, exiting") break } debugLog("Read error: %v", err) continue } line = bytes.TrimSpace(line) if len(line) == 0 { continue } debugLog("Received: %s", string(line)) // Forward to Gitea's MCP endpoint response, err := forwardToGitea(line) if err != nil { debugLog("Forward error: %v", err) // Send error response errorResp := map[string]interface{}{ "jsonrpc": "2.0", "id": nil, "error": map[string]interface{}{ "code": -32603, "message": "Internal error", "data": err.Error(), }, } writeResponse(errorResp) continue } debugLog("Response: %s", string(response)) // Write response to stdout fmt.Println(string(response)) } } func forwardToGitea(request []byte) ([]byte, error) { mcpURL := giteaURL + "/api/v2/mcp" req, err := http.NewRequest("POST", mcpURL, bytes.NewReader(request)) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") if giteaToken != "" { req.Header.Set("Authorization", "token "+giteaToken) } client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("http request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("http status %d: %s", resp.StatusCode, string(body)) } return body, nil } func writeResponse(resp interface{}) { data, _ := json.Marshal(resp) fmt.Println(string(data)) } func debugLog(format string, args ...interface{}) { if debug { fmt.Fprintf(os.Stderr, "[DEBUG] "+format+"\n", args...) } }