From 8ca7199847d36adcb708d4cc7cc90f3d3caa7e75 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 20 Mar 2026 21:22:06 +0100 Subject: [PATCH] Add Content-Length framed transport support (OpenCode compatibility) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some MCP clients (OpenCode, some VS Code extensions) use LSP-style Content-Length framing instead of bare JSONL: Content-Length: 123\r\n\r\n{"jsonrpc":"2.0",...} The server now auto-detects the transport: if a line starts with "Content-Length:", it reads the header, skips the blank separator, reads the exact payload, and responds with Content-Length framing. Regular JSONL clients are unaffected — the detection only triggers on the Content-Length prefix. Fixes #78. --- src/mcp/mcp.c | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index fd02c8d13..bdfdae8b5 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -2406,7 +2406,7 @@ int cbm_mcp_server_run(cbm_mcp_server_t *srv, FILE *in, FILE *out) { break; } - /* Trim trailing newline */ + /* Trim trailing newline/CR */ size_t len = strlen(line); while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { line[--len] = '\0'; @@ -2415,6 +2415,49 @@ int cbm_mcp_server_run(cbm_mcp_server_t *srv, FILE *in, FILE *out) { continue; } + /* Content-Length framing support (LSP-style transport). + * Some MCP clients (OpenCode, VS Code extensions) send: + * Content-Length: \r\n\r\n + * instead of bare JSONL. Detect the header, read the payload, + * and respond with the same framing. */ + if (strncmp(line, "Content-Length:", 15) == 0) { + int content_len = (int)strtol(line + 15, NULL, 10); + if (content_len <= 0 || content_len > 10 * 1024 * 1024) { + continue; /* invalid or too large */ + } + + /* Skip blank line(s) between header and body */ + while (cbm_getline(&line, &cap, in) > 0) { + size_t hlen = strlen(line); + while (hlen > 0 && (line[hlen - 1] == '\n' || line[hlen - 1] == '\r')) { + line[--hlen] = '\0'; + } + if (hlen == 0) { + break; /* found the blank separator */ + } + /* Skip other headers (e.g. Content-Type) */ + } + + /* Read exact content_len bytes */ + char *body = malloc((size_t)content_len + 1); + if (!body) { + continue; + } + size_t nread = fread(body, 1, (size_t)content_len, in); + body[nread] = '\0'; + + char *resp = cbm_mcp_server_handle(srv, body); + free(body); + + if (resp) { + size_t rlen = strlen(resp); + (void)fprintf(out, "Content-Length: %zu\r\n\r\n%s", rlen, resp); + (void)fflush(out); + free(resp); + } + continue; + } + char *resp = cbm_mcp_server_handle(srv, line); if (resp) { // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)