mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
fix(mcp): export_graph failed on every format — convert kg dict, disable progress
The MCP server's export_graph tool was broken on all formats in 0.6.5/0.6.6:
- json: JSONExporter().export(graph) was called without the required
file_path argument -> TypeError surfaced as {"error": ...}.
- RDF branches: RDFExporter().export_to_rdf(graph, ...) received the
ContextGraph object instead of the canonical kg dict -> AttributeError
(ContextGraph has no 'get').
- All branches: the RDF path printed a rich progress bar to stdout,
corrupting the stdio JSON-RPC framing and hanging the client (observed:
300s timeout over MCP while the same call returns in <1s directly).
Fix: convert via ContextGraph.to_kg_dict() before exporting, serialize the
json branch to a string, and force SEMANTICA_DISABLE_PROGRESS=1 for the
server process — stdout is the protocol channel, not a console.
Tests: tests/test_mcp_server_export_graph.py covers every format, the json
payload shape (entities/relationships), and the progress-disable env var.
This commit is contained in:
@@ -62,6 +62,13 @@ logging.basicConfig(stream=sys.stderr, level=_log_level,
|
||||
format="%(asctime)s [semantica-mcp] %(levelname)s %(message)s")
|
||||
log = logging.getLogger("semantica.mcp_server")
|
||||
|
||||
# MCP stdio framing IS stdout: a progress bar or other console renderer writing
|
||||
# to stdout would interleave with the JSON-RPC stream and hang every client
|
||||
# (observed 2026-08-20: export_graph over MCP timed out at 300s while the same
|
||||
# call returned in <1s directly). Force the progress trackers off for this
|
||||
# process — stdout is not a console here.
|
||||
os.environ["SEMANTICA_DISABLE_PROGRESS"] = "1"
|
||||
|
||||
# ── lazy graph session ──────────────────────────────────────────────────────
|
||||
_graph: Any = None
|
||||
|
||||
@@ -265,11 +272,16 @@ def _tool_export_graph(args: dict) -> dict:
|
||||
fmt = args.get("format", "json-ld")
|
||||
graph = _get_graph()
|
||||
try:
|
||||
from semantica.export import RDFExporter, JSONExporter
|
||||
from semantica.export import RDFExporter
|
||||
# The exporters consume the canonical kg dict, not the ContextGraph
|
||||
# object (regression: the old code passed the object straight through,
|
||||
# so every branch failed — JSONExporter.export() with no file_path on
|
||||
# the json branch, AttributeError on the RDF branches).
|
||||
kg = graph.to_kg_dict()
|
||||
if fmt in ("turtle", "ttl", "nt", "xml", "json-ld"):
|
||||
result = RDFExporter().export_to_rdf(graph, format=fmt)
|
||||
result = RDFExporter().export_to_rdf(kg, format=fmt)
|
||||
else:
|
||||
result = JSONExporter().export(graph)
|
||||
result = json.dumps(kg, indent=2, ensure_ascii=False)
|
||||
return {"format": fmt, "data": result}
|
||||
except Exception as exc:
|
||||
return {"error": str(exc)}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Regression tests for the MCP export_graph tool (issue: all branches broken).
|
||||
|
||||
The MCP server's export_graph tool failed on every format in 0.6.5/0.6.6:
|
||||
- json: JSONExporter().export(graph) called without the required file_path
|
||||
argument -> TypeError, surfaced as {"error": ...}
|
||||
- RDF: RDFExporter().export_to_rdf(graph, ...) received the ContextGraph
|
||||
object instead of the canonical kg dict -> AttributeError
|
||||
- all: the RDF path printed a rich progress bar to stdout, corrupting the
|
||||
stdio JSON-RPC framing and hanging the client (observed: 300s
|
||||
timeout over MCP, <1s directly).
|
||||
|
||||
The fix: convert the graph with ContextGraph.to_kg_dict() before handing it to
|
||||
the exporters, serialize json to a string, and force SEMANTICA_DISABLE_PROGRESS
|
||||
for the server process (stdout is the protocol channel, not a console).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from semantica import mcp_server
|
||||
from semantica.context import ContextGraph
|
||||
|
||||
|
||||
def _graph_with_content() -> ContextGraph:
|
||||
graph = ContextGraph(advanced_analytics=True)
|
||||
graph.add_node("n1", node_type="entity", properties={"text": "hello"})
|
||||
graph.add_node("n2", node_type="entity", properties={"text": "world"})
|
||||
graph.add_edge("n1", "n2", "related_to")
|
||||
return graph
|
||||
|
||||
|
||||
class TestExportGraphTool(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._old_graph = mcp_server._graph
|
||||
mcp_server._graph = _graph_with_content()
|
||||
|
||||
def tearDown(self):
|
||||
mcp_server._graph = self._old_graph
|
||||
|
||||
def test_json_branch_returns_string_data_not_error(self):
|
||||
result = mcp_server._tool_export_graph({"format": "json"})
|
||||
self.assertNotIn("error", result)
|
||||
self.assertEqual(result["format"], "json")
|
||||
payload = json.loads(result["data"])
|
||||
self.assertEqual(len(payload["entities"]), 2)
|
||||
self.assertEqual(len(payload["relationships"]), 1)
|
||||
|
||||
def test_jsonld_branch_returns_string_data_not_error(self):
|
||||
result = mcp_server._tool_export_graph({"format": "json-ld"})
|
||||
self.assertNotIn("error", result)
|
||||
self.assertEqual(result["format"], "json-ld")
|
||||
self.assertIsInstance(result["data"], str)
|
||||
self.assertGreater(len(result["data"]), 0)
|
||||
|
||||
def test_turtle_branch_returns_string_data_not_error(self):
|
||||
result = mcp_server._tool_export_graph({"format": "turtle"})
|
||||
self.assertNotIn("error", result)
|
||||
self.assertIsInstance(result["data"], str)
|
||||
self.assertIn("@prefix", result["data"])
|
||||
|
||||
def test_all_rdf_formats_succeed(self):
|
||||
for fmt in ("turtle", "ttl", "nt", "xml", "json-ld"):
|
||||
with self.subTest(fmt=fmt):
|
||||
result = mcp_server._tool_export_graph({"format": fmt})
|
||||
self.assertNotIn("error", result, fmt)
|
||||
self.assertIsInstance(result["data"], str)
|
||||
|
||||
def test_progress_is_disabled_for_the_server_process(self):
|
||||
self.assertEqual(os.environ.get("SEMANTICA_DISABLE_PROGRESS"), "1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user