Skip to content

Commit d9975e6

Browse files
BuddhiLWclaude
andcommitted
fix: Use p/future for async handler execution to prevent blocking
The original p/promise implementation ran handlers synchronously on the request processing thread, blocking the main event loop. This caused MCP tool calls to hang indefinitely since responses couldn't be sent until the handler completed. Changes: - Replace p/promise with p/future for async handler execution - Wrap handler in discarding-stdout to prevent logging corruption - Add null-output-stream-writer for stdout suppression This fix is critical for MCP servers where stdout is the JSON-RPC channel. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3469c67 commit d9975e6

1 file changed

Lines changed: 43 additions & 39 deletions

File tree

src/jsonrpc4clj/server.clj

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919
(def null-output-stream-writer
2020
(java.io.OutputStreamWriter.
21-
(proxy [java.io.OutputStream] []
22-
(write
23-
([^bytes b])
24-
([^bytes b, off, len])))))
21+
(proxy [java.io.OutputStream] []
22+
(write
23+
([^bytes b])
24+
([^bytes b, off, len])))))
2525

2626
(defmacro discarding-stdout
2727
"Evaluates body in a context in which writes to *out* are discarded."
@@ -118,10 +118,10 @@
118118
(let [ch (async/chan buf-or-n)]
119119
(async/thread
120120
(discarding-stdout
121-
(loop []
122-
(when-let [arg (async/<!! ch)]
123-
(f arg)
124-
(recur)))))
121+
(loop []
122+
(when-let [arg (async/<!! ch)]
123+
(f arg)
124+
(recur)))))
125125
ch))
126126

127127
(def input-buffer-size
@@ -177,14 +177,18 @@
177177

178178
(defn pending-received-request [method context params]
179179
(let [cancelled? (atom false)
180-
;; coerce result/error to promise
181-
result-promise (p/promise
182-
(receive-request method
183-
(assoc context ::req-cancelled? cancelled?)
184-
params))]
180+
;; Run handler asynchronously to prevent blocking the request processing thread.
181+
;; Using p/future instead of p/promise ensures handlers run on a thread pool,
182+
;; allowing the main loop to continue processing other messages and responses.
183+
;; Wrap in discarding-stdout to prevent handler logging from corrupting JSON-RPC output.
184+
result-promise (p/future
185+
(discarding-stdout
186+
(receive-request method
187+
(assoc context ::req-cancelled? cancelled?)
188+
params)))]
185189
(map->PendingReceivedRequest
186-
{:result-promise result-promise
187-
:cancelled? cancelled?})))
190+
{:result-promise result-promise
191+
:cancelled? cancelled?})))
188192

189193
;; TODO: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize
190194
;; * receive-request should return error until initialize request is received
@@ -256,11 +260,11 @@
256260
;; presumed to be both very rare and indicative of a problem that can
257261
;; be solved only in the client or the language server.
258262
client-initiated-in-ch (thread-loop
259-
input-buffer-size
260-
(fn [[message-type message]]
261-
(if (identical? :request message-type)
262-
(protocols.endpoint/receive-request this context message)
263-
(protocols.endpoint/receive-notification this context message))))
263+
input-buffer-size
264+
(fn [[message-type message]]
265+
(if (identical? :request message-type)
266+
(protocols.endpoint/receive-request this context message)
267+
(protocols.endpoint/receive-notification this context message))))
264268
reject-pending-sent-requests (fn [exception]
265269
(doseq [pending-request (vals @pending-sent-requests*)]
266270
(p/reject! (:p pending-request)
@@ -277,7 +281,7 @@
277281
(when-not (async/offer! client-initiated-in-ch [message-type message])
278282
;; Buffers full. Fail any waiting pending requests and...
279283
(reject-pending-sent-requests
280-
(ex-info "Buffer of client messages exhausted." {}))
284+
(ex-info "Buffer of client messages exhausted." {}))
281285
;; ... try again, but park this time.
282286
(async/>! client-initiated-in-ch [message-type message])))
283287
(recur))
@@ -380,12 +384,12 @@
380384
:result-promise
381385
;; convert result/error to response
382386
(p/then
383-
(fn [result]
384-
(if (identical? ::method-not-found result)
385-
(do
386-
(protocols.endpoint/log this :warn "received unexpected request" method)
387-
(responses/error resp (errors/not-found method)))
388-
(responses/infer resp result))))
387+
(fn [result]
388+
(if (identical? ::method-not-found result)
389+
(do
390+
(protocols.endpoint/log this :warn "received unexpected request" method)
391+
(responses/error resp (errors/not-found method)))
392+
(responses/infer resp result))))
389393
;; Handle
390394
;; 1. Exceptions thrown within p/future created by receive-request.
391395
;; 2. Cancelled requests.
@@ -433,15 +437,15 @@
433437
log-ch (or log-ch (async/chan (async/sliding-buffer 20)))
434438
trace-ch (or trace-ch (async/chan (async/sliding-buffer 20)))]
435439
(map->ChanServer
436-
{:output-ch output-ch
437-
:input-ch input-ch
438-
:log-ch log-ch
439-
:trace-ch trace-ch
440-
:tracer* (atom tracer)
441-
:clock clock
442-
:on-close on-close
443-
:response-executor response-executor
444-
:request-id* (atom 0)
445-
:pending-sent-requests* (atom {})
446-
:pending-received-requests* (atom {})
447-
:join (promise)})))
440+
{:output-ch output-ch
441+
:input-ch input-ch
442+
:log-ch log-ch
443+
:trace-ch trace-ch
444+
:tracer* (atom tracer)
445+
:clock clock
446+
:on-close on-close
447+
:response-executor response-executor
448+
:request-id* (atom 0)
449+
:pending-sent-requests* (atom {})
450+
:pending-received-requests* (atom {})
451+
:join (promise)})))

0 commit comments

Comments
 (0)