stdio_server.py
python
sha256:1ccb8409daa5aabe577bd26d11b56ed12f3376d64011d0e75a247e81211a66ee
docs(mwp4/phase5): tick Phase 5 checkboxes, close musehub#109
Sonnet 4.6
21 days ago
| 1 | """MuseHub MCP stdio transport — local dev and Cursor integration. |
| 2 | |
| 3 | Entry point: ``python -m musehub.mcp.stdio_server`` |
| 4 | |
| 5 | Protocol: |
| 6 | - Reads newline-delimited JSON-RPC 2.0 messages from ``stdin`` |
| 7 | - Writes JSON-RPC 2.0 responses to ``stdout`` (one per line) |
| 8 | - Logs diagnostic messages to ``stderr`` |
| 9 | - Processes messages sequentially (no concurrency concerns) |
| 10 | - Notifications (no ``id``) produce no output (202-equivalent silence) |
| 11 | |
| 12 | Auth: |
| 13 | - No auth required — stdio is a trusted local process. |
| 14 | - All tools including write tools are available. |
| 15 | - ``user_id`` defaults to ``"stdio-user"`` so write tools can record an author. |
| 16 | |
| 17 | Usage: |
| 18 | # Direct: |
| 19 | python -m musehub.mcp.stdio_server |
| 20 | |
| 21 | # Via Cursor (.cursor/mcp.json): |
| 22 | { |
| 23 | "mcpServers": { |
| 24 | "musehub": { |
| 25 | "command": "python", |
| 26 | "args": ["-m", "musehub.mcp.stdio_server"], |
| 27 | "env": {"DATABASE_URL": "postgresql+asyncpg://..."} |
| 28 | } |
| 29 | } |
| 30 | } |
| 31 | """ |
| 32 | |
| 33 | import asyncio |
| 34 | import json |
| 35 | import logging |
| 36 | import sys |
| 37 | |
| 38 | from musehub.types.json_types import JSONObject |
| 39 | |
| 40 | logger = logging.getLogger("musehub.mcp.stdio") |
| 41 | |
| 42 | |
| 43 | async def _run() -> None: |
| 44 | """Main async stdio loop.""" |
| 45 | from musehub.db import init_db |
| 46 | from musehub.mcp.dispatcher import handle_request |
| 47 | |
| 48 | # Initialise DB before handling any requests. |
| 49 | try: |
| 50 | await init_db() |
| 51 | logger.info("MuseHub MCP stdio server started") |
| 52 | except Exception as exc: |
| 53 | logger.warning("DB init failed (tools requiring DB will return db_unavailable): %s", exc) |
| 54 | |
| 55 | loop = asyncio.get_event_loop() |
| 56 | reader = asyncio.StreamReader() |
| 57 | protocol = asyncio.StreamReaderProtocol(reader) |
| 58 | await loop.connect_read_pipe(lambda: protocol, sys.stdin) |
| 59 | |
| 60 | stdout_transport, stdout_protocol = await loop.connect_write_pipe( |
| 61 | asyncio.BaseProtocol, sys.stdout |
| 62 | ) |
| 63 | |
| 64 | while True: |
| 65 | try: |
| 66 | line_bytes = await reader.readline() |
| 67 | except asyncio.IncompleteReadError: |
| 68 | break |
| 69 | if not line_bytes: |
| 70 | break |
| 71 | |
| 72 | line = line_bytes.decode("utf-8").strip() |
| 73 | if not line: |
| 74 | continue |
| 75 | |
| 76 | try: |
| 77 | raw = json.loads(line) |
| 78 | except json.JSONDecodeError as exc: |
| 79 | error_resp = { |
| 80 | "jsonrpc": "2.0", |
| 81 | "id": None, |
| 82 | "error": {"code": -32700, "message": f"Parse error: {exc}"}, |
| 83 | } |
| 84 | _write_json(stdout_transport, error_resp) |
| 85 | continue |
| 86 | |
| 87 | try: |
| 88 | resp = await handle_request(raw, user_id="stdio-user") |
| 89 | except Exception as exc: |
| 90 | logger.exception("Dispatcher error: %s", exc) |
| 91 | resp = { |
| 92 | "jsonrpc": "2.0", |
| 93 | "id": raw.get("id"), |
| 94 | "error": {"code": -32603, "message": f"Internal error: {exc}"}, |
| 95 | } |
| 96 | |
| 97 | if resp is not None: |
| 98 | _write_json(stdout_transport, resp) |
| 99 | |
| 100 | |
| 101 | def _write_json(transport: asyncio.BaseTransport, obj: JSONObject) -> None: |
| 102 | """Serialise ``obj`` to JSON and write it as a single newline-terminated line.""" |
| 103 | line = f"{json.dumps(obj, default=str)}\n" |
| 104 | if isinstance(transport, asyncio.WriteTransport): |
| 105 | transport.write(line.encode("utf-8")) |
| 106 | |
| 107 | |
| 108 | def main() -> None: |
| 109 | """Stdio server entry point.""" |
| 110 | logging.basicConfig( |
| 111 | level=logging.INFO, |
| 112 | format="%(asctime)s %(levelname)s %(name)s: %(message)s", |
| 113 | stream=sys.stderr, |
| 114 | ) |
| 115 | asyncio.run(_run()) |
| 116 | |
| 117 | |
| 118 | if __name__ == "__main__": |
| 119 | main() |
File History
2 commits
sha256:1ccb8409daa5aabe577bd26d11b56ed12f3376d64011d0e75a247e81211a66ee
docs(mwp4/phase5): tick Phase 5 checkboxes, close musehub#109
Sonnet 4.6
21 days ago
sha256:2c523da45351334b5c4dbefed4dc3dd553b3faa8737a4e6caf301e5dc82141be
test(mwp4): Phase 0 RED reproduction tests for RC-4 ordering race
Sonnet 4.6
21 days ago