From 2499a1c3775dbd6338917a3a109da945fdbc3592 Mon Sep 17 00:00:00 2001 From: Safi Date: Sun, 12 Apr 2026 18:48:29 +0100 Subject: [PATCH] fix MCP ValidationError on blank stdin lines and bump to 0.4.5 Some MCP clients send blank lines between JSON messages. The stdio transport tried to parse every line as JSONRPCMessage, crashing with a Pydantic ValidationError. _filter_blank_stdin() installs an OS-level pipe that relays stdin while silently dropping blank-only lines. Closes #201 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 4 ++++ graphify/serve.py | 31 +++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f3a4532..9bb7485c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) +## 0.4.5 (2026-04-12) + +- Fix: MCP server no longer crashes with `ValidationError` on blank lines sent between JSON messages by some clients (#201) + ## 0.4.4 (2026-04-12) - Fix: `watch` now preserves INFERRED/AMBIGUOUS edges (code↔doc rationale links) across rebuilds — previously all cross-type edges were dropped (#261) diff --git a/graphify/serve.py b/graphify/serve.py index 279b5d31..a0778343 100644 --- a/graphify/serve.py +++ b/graphify/serve.py @@ -108,6 +108,36 @@ def _find_node(G: nx.Graph, label: str) -> list[str]: if term in d.get("label", "").lower() or term == nid.lower()] +def _filter_blank_stdin() -> None: + """Filter blank lines from stdin before MCP reads it. + + Some MCP clients (Claude Desktop, etc.) send blank lines between JSON + messages. The MCP stdio transport tries to parse every line as a + JSONRPCMessage, so a bare newline triggers a Pydantic ValidationError. + This installs an OS-level pipe that relays stdin while dropping blanks. + """ + import os + import threading + + r_fd, w_fd = os.pipe() + saved_fd = os.dup(sys.stdin.fileno()) + + def _relay() -> None: + try: + with open(saved_fd, "rb") as src, open(w_fd, "wb") as dst: + for line in src: + if line.strip(): + dst.write(line) + dst.flush() + except Exception: + pass + + threading.Thread(target=_relay, daemon=True).start() + os.dup2(r_fd, sys.stdin.fileno()) + os.close(r_fd) + sys.stdin = open(0, "r", closefd=False) + + def serve(graph_path: str = "graphify-out/graph.json") -> None: """Start the MCP server. Requires pip install mcp.""" try: @@ -325,6 +355,7 @@ def serve(graph_path: str = "graphify-out/graph.json") -> None: async with stdio_server() as streams: await server.run(streams[0], streams[1], server.create_initialization_options()) + _filter_blank_stdin() asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index 3c9f34f9..baae916c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.4.4" +version = "0.4.5" description = "AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, OpenClaw, Factory Droid, Trae) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" readme = "README.md" license = { file = "LICENSE" }