Skip to content

Commit 000fee2

Browse files
committed
Bump plumcp + fix streaming error
1 parent 40c068b commit 000fee2

7 files changed

Lines changed: 48 additions & 61 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Bump plumcp to 0.2.0-beta5.
6+
- Fix auto-continue clobbering new prompt status and losing the stop button.
7+
58
## 0.116.5
69

710
- Add `/remote` command to display connection URL, password and setup guide. Password is no longer shown in logs or welcome message.

deps-lock.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,24 +1063,24 @@
10631063
"hash": "sha256-RLTLjpPU9rJiwE7Qdx1w3WbnbUXX/HVYIGcaYmVcVDk="
10641064
},
10651065
{
1066-
"mvn-path": "io/github/plumce/plumcp.core-json-cheshire/0.2.0-beta4/plumcp.core-json-cheshire-0.2.0-beta4.jar",
1066+
"mvn-path": "io/github/plumce/plumcp.core-json-cheshire/0.2.0-beta5/plumcp.core-json-cheshire-0.2.0-beta5.jar",
10671067
"mvn-repo": "https://repo.clojars.org/",
1068-
"hash": "sha256-Go75A0bKRlFzzrFYnYqOzZTvZMBIQUC0Up8o5hpKOME="
1068+
"hash": "sha256-a1tj4UByfJ10IpN/n608gLCyJim5vDyWiKl2ZtZL8CU="
10691069
},
10701070
{
1071-
"mvn-path": "io/github/plumce/plumcp.core-json-cheshire/0.2.0-beta4/plumcp.core-json-cheshire-0.2.0-beta4.pom",
1071+
"mvn-path": "io/github/plumce/plumcp.core-json-cheshire/0.2.0-beta5/plumcp.core-json-cheshire-0.2.0-beta5.pom",
10721072
"mvn-repo": "https://repo.clojars.org/",
1073-
"hash": "sha256-f9YSqBznPLeapNXmEAG0J0ao9j8lphA+AE074mw3Txw="
1073+
"hash": "sha256-QdJ6GMZuXdwVflLLTDnb5qHIUrWoJ26CwPgYYfnoPZg="
10741074
},
10751075
{
1076-
"mvn-path": "io/github/plumce/plumcp.core/0.2.0-beta4/plumcp.core-0.2.0-beta4.jar",
1076+
"mvn-path": "io/github/plumce/plumcp.core/0.2.0-beta5/plumcp.core-0.2.0-beta5.jar",
10771077
"mvn-repo": "https://repo.clojars.org/",
1078-
"hash": "sha256-veMNSRikNa9wpbP4tq1aIyqbXybFMFjFj1pzmahd/h0="
1078+
"hash": "sha256-ROriVBoqitK73+BAdW0kcdI0QBoNZ2QNZlek+5imj4g="
10791079
},
10801080
{
1081-
"mvn-path": "io/github/plumce/plumcp.core/0.2.0-beta4/plumcp.core-0.2.0-beta4.pom",
1081+
"mvn-path": "io/github/plumce/plumcp.core/0.2.0-beta5/plumcp.core-0.2.0-beta5.pom",
10821082
"mvn-repo": "https://repo.clojars.org/",
1083-
"hash": "sha256-pdR9owenFET4Kz09C3GXWEqqvFet96OvKKIqy9bbF0A="
1083+
"hash": "sha256-XGSRNsxeuPjG5xrJtecWEWauTRfrEY9Q+Sq7ApK1qC0="
10841084
},
10851085
{
10861086
"mvn-path": "io/methvin/directory-watcher/0.17.3/directory-watcher-0.17.3.jar",

deps.edn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
org.clojure/core.async {:mvn/version "1.8.741"}
44
org.babashka/cli {:mvn/version "0.8.65"}
55
com.github.clojure-lsp/jsonrpc4clj {:mvn/version "1.0.2"}
6-
io.github.plumce/plumcp.core-json-cheshire {:mvn/version "0.2.0-beta4"}
6+
io.github.plumce/plumcp.core-json-cheshire {:mvn/version "0.2.0-beta5"}
77
org.yaml/snakeyaml {:mvn/version "2.4"} ;; used by eca.shared for YAML parsing
88
borkdude/dynaload {:mvn/version "0.3.5"}
99
dev.ericdallo/rewrite-json {:mvn/version "0.1.1"}

src/eca/features/chat.clj

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@
577577
:premature? (:premature? msg)
578578
:truncated? (truncated-response? response-text)})
579579
(lifecycle/send-content! chat-ctx :system
580-
{:type :text :text "\n\nResponse was interrupted. Continuing..."})
580+
{:type :progress :state :running :text "Response interrupted, continuing..."})
581581
(swap! db* assoc-in [:chats chat-id :auto-compacting?] true)
582582
(lifecycle/finish-chat-prompt! :idle
583583
(assoc chat-ctx :on-finished-side-effect
@@ -736,7 +736,7 @@
736736
(logger/info logger-tag "Transient error during response, auto-continuing"
737737
{:chat-id chat-id :error-type error-type})
738738
(lifecycle/send-content! chat-ctx :system
739-
{:type :text :text (str "\n\n" (or message "Connection interrupted") ". Continuing...")})
739+
{:type :progress :state :running :text (str (or message "Connection interrupted") ", continuing...")})
740740
(swap! db* assoc-in [:chats chat-id :auto-compacting?] true)
741741
(lifecycle/finish-chat-prompt! :idle
742742
(assoc chat-ctx :on-finished-side-effect
@@ -760,7 +760,8 @@
760760
(lifecycle/send-content! chat-ctx :system {:type :text :text (str "\n\n" "Error: " (or (ex-message e) (.getName (class e))))})
761761
(lifecycle/finish-chat-prompt! :idle (dissoc chat-ctx :on-finished-side-effect))))
762762
(finally
763-
(when (contains? #{:stopping :running} (get-in @db* [:chats chat-id :status]))
763+
(when (and (= prompt-id (get-in @db* [:chats chat-id :prompt-id]))
764+
(contains? #{:stopping :running} (get-in @db* [:chats chat-id :status])))
764765
(swap! db* assoc-in [:chats chat-id :status] :idle)
765766
(messenger/chat-status-changed (:messenger chat-ctx) {:chat-id chat-id :status :idle})
766767
(db/update-workspaces-cache! @db* metrics))))))))))
@@ -1010,6 +1011,9 @@
10101011
(defn prompt-stop
10111012
[{:keys [chat-id]} db* messenger metrics]
10121013
(when (identical? :running (get-in @db* [:chats chat-id :status]))
1014+
;; Set :stopping immediately to prevent race with stream callbacks
1015+
;; that check status via assert-chat-not-stopped! or cancelled?
1016+
(swap! db* assoc-in [:chats chat-id :status] :stopping)
10131017
(let [chat-ctx {:chat-id chat-id
10141018
:db* db*
10151019
:metrics metrics

src/eca/features/chat/lifecycle.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@
8989
:state :finished})
9090
(when-not (get-in @db* [:chats chat-id :created-at])
9191
(swap! db* assoc-in [:chats chat-id :created-at] (System/currentTimeMillis))))
92-
(when on-finished-side-effect
92+
(when (and on-finished-side-effect
93+
(not (identical? :stopping (get-in @db* [:chats chat-id :status]))))
9394
(on-finished-side-effect))
9495
(db/update-workspaces-cache! @db* metrics)))
9596

src/eca/features/tools/mcp.clj

Lines changed: 26 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -82,42 +82,7 @@
8282
(when (string/includes? url "/sse")
8383
(logger/warn logger-tag (format "SSE transport is no longer supported for server '%s'. Using Streamable HTTP instead. Consider updating the URL." server-name)))
8484
(logger/info logger-tag (format "Creating HTTP transport for server '%s' at %s" server-name url))
85-
{:transport (phct/make-streamable-http-transport
86-
hc
87-
:on-other-response (fn [response]
88-
(let [status (long (:status response))
89-
read-body (fn []
90-
(try
91-
(let [body* (atom nil)]
92-
(when-let [on-msg (:on-msg response)]
93-
(on-msg (fn [text] (reset! body* text))))
94-
(when-let [body @body*]
95-
(subs body 0 (min (count body) 1000))))
96-
(catch Exception _ nil)))]
97-
(cond
98-
(= 202 status) nil
99-
100-
(or (<= 400 status 499)
101-
(<= 500 status))
102-
(let [body (read-body)
103-
;; 405 with body mentioning GET means server doesn't support
104-
;; SSE streaming, which is fine — it still works via POST.
105-
sse-unsupported? (and (= 405 status)
106-
body
107-
(string/includes? body "GET"))]
108-
(if sse-unsupported?
109-
(logger/info logger-tag
110-
(format "MCP server '%s' does not support SSE streaming, continuing with POST only"
111-
server-name))
112-
(do (logger/warn logger-tag
113-
(format "MCP server '%s' returned %d, re-initializing.%s"
114-
server-name status
115-
(if body (str " Response: " body) "")))
116-
(reset! needs-reinit?* true))))
117-
118-
:else
119-
(logger/warn logger-tag (format "Unexpected HTTP response from MCP server '%s': %d"
120-
server-name status))))))
85+
{:transport (phct/make-streamable-http-transport hc)
12186
:needs-reinit?* needs-reinit?*})
12287

12388
;; STDIO transport
@@ -240,8 +205,10 @@
240205
[jsonrpc-error]
241206
(let [code (:code jsonrpc-error)
242207
message (:message jsonrpc-error)
243-
data (:data jsonrpc-error)]
208+
data (:data jsonrpc-error)
209+
http-status (:plumcp.core/http-status jsonrpc-error)]
244210
(cond-> ""
211+
http-status (str "http=" http-status " ")
245212
code (str "code=" code)
246213
message (str (when code " ") "message=" message)
247214
data (str " data=" (pr-str data))
@@ -590,7 +557,7 @@
590557
(:mcp-clients db)))
591558

592559
(defn ^:private reinitialize-server!
593-
"Re-initialize an MCP server after a transport error (HTTP 404/5xx).
560+
"Re-initialize an MCP server after a transport error (HTTP 401/403/5xx).
594561
Stops the old transport without attempting disconnect (the session is already
595562
gone server-side), then runs a fresh initialize-server! cycle."
596563
[server-name old-client db* config metrics]
@@ -605,18 +572,30 @@
605572
{:error true
606573
:contents [{:type :text :text msg}]})
607574

575+
(defn ^:private reinit-worthy-http-status?
576+
"HTTP status codes that indicate the session/auth is broken and the server
577+
connection should be re-initialized (e.g. expired token, server error)."
578+
[status]
579+
(or (= 401 (long status))
580+
(= 403 (long status))
581+
(<= 500 (long status))))
582+
608583
(defn ^:private do-call-tool
609584
"Execute a tool call. Delegates timeout handling to plumcp via :timeout-millis.
610-
HTTP 400/404/500 errors are returned as JSON-RPC error responses by plumcp,
611-
so no polling loop is needed."
612-
[mcp-client name arguments]
585+
All non-200 HTTP errors are returned as JSON-RPC errors by plumcp with
586+
:plumcp.core/http-status on the error map."
587+
[mcp-client name arguments needs-reinit?*]
613588
(locking mcp-client
614589
(let [error-msg* (atom nil)
615590
call-opts {:timeout-millis tool-call-timeout-ms
616591
:on-error (fn [_id jsonrpc-error]
617592
(let [msg (or (:message jsonrpc-error) "Unknown JSON-RPC error")]
618593
(logger/warn logger-tag "Error calling tool:" (format-jsonrpc-error jsonrpc-error))
619-
(reset! error-msg* msg))
594+
(reset! error-msg* msg)
595+
(when-let [http-status (:plumcp.core/http-status jsonrpc-error)]
596+
(when (and needs-reinit?* (reinit-worthy-http-status? http-status))
597+
(logger/warn logger-tag (format "HTTP %d error, flagging for re-initialization" http-status))
598+
(reset! needs-reinit?* true))))
620599
nil)}
621600
result (try
622601
(pmc/call-tool mcp-client name arguments call-opts)
@@ -643,7 +622,7 @@
643622
(defn ^:private reinit-and-call-tool! [server-name mcp-client db* config metrics name arguments]
644623
(reinitialize-server! server-name mcp-client db* config metrics)
645624
(if-let [new-client (get-in @db* [:mcp-clients server-name :client])]
646-
(do-call-tool new-client name arguments)
625+
(do-call-tool new-client name arguments nil)
647626
(tool-call-error (format "Failed to re-initialize MCP server '%s'" server-name))))
648627

649628
(defn call-tool! [name arguments {:keys [db db* config metrics]}]
@@ -654,15 +633,15 @@
654633
[sn client needs-reinit?*])))
655634
first)]
656635
(if (and needs-reinit?* @needs-reinit?* db* config metrics)
657-
;; Already flagged (e.g. GET stream 5xx) — reinit before attempting the call
636+
;; Already flagged — reinit before attempting the call
658637
(reinit-and-call-tool! server-name mcp-client db* config metrics name arguments)
659-
(let [result (do-call-tool mcp-client name arguments)]
638+
(let [result (do-call-tool mcp-client name arguments needs-reinit?*)]
660639
(cond
661640
;; nil = transient transport error, retry once
662641
(nil? result)
663-
(do-call-tool mcp-client name arguments)
642+
(do-call-tool mcp-client name arguments needs-reinit?*)
664643

665-
;; Flagged during the call (e.g. GET stream 404/5xx) — reinit and retry
644+
;; Flagged during the call (e.g. HTTP 401/403/5xx) — reinit and retry
666645
(and (:error result) needs-reinit?* @needs-reinit?* db* config metrics)
667646
(reinit-and-call-tool! server-name mcp-client db* config metrics name arguments)
668647

test/eca/features/tools/mcp_test.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@
140140
(with-redefs [phc/make-http-client (fn [_url {:keys [request-middleware]}]
141141
(reset! captured-rm* request-middleware)
142142
:mock-http-client)
143-
phct/make-streamable-http-transport (fn [_hc & _opts] :mock-transport)]
143+
phct/make-streamable-http-transport (fn [_hc] :mock-transport)]
144144
(->transport "test-server" {:url "https://example.com/mcp"} [] db*)))
145145
(let [rm @captured-rm*]
146146
(is (some? rm) "middleware should have been captured")

0 commit comments

Comments
 (0)