Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions mcp-runtime/mcps/azure-monitor-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
86 changes: 57 additions & 29 deletions mcp-runtime/mcps/azure-monitor-mcp/src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -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) {
Expand All @@ -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" },
});
}

Expand Down Expand Up @@ -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;
}
});

Expand Down
Loading