Skip to content

Commit 48ae18d

Browse files
committed
Support clojureMCP dry-run flags for edit/write tools, being able to show preview of diffs before running tool.
1 parent f8bafc9 commit 48ae18d

File tree

9 files changed

+80
-50
lines changed

9 files changed

+80
-50
lines changed

CHANGELOG.md

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

33
## Unreleased
44

5+
- Support clojureMCP dry-run flags for edit/write tools, being able to show preview of diffs before running tool.
6+
57
## 0.71.3
68

79
- Assoc `parallel_tool_calls` to openai-chat only if truth.

src/eca/features/chat.clj

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,6 @@
259259
(defn ^:private execute-action!
260260
"Execute a single action during state transition"
261261
[action db* chat-ctx tool-call-id event-data]
262-
(logger/debug logger-tag "About to run the tool-call action" {:tool-call-id tool-call-id :action action :event-data event-data})
263262
(case action
264263
;; Notification actions
265264
:send-progress
@@ -633,28 +632,30 @@
633632
(let [any-rejected-tool-call?* (atom nil)]
634633
(run! (fn do-tool-call [{:keys [id name arguments] :as tool-call}]
635634
(let [approved?* (promise) ; created here, stored in the state.
635+
db @db*
636636
hook-approved?* (atom true)
637-
details (f.tools/tool-call-details-before-invocation name arguments)
638-
summary (f.tools/tool-call-summary all-tools name arguments config)
639637
origin (tool-name->origin name all-tools)
640638
server (tool-name->server name all-tools)
641-
approval (f.tools/approval all-tools name arguments @db* config behavior)
642-
ask? (= :ask approval)]
639+
server-name (:name server)
640+
approval (f.tools/approval all-tools name arguments db config behavior)
641+
ask? (= :ask approval)
642+
details (f.tools/tool-call-details-before-invocation name arguments server db ask?)
643+
summary (f.tools/tool-call-summary all-tools name arguments config)]
643644
;; assert: In :preparing or :stopping or :cleanup
644645
;; Inform client the tool is about to run and store approval promise
645-
(when-not (#{:stopping :cleanup} (:status (get-tool-call-state @db* chat-id id)))
646+
(when-not (#{:stopping :cleanup} (:status (get-tool-call-state db chat-id id)))
646647
(transition-tool-call! db* chat-ctx id :tool-run
647648
{:approved?* approved?*
648649
:future-cleanup-complete?* (promise)
649650
:name name
650-
:server server
651+
:server server-name
651652
:origin origin
652653
:arguments arguments
653654
:manual-approval ask?
654655
:details details
655656
:summary summary}))
656657
;; assert: In: :check-approval or :stopping or :cleanup or :rejected
657-
(when-not (#{:stopping :cleanup :rejected} (:status (get-tool-call-state @db* chat-id id)))
658+
(when-not (#{:stopping :cleanup :rejected} (:status (get-tool-call-state db chat-id id)))
658659
(case approval
659660
:ask (transition-tool-call! db* chat-ctx id :approval-ask
660661
{:progress-text "Waiting for tool call approval"})
@@ -671,7 +672,7 @@
671672
(f.hooks/trigger-if-matches! :preToolCall
672673
{:chat-id chat-id
673674
:tool-name name
674-
:server server
675+
:server server-name
675676
:arguments arguments}
676677
{:on-before-action (partial notify-before-hook-action! chat-ctx)
677678
:on-after-action (fn [result]
@@ -695,8 +696,6 @@
695696
(delay
696697
(future
697698
;; assert: In :executing
698-
(logger/debug logger-tag "Just set up the call promise"
699-
{:tool-call-id id})
700699
(let [result (f.tools/call-tool! name arguments chat-id id behavior db* config messenger metrics
701700
(partial get-tool-call-state @db* chat-id id)
702701
(partial transition-tool-call! db* chat-ctx id))
@@ -708,7 +707,7 @@
708707
:details details
709708
:summary summary
710709
:origin origin
711-
:server server)})
710+
:server server-name)})
712711
(add-to-history! {:role "tool_call_output"
713712
:content (assoc tool-call
714713
:error (:error result)
@@ -717,15 +716,15 @@
717716
:details details
718717
:summary summary
719718
:origin origin
720-
:server server)})
719+
:server server-name)})
721720
;; assert: In :executing or :stopping
722721
(let [state (get-tool-call-state @db* chat-id id)
723722
status (:status state)]
724723
(case status
725724
:executing (transition-tool-call! db* chat-ctx id :execution-end
726725
{:origin origin
727726
:name name
728-
:server server
727+
:server server-name
729728
:arguments arguments
730729
:error (:error result)
731730
:outputs (:contents result)
@@ -736,7 +735,7 @@
736735
:stopping (transition-tool-call! db* chat-ctx id :stop-attempted
737736
{:origin origin
738737
:name name
739-
:server server
738+
:server server-name
740739
:arguments arguments
741740
:error (:error result)
742741
:outputs (:contents result)
@@ -749,7 +748,7 @@
749748
{:delayed-future delayed-future
750749
:origin origin
751750
:name name
752-
:server server
751+
:server server-name
753752
:arguments arguments
754753
:start-time (System/currentTimeMillis)
755754
:details details
@@ -767,7 +766,7 @@
767766
(transition-tool-call! db* chat-ctx id :send-reject
768767
{:origin origin
769768
:name name
770-
:server server
769+
:server server-name
771770
:arguments arguments
772771
:reason code
773772
:details details
@@ -792,8 +791,7 @@
792791
{:tool-call-id tool-call-id
793792
:promise p})
794793
(deref p) ; TODO: May need a timeout here too, in case the tool does not clean up.
795-
(logger/debug logger-tag "Future has finished cleanup."
796-
{:tool-call-id tool-call-id})))
794+
))
797795
(catch Throwable t
798796
(logger/debug logger-tag "Ignoring a Throwable while deref'ing a tool call future"
799797
{:tool-call-id tool-call-id

src/eca/features/tools.clj

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,7 @@
114114
:tool-call-id tool-call-id
115115
:call-state-fn call-state-fn
116116
:state-transition-fn state-transition-fn})
117-
(f.mcp/call-tool! name arguments {:db db
118-
:db* db*
119-
:config config
120-
:messenger messenger
121-
:behavior behavior
122-
:chat-id chat-id
123-
:tool-call-id tool-call-id
124-
:call-state-fn call-state-fn
125-
:state-transition-fn state-transition-fn}))
117+
(f.mcp/call-tool! name arguments {:db db}))
126118
(pretty-format-any-json-output))]
127119
(logger/debug logger-tag "Tool call result: " result)
128120
(metrics/count-up! "tool-called" {:name name :error (:error result)} metrics)
@@ -261,8 +253,14 @@
261253

262254
(defn tool-call-details-before-invocation
263255
"Return the tool call details before invoking the tool."
264-
[name arguments]
265-
(tools.util/tool-call-details-before-invocation name arguments))
256+
[name arguments server db ask-approval?]
257+
(try
258+
(tools.util/tool-call-details-before-invocation name arguments server {:db db
259+
:ask-approval? ask-approval?})
260+
(catch Exception e
261+
;; Avoid failling tool call because of error on getting details.
262+
(logger/error logger-tag (format "Error getting details for %s with args %s: %s" name arguments e))
263+
nil)))
266264

267265
(defn tool-call-details-after-invocation
268266
"Return the tool call details after invoking the tool."

src/eca/features/tools/filesystem.clj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@
364364
:handler #'grep
365365
:summary-fn #'grep-summary}})
366366

367-
(defmethod tools.util/tool-call-details-before-invocation :eca_edit_file [_name arguments]
367+
(defmethod tools.util/tool-call-details-before-invocation :eca_edit_file [_name arguments _server _ctx]
368368
(let [path (get arguments "path")
369369
original-content (get arguments "original_content")
370370
new-content (get arguments "new_content")
@@ -394,10 +394,10 @@
394394

395395
:else nil)))
396396

397-
(defmethod tools.util/tool-call-details-before-invocation :eca_preview_file_change [_name arguments]
398-
(tools.util/tool-call-details-before-invocation :eca_edit_file arguments))
397+
(defmethod tools.util/tool-call-details-before-invocation :eca_preview_file_change [_name arguments server ctx]
398+
(tools.util/tool-call-details-before-invocation :eca_edit_file arguments server ctx))
399399

400-
(defmethod tools.util/tool-call-details-before-invocation :eca_write_file [_name arguments]
400+
(defmethod tools.util/tool-call-details-before-invocation :eca_write_file [_name arguments _server _ctx]
401401
(let [path (get arguments "path")
402402
content (get arguments "content")]
403403
(when (and path content)

src/eca/features/tools/mcp.clj

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
(swap! db* assoc-in [:mcp-clients name] {:client client :status :starting})
176176
(try
177177
(.initialize client)
178+
(swap! db* assoc-in [:mcp-clients name :version] (.version (.getServerInfo client)))
178179
(swap! db* assoc-in [:mcp-clients name :tools] (list-server-tools obj-mapper client))
179180
(swap! db* assoc-in [:mcp-clients name :prompts] (list-server-prompts client))
180181
(swap! db* assoc-in [:mcp-clients name :resources] (list-server-resources client))
@@ -219,14 +220,12 @@
219220

220221
(defn all-tools [db]
221222
(into []
222-
(mapcat (fn [[name {:keys [tools]}]]
223-
(map #(assoc % :server name) tools)))
223+
(mapcat (fn [[name {:keys [tools version]}]]
224+
(map #(assoc % :server {:name name
225+
:version version}) tools)))
224226
(:mcp-clients db)))
225227

226-
(defn call-tool! [^String name ^Map arguments {:keys [db _db*
227-
_config _messenger _behavior
228-
_chat-id _tool-call-id
229-
_call-state-fn _state-transition-fn]}]
228+
(defn call-tool! [^String name ^Map arguments {:keys [db]}]
230229
(let [mcp-client (->> (vals (:mcp-clients db))
231230
(keep (fn [{:keys [client tools]}]
232231
(when (some #(= name (:name %)) tools)

src/eca/features/tools/mcp/clojure_mcp.clj

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,39 @@
22
(:require
33
[clojure.string :as string]
44
[eca.diff :as diff]
5+
[eca.features.tools.mcp :as f.mcp]
56
[eca.features.tools.util :as tools.util]))
67

8+
(defn ^:private clojure-edit-details-before-invocation [name args server {:keys [db ask-approval?]} new-file?]
9+
(when (not= "0.1.0" (:version server))
10+
(when ask-approval?
11+
(let [path (get args "file_path")
12+
{:keys [error contents]} (f.mcp/call-tool! name (assoc args "dry_run" "new-source") {:db db})]
13+
(when-not error
14+
(when-let [new-source (some->> contents (filter #(= :text (:type %))) first :text)]
15+
(let [{:keys [added removed diff]} (diff/diff (if new-file?
16+
""
17+
(slurp path))
18+
new-source
19+
path)]
20+
{:type :fileChange
21+
:path path
22+
:linesAdded added
23+
:linesRemoved removed
24+
:diff diff})))))))
25+
26+
(defmethod tools.util/tool-call-details-before-invocation :clojure_edit [name args server ctx]
27+
(clojure-edit-details-before-invocation name args server ctx false))
28+
29+
(defmethod tools.util/tool-call-details-before-invocation :clojure_edit_replace_sexp [name args server ctx]
30+
(clojure-edit-details-before-invocation name args server ctx false))
31+
32+
(defmethod tools.util/tool-call-details-before-invocation :file_edit [name args server ctx]
33+
(clojure-edit-details-before-invocation name args server ctx false))
34+
35+
(defmethod tools.util/tool-call-details-before-invocation :file_write [name args server ctx]
36+
(clojure-edit-details-before-invocation name args server ctx true))
37+
738
(defmethod tools.util/tool-call-details-after-invocation :clojure_edit [_name arguments details result]
839
(tools.util/tool-call-details-after-invocation :file_edit arguments details result))
940

src/eca/features/tools/util.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
(defmulti tool-call-details-before-invocation
99
"Return the tool call details before invoking the tool."
10-
(fn [name _arguments] (keyword name)))
10+
(fn [name _arguments _server _ctx] (keyword name)))
1111

12-
(defmethod tool-call-details-before-invocation :default [_name _arguments]
12+
(defmethod tool-call-details-before-invocation :default [_name _arguments _server _ctx]
1313
nil)
1414

1515
(defmulti tool-call-details-after-invocation

test/eca/features/tools/mcp_test.clj

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@
2121
(mcp/all-tools {:mcp-clients {"server1" {}}}))))
2222

2323
(testing "db with single server with tools"
24-
(let [tools [{:name "tool1" :description "desc1" :server "server1"}
25-
{:name "tool2" :description "desc2" :server "server1"}]]
24+
(let [tools [{:name "tool1" :description "desc1" :server {:name "server1" :version "1.0.0"}}
25+
{:name "tool2" :description "desc2" :server {:name "server1" :version "1.0.0"}}]]
2626
(is (= tools
27-
(mcp/all-tools {:mcp-clients {"server1" {:tools tools}}})))))
27+
(mcp/all-tools {:mcp-clients {"server1" {:version "1.0.0"
28+
:tools tools}}})))))
2829

2930
(testing "db with multiple servers with tools"
30-
(let [tools1 [{:name "tool1" :description "desc1" :server "server1"}]
31-
tools2 [{:name "tool2" :description "desc2" :server "server2"}
32-
{:name "tool3" :description "desc3" :server "server2"}]]
31+
(let [tools1 [{:name "tool1" :description "desc1" :server {:name "server1" :version nil}}]
32+
tools2 [{:name "tool2" :description "desc2" :server {:name "server2" :version nil}}
33+
{:name "tool3" :description "desc3" :server {:name "server2" :version nil}}]]
3334
(is (= (concat tools1 tools2)
3435
(mcp/all-tools {:mcp-clients {"server1" {:tools tools1}
3536
"server2" {:tools tools2}}}))))))

0 commit comments

Comments
 (0)