From 4d0cc3f1fee9541a87b72f812bd54f7e124e6c76 Mon Sep 17 00:00:00 2001 From: Vitor Castro Date: Mon, 31 Aug 2026 00:07:26 +0100 Subject: [PATCH] fix(mcp): speak NDJSON in azure-monitor-mcp for mcp-runtime mcp-runtime hosts stdio as line-delimited JSON; Content-Length-only framing caused initialize to hang until the 120s timeout. --- mcp-runtime/mcps/azure-monitor-mcp/README.md | 10 ++- .../mcps/azure-monitor-mcp/src/index.mjs | 86 ++++++++++++------- 2 files changed, 65 insertions(+), 31 deletions(-) diff --git a/mcp-runtime/mcps/azure-monitor-mcp/README.md b/mcp-runtime/mcps/azure-monitor-mcp/README.md index 9685a27..df383f2 100644 --- a/mcp-runtime/mcps/azure-monitor-mcp/README.md +++ b/mcp-runtime/mcps/azure-monitor-mcp/README.md @@ -66,8 +66,14 @@ See [inbound-mcp-guide.md](../../../docs/inbound-mcp-guide.md) for MCP-first + s ## Local smoke ```bash -node src/index.mjs -# speak MCP Content-Length JSON-RPC on stdin (or use ContextMemory mcp/test) +# NDJSON (same framing as ContextMemory mcp-runtime) +printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' \ + | node src/index.mjs +# expect a single-line JSON initialize result, then tools/list works the same way ``` Zero npm dependencies (Node 18+ `fetch`). + +## Framing note + +ContextMemory `mcp-runtime` speaks **NDJSON** on stdio (not Content-Length). This server writes NDJSON and accepts both NDJSON and Content-Length on stdin so catalog rebuild / `mcp/test` does not hang on `initialize`. diff --git a/mcp-runtime/mcps/azure-monitor-mcp/src/index.mjs b/mcp-runtime/mcps/azure-monitor-mcp/src/index.mjs index f7be9b6..1913b65 100644 --- a/mcp-runtime/mcps/azure-monitor-mcp/src/index.mjs +++ b/mcp-runtime/mcps/azure-monitor-mcp/src/index.mjs @@ -5,9 +5,10 @@ * AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET * LOG_ANALYTICS_WORKSPACE_ID (default workspace) * - * Protocol: JSON-RPC 2.0 with Content-Length framing (MCP stdio). + * Protocol: JSON-RPC 2.0 over stdio. + * - Writes NDJSON (JSON + "\n") — required by ContextMemory mcp-runtime / McpStdioClient. + * - Reads NDJSON and Content-Length framing (dual) for local smoke / standard MCP clients. */ -import { createInterface } from "node:readline"; const TOOLS = [ { @@ -239,10 +240,8 @@ async function callTool(name, args) { } function writeMessage(msg) { - const json = JSON.stringify(msg); - const payload = Buffer.from(json, "utf8"); - const header = Buffer.from(`Content-Length: ${payload.length}\r\n\r\n`, "utf8"); - process.stdout.write(Buffer.concat([header, payload])); + // mcp-runtime Session reads line-delimited JSON (createInterface + JSON.parse). + process.stdout.write(JSON.stringify(msg) + "\n"); } function handleRequest(msg) { @@ -259,7 +258,7 @@ function handleRequest(msg) { return reply({ protocolVersion: "2024-11-05", capabilities: { tools: {} }, - serverInfo: { name: "azure-monitor-mcp", version: "0.1.0" }, + serverInfo: { name: "azure-monitor-mcp", version: "0.1.1" }, }); } @@ -299,34 +298,63 @@ function handleRequest(msg) { } } -/** Content-Length framed reader */ +function dispatchMessage(raw) { + try { + const msg = JSON.parse(raw); + if (msg.method) handleRequest(msg); + } catch (err) { + writeMessage({ + jsonrpc: "2.0", + id: null, + error: { code: -32700, message: `Parse error: ${err.message}` }, + }); + } +} + +/** Dual reader: NDJSON (mcp-runtime) + Content-Length (spec / local clients). */ let buffer = Buffer.alloc(0); process.stdin.on("data", (chunk) => { buffer = Buffer.concat([buffer, chunk]); - while (true) { - const headerEnd = buffer.indexOf("\r\n\r\n"); - if (headerEnd < 0) break; - const header = buffer.slice(0, headerEnd).toString("utf8"); - const match = /Content-Length:\s*(\d+)/i.exec(header); - if (!match) { - buffer = buffer.slice(headerEnd + 4); + while (buffer.length > 0) { + const asText = buffer.toString("utf8"); + const trimmedStart = asText.match(/^\s*/)?.[0].length ?? 0; + const first = asText.slice(trimmedStart, trimmedStart + 1); + + // Content-Length framing + if (/^content-length:/i.test(asText.slice(trimmedStart))) { + const headerEnd = buffer.indexOf("\r\n\r\n"); + if (headerEnd < 0) break; + const header = buffer.slice(0, headerEnd).toString("utf8"); + const match = /Content-Length:\s*(\d+)/i.exec(header); + if (!match) { + buffer = buffer.slice(headerEnd + 4); + continue; + } + const len = Number(match[1]); + const total = headerEnd + 4 + len; + if (buffer.length < total) break; + const body = buffer.slice(headerEnd + 4, total).toString("utf8"); + buffer = buffer.slice(total); + dispatchMessage(body); continue; } - const len = Number(match[1]); - const total = headerEnd + 4 + len; - if (buffer.length < total) break; - const body = buffer.slice(headerEnd + 4, total).toString("utf8"); - buffer = buffer.slice(total); - try { - const msg = JSON.parse(body); - if (msg.method) handleRequest(msg); - } catch (err) { - writeMessage({ - jsonrpc: "2.0", - id: null, - error: { code: -32700, message: `Parse error: ${err.message}` }, - }); + + // NDJSON / line-delimited JSON (ContextMemory mcp-runtime) + if (first === "{" || first === "[") { + const nl = buffer.indexOf(0x0a); // \n + if (nl < 0) break; + const line = buffer.slice(0, nl).toString("utf8").replace(/\r$/, "").trim(); + buffer = buffer.slice(nl + 1); + if (line) dispatchMessage(line); + continue; + } + + // Skip leading junk / incomplete header wait + if (trimmedStart > 0) { + buffer = buffer.slice(trimmedStart); + continue; } + break; } });