char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Xuan-Son Nguyenserver: support MCP stdio (#26062)20455a4ad

char/aliceai-t5-moe
3.1 KiB100 linesraw
1#!/usr/bin/env python3
2"""
3MCP server (NDJSON JSON-RPC over stdio) that spawns a long-lived grandchild which inherits
4this process's stdin/stdout/stderr and keeps them open.
5
6This reproduces the reader-teardown deadlock: killing the direct MCP child (SIGKILL, which is
7all subprocess_terminate() does) does NOT close the stdout/stderr pipe write ends, because the
8grandchild still holds them. A server that reads those pipes with a blocking read would then
9wait forever for an EOF that never arrives, hanging teardown (both warmup shutdown at startup
10and process shutdown). The polled, running-aware reader must exit regardless.
11"""
12import json
13import os
14import subprocess
15import sys
16
17# Spawn a grandchild that inherits our std handles (fds 0/1/2 = the MCP pipes) and lives well
18# past any teardown in the tests. We do NOT redirect its stdio, so it keeps the pipe write ends
19# open even after this process is killed.
20subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"])
21
22TOOLS = [
23    {
24        "name": "echo",
25        "description": "Echo back the input message",
26        "inputSchema": {
27            "type": "object",
28            "properties": {"message": {"type": "string", "description": "Message to echo"}},
29            "required": ["message"],
30        },
31    }
32]
33
34
35def handle_initialize(params, req_id):
36    return {
37        "jsonrpc": "2.0",
38        "id": req_id,
39        "result": {
40            "protocolVersion": "2024-11-05",
41            "capabilities": {"tools": {}},
42            "serverInfo": {"name": "grandchild-test", "version": "1.0"},
43        },
44    }
45
46
47def handle_tools_list(params, req_id):
48    return {"jsonrpc": "2.0", "id": req_id, "result": {"tools": TOOLS}}
49
50
51def handle_tools_call(params, req_id):
52    if params.get("name") == "echo":
53        message = params.get("arguments", {}).get("message", "")
54        return {
55            "jsonrpc": "2.0",
56            "id": req_id,
57            "result": {"content": [{"type": "text", "text": f"echo: {message}"}]},
58        }
59    return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32602, "message": "Unknown tool"}}
60
61
62HANDLERS = {
63    "initialize": handle_initialize,
64    "tools/list": handle_tools_list,
65    "tools/call": handle_tools_call,
66}
67
68
69def main():
70    sys.stdout = os.fdopen(sys.stdout.fileno(), "w", buffering=1)
71    sys.stderr = os.fdopen(sys.stderr.fileno(), "w", buffering=1)
72
73    for line in sys.stdin:
74        line = line.strip()
75        if not line:
76            continue
77        try:
78            request = json.loads(line)
79        except json.JSONDecodeError:
80            continue
81
82        method = request.get("method")
83        req_id = request.get("id")
84        params = request.get("params", {})
85
86        if req_id is None:
87            continue  # notification, no response
88
89        handler = HANDLERS.get(method)
90        if handler:
91            response = handler(params, req_id)
92        else:
93            response = {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Method not found: {method}"}}
94
95        sys.stdout.write(json.dumps(response) + "\n")
96        sys.stdout.flush()
97
98
99if __name__ == "__main__":
100    main()