MCPcopy Index your code
hub / github.com/github/copilot-sdk / JsonRpcClient

Class JsonRpcClient

python/copilot/_jsonrpc.py:65–478  ·  view source on GitHub ↗

Minimal async JSON-RPC 2.0 client for stdio transport Uses threads for blocking IO but provides async interface.

Source from the content-addressed store, hash-verified

63
64
65class JsonRpcClient:
66 """
67 Minimal async JSON-RPC 2.0 client for stdio transport
68
69 Uses threads for blocking IO but provides async interface.
70 """
71
72 def __init__(self, process):
73 """
74 Create client from subprocess.Popen with stdin/stdout pipes
75
76 Args:
77 process: subprocess.Popen with stdin=PIPE, stdout=PIPE
78 """
79 self.process = process
80 self.pending_requests: dict[str, asyncio.Future] = {}
81 self._pending_inline_callbacks: dict[str, Callable[[Any], None]] = {}
82 self.notification_handler: Callable[[str, dict], None] | None = None
83 self.request_handlers: dict[str, RequestHandler] = {}
84 self._running = False
85 self._read_thread: threading.Thread | None = None
86 self._stderr_thread: threading.Thread | None = None
87 self._loop: asyncio.AbstractEventLoop | None = None
88 self._write_lock = threading.Lock()
89 self._pending_lock = threading.Lock()
90 self._process_exit_error: str | None = None
91 self._stderr_output: list[str] = []
92 self._stderr_lock = threading.Lock()
93 self.on_close: Callable[[], None] | None = None
94
95 def start(self, loop: asyncio.AbstractEventLoop | None = None):
96 """Start listening for messages in background thread"""
97 if not self._running:
98 self._running = True
99 # Always use the provided loop or get the running loop
100 self._loop = loop or asyncio.get_running_loop()
101 self._read_thread = threading.Thread(target=self._read_loop, daemon=True)
102 self._read_thread.start()
103 # Start stderr reader thread if process has stderr
104 if hasattr(self.process, "stderr") and self.process.stderr:
105 self._stderr_thread = threading.Thread(target=self._stderr_loop, daemon=True)
106 self._stderr_thread.start()
107
108 def _stderr_loop(self):
109 """Read stderr in background to capture error messages"""
110 try:
111 while self._running:
112 if not self.process.stderr:
113 break
114 line = self.process.stderr.readline()
115 if not line:
116 break
117 stderr_line = line.decode("utf-8") if isinstance(line, bytes) else line
118 logger.warning("[CLI] %s", stderr_line.rstrip())
119 with self._stderr_lock:
120 self._stderr_output.append(stderr_line)
121 except Exception:
122 logger.debug("Error reading Copilot CLI stderr", exc_info=True)

Calls

no outgoing calls

Used in the wild real call sites across dependent graphs

searching dependent graphs…