2
0

2 Commits

Author SHA1 Message Date
4b350fe967 fix(mcp): use Content-Length framing for all responses
All checks were successful
Release / build (amd64, linux) (push) Successful in 38s
Release / build (amd64, windows) (push) Successful in 38s
Release / build (arm64, darwin) (push) Successful in 44s
Release / build (amd64, darwin) (push) Successful in 1m4s
Release / build (arm64, linux) (push) Successful in 1m14s
Release / release (push) Successful in 21s
Adds writeFramed helper to send responses with HTTP-style Content-Length headers instead of raw JSON lines. Ensures compatibility with Claude Code which expects framed messages. Updates both main loop and writeResponse to use consistent framing.
2026-04-05 02:48:24 -04:00
1d770b45c9 feat(mcp): add support for Content-Length framed JSON-RPC messages
All checks were successful
Release / build (amd64, windows) (push) Successful in 41s
Release / build (arm64, darwin) (push) Successful in 42s
Release / build (amd64, linux) (push) Successful in 46s
Release / build (arm64, linux) (push) Successful in 1m11s
Release / release (push) Successful in 19s
Release / build (amd64, darwin) (push) Successful in 1m4s
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.
2026-04-05 02:24:17 -04:00
2 changed files with 74 additions and 4 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*.exe
*.dll
*.so
*.dylib

74
main.go
View File

@@ -69,10 +69,11 @@ func main() {
debugLog("Connecting to: %s", giteaURL) debugLog("Connecting to: %s", giteaURL)
// Read JSON-RPC messages from stdin, forward to Gitea, write responses to stdout // 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) reader := bufio.NewReader(os.Stdin)
for { for {
line, err := reader.ReadBytes('\n') line, err := readMessage(reader)
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
debugLog("EOF received, exiting") debugLog("EOF received, exiting")
@@ -109,8 +110,8 @@ func main() {
debugLog("Response: %s", string(response)) debugLog("Response: %s", string(response))
// Write response to stdout // Write response to stdout with Content-Length framing
fmt.Println(string(response)) writeFramed(response)
} }
} }
@@ -147,9 +148,74 @@ func forwardToGitea(request []byte) ([]byte, error) {
return body, nil 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{}) { func writeResponse(resp interface{}) {
data, _ := json.Marshal(resp) data, _ := json.Marshal(resp)
fmt.Println(string(data)) writeFramed(data)
}
func writeFramed(data []byte) {
fmt.Fprintf(os.Stdout, "Content-Length: %d\r\n\r\n%s", len(data), data)
} }
func debugLog(format string, args ...interface{}) { func debugLog(format string, args ...interface{}) {