From 1d770b45c92f7c7985edeebb53f57726af6aedc4 Mon Sep 17 00:00:00 2001 From: logikonline Date: Sun, 5 Apr 2026 02:24:17 -0400 Subject: [PATCH] feat(mcp): add support for Content-Length framed JSON-RPC messages Implements readMessage function to handle both raw JSON lines and Content-Length framed messages. Claude Code uses HTTP-style framing (Content-Length: N\r\n\r\n{json}) while other clients may send raw JSON. Auto-detects format by peeking at first bytes. Adds .gitignore for compiled binaries. --- .gitignore | 4 ++++ main.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77d15cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.exe +*.dll +*.so +*.dylib diff --git a/main.go b/main.go index 98e0f54..977f1cb 100644 --- a/main.go +++ b/main.go @@ -69,10 +69,11 @@ func main() { debugLog("Connecting to: %s", giteaURL) // Read JSON-RPC messages from stdin, forward to Gitea, write responses to stdout + // Supports both raw JSON lines and Content-Length framed messages (Claude Code uses framing) reader := bufio.NewReader(os.Stdin) for { - line, err := reader.ReadBytes('\n') + line, err := readMessage(reader) if err != nil { if err == io.EOF { debugLog("EOF received, exiting") @@ -147,6 +148,67 @@ func forwardToGitea(request []byte) ([]byte, error) { return body, nil } +// readMessage reads a JSON-RPC message from stdin. +// Handles both raw JSON lines and Content-Length framed messages. +// Claude Code sends: Content-Length: N\r\n\r\n{json} +func readMessage(reader *bufio.Reader) ([]byte, error) { + // Peek at first bytes to detect format + for { + line, err := reader.ReadBytes('\n') + if err != nil { + return nil, err + } + + trimmed := bytes.TrimSpace(line) + if len(trimmed) == 0 { + continue + } + + // If it starts with '{', it's a raw JSON line + if trimmed[0] == '{' { + return trimmed, nil + } + + // If it starts with "Content-Length:", read the framed message + if bytes.HasPrefix(bytes.ToLower(trimmed), []byte("content-length:")) { + // Parse content length + parts := bytes.SplitN(trimmed, []byte(":"), 2) + if len(parts) != 2 { + continue + } + lengthStr := bytes.TrimSpace(parts[1]) + var contentLength int + fmt.Sscanf(string(lengthStr), "%d", &contentLength) + if contentLength <= 0 { + continue + } + + // Read until empty line (end of headers) + for { + headerLine, err := reader.ReadBytes('\n') + if err != nil { + return nil, err + } + if len(bytes.TrimSpace(headerLine)) == 0 { + break + } + } + + // Read exactly contentLength bytes + body := make([]byte, contentLength) + _, err := io.ReadFull(reader, body) + if err != nil { + return nil, fmt.Errorf("read body: %w", err) + } + + return body, nil + } + + // Unknown line, skip + debugLog("Skipping unknown line: %s", string(trimmed)) + } +} + func writeResponse(resp interface{}) { data, _ := json.Marshal(resp) fmt.Println(string(data))