Skip to content

Commit 2687964

Browse files
committed
Add integration tests for toos list change
1 parent 04fff6c commit 2687964

3 files changed

Lines changed: 193 additions & 7 deletions

File tree

integration-test/integration/chat/mcp_remote_test.clj

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
(testing "MCP remote server running with tools"
3939
(is (match? {:type "mcp"
4040
:name "testMcp"
41-
:tools (m/embeds [{:name "echo"} {:name "add"}])}
41+
:tools (m/embeds [{:name "echo"} {:name "add"} {:name "add-tool"}])}
4242
(eca/client-awaits-server-notification :tool/serverUpdated))))
4343

4444
(testing "MCP mock received initialize request"
@@ -181,3 +181,101 @@
181181
"System prompt should contain MCP server instruction block")
182182
(is (string/includes? system-text "This is a test MCP server for integration testing.")
183183
"System prompt should contain the MCP server's instructions text")))))
184+
185+
(deftest mcp-remote-tool-list-changed-via-tool-call
186+
(init-with-mcp-remote!)
187+
188+
;; Wait for MCP server to be fully ready with initial tools
189+
(eca/client-awaits-server-notification :tool/serverUpdated) ;; native
190+
(eca/client-awaits-server-notification :tool/serverUpdated) ;; mcp starting
191+
(is (match? {:type "mcp"
192+
:name "testMcp"
193+
:tools (m/embeds [{:name "echo"} {:name "add"} {:name "add-tool"}])}
194+
(eca/client-awaits-server-notification :tool/serverUpdated))) ;; mcp running
195+
196+
(testing "Calling add-tool triggers list_changed and ECA refreshes tools"
197+
(mcp-mock/reset-requests!)
198+
(llm.mocks/set-case! :mcp-add-tool-0)
199+
200+
(let [resp (eca/request! (fixture/chat-prompt-request
201+
{:model "anthropic/claude-sonnet-4-6"
202+
:message "Add the multiply tool"}))
203+
chat-id (:chatId resp)]
204+
205+
(is (match? {:chatId string?
206+
:model "anthropic/claude-sonnet-4-6"
207+
:status "prompting"}
208+
resp))
209+
210+
;; User message
211+
(match-content chat-id "user" {:type "text" :text "Add the multiply tool\n"})
212+
213+
;; Title + progress
214+
(match-content chat-id "system" {:type "metadata" :title "Some Cool Title"})
215+
(match-content chat-id "system" {:type "progress" :state "running" :text "Waiting model"})
216+
(match-content chat-id "system" {:type "progress" :state "running" :text "Generating"})
217+
218+
;; Assistant text before tool use
219+
(match-content chat-id "assistant" {:type "text" :text "I will add the multiply tool"})
220+
221+
;; Tool call prepare/run
222+
(match-content chat-id "assistant" {:type "toolCallPrepare"
223+
:origin "mcp"
224+
:name "add-tool"
225+
:id "mcp-add-tool-1"})
226+
(match-content chat-id "assistant" {:type "toolCallPrepare"
227+
:origin "mcp"
228+
:name "add-tool"
229+
:id "mcp-add-tool-1"})
230+
231+
;; Usage from first LLM turn
232+
(match-content chat-id "system" {:type "usage"})
233+
234+
(match-content chat-id "assistant" {:type "toolCallRun"
235+
:origin "mcp"
236+
:name "add-tool"
237+
:id "mcp-add-tool-1"
238+
:arguments {:name "multiply"}
239+
:manualApproval false})
240+
(match-content chat-id "assistant" {:type "toolCallRunning"
241+
:origin "mcp"
242+
:name "add-tool"
243+
:id "mcp-add-tool-1"
244+
:arguments {:name "multiply"}})
245+
(match-content chat-id "system" {:type "progress" :state "running" :text "Calling tool"})
246+
247+
;; Tool result
248+
(match-content chat-id "assistant" {:type "toolCalled"
249+
:origin "mcp"
250+
:name "add-tool"
251+
:id "mcp-add-tool-1"
252+
:arguments {:name "multiply"}
253+
:error nil
254+
:outputs [{:type "text"
255+
:text "Tool 'multiply' registered successfully"}]})
256+
257+
;; Second LLM turn: final response
258+
(match-content chat-id "assistant" {:type "text" :text "Tool added successfully"})
259+
(match-content chat-id "system" {:type "usage"})
260+
(match-content chat-id "system" {:type "progress" :state "finished"})
261+
262+
(testing "MCP mock received add-tool call"
263+
(let [call-reqs (mcp-mock/get-requests-by-method "tools/call")]
264+
(is (= 1 (count call-reqs)))
265+
(is (match? {:method "tools/call"
266+
:params {:name "add-tool"
267+
:arguments {:name "multiply"}}}
268+
(first call-reqs)))))
269+
270+
(testing "ECA re-fetched tools after list_changed notification"
271+
(let [tools-reqs (mcp-mock/get-requests-by-method "tools/list")]
272+
(is (<= 1 (count tools-reqs))
273+
"Expected at least one tools/list request after the notification")))
274+
275+
(testing "ECA propagated updated tool list to client"
276+
(is (match? {:type "mcp"
277+
:name "testMcp"
278+
:tools (m/embeds [{:name "echo"} {:name "add"}
279+
{:name "add-tool"} {:name "multiply"}])}
280+
(eca/client-awaits-server-notification :tool/serverUpdated))
281+
"ECA should include the new 'multiply' tool after list_changed notification")))))

integration-test/llm_mock/anthropic.clj

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,47 @@
257257
(sse-send! ch "message_stop" {:type "message_stop"})
258258
(hk/close ch)))))
259259

260+
(defn ^:private mcp-add-tool-0 [ch body]
261+
(let [second-stage? (some (fn [{:keys [content]}]
262+
(some #(= "tool_result" (:type %)) content))
263+
(:messages body))]
264+
(if-not second-stage?
265+
(do
266+
(sse-send! ch "content_block_delta"
267+
{:type "content_block_delta"
268+
:index 0
269+
:delta {:type "text_delta" :text "I will add the multiply tool"}})
270+
(sse-send! ch "content_block_start"
271+
{:type "content_block_start"
272+
:index 1
273+
:content_block {:type "tool_use"
274+
:id "mcp-add-tool-1"
275+
:name "testMcp__add-tool"}})
276+
(sse-send! ch "content_block_delta"
277+
{:type "content_block_delta"
278+
:index 1
279+
:delta {:type "input_json_delta"
280+
:partial_json "{\"name\":\"multiply\"}"}})
281+
(sse-send! ch "message_delta"
282+
{:type "message_delta"
283+
:delta {:stop_reason "tool_use"}
284+
:usage {:input_tokens 10
285+
:output_tokens 20}})
286+
(sse-send! ch "message_stop" {:type "message_stop"})
287+
(hk/close ch))
288+
(do
289+
(sse-send! ch "content_block_delta"
290+
{:type "content_block_delta"
291+
:index 0
292+
:delta {:type "text_delta" :text "Tool added successfully"}})
293+
(sse-send! ch "message_delta"
294+
{:type "message_delta"
295+
:delta {:stop_reason "end_turn"}
296+
:usage {:input_tokens 15
297+
:output_tokens 10}})
298+
(sse-send! ch "message_stop" {:type "message_stop"})
299+
(hk/close ch)))))
300+
260301
(defn ^:private compact-0 [ch]
261302
;; LLM calls eca__compact_chat with a summary — no text or reasoning
262303
(sse-send! ch "content_block_start"
@@ -308,4 +349,5 @@
308349
:reasoning-1 (reasoning-1 ch)
309350
:tool-calling-0 (tool-calling-0 ch body)
310351
:mcp-tool-call-0 (mcp-tool-call-0 ch body)
352+
:mcp-add-tool-0 (mcp-add-tool-0 ch body)
311353
:compact-0 (compact-0 ch)))))})))

integration-test/mcp_mock/server.clj

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
(defonce ^:private requests* (atom []))
2323

24+
(defonce ^:private sse-channels* (atom #{}))
25+
2426
(def ^:private default-tools
2527
[{:name "echo"
2628
:description "Echoes back the message"
@@ -32,7 +34,12 @@
3234
:inputSchema {:type "object"
3335
:properties {:a {:type "number" :description "First number"}
3436
:b {:type "number" :description "Second number"}}
35-
:required ["a" "b"]}}])
37+
:required ["a" "b"]}}
38+
{:name "add-tool"
39+
:description "Dynamically registers a new tool on this server"
40+
:inputSchema {:type "object"
41+
:properties {:name {:type "string" :description "Name of the tool to add"}}
42+
:required ["name"]}}])
3643

3744
(def ^:private default-instructions
3845
"This is a test MCP server for integration testing.")
@@ -65,6 +72,20 @@
6572
(reset! config* {:tools default-tools
6673
:instructions default-instructions}))
6774

75+
(defn ^:private send-sse-event!
76+
"Send a JSON-RPC message as an SSE event through the given channel."
77+
[ch jsonrpc-message]
78+
(let [data (json/generate-string jsonrpc-message)]
79+
(hk/send! ch (str "event: message\ndata: " data "\n\n") false)))
80+
81+
(defn notify-tools-changed!
82+
"Push a notifications/tools/list_changed event to all connected SSE clients.
83+
Call set-config! first to update the tool list before triggering this."
84+
[]
85+
(let [msg {:jsonrpc "2.0" :method "notifications/tools/list_changed"}]
86+
(doseq [ch @sse-channels*]
87+
(send-sse-event! ch msg))))
88+
6889
(defn ^:private handle-initialize [body]
6990
(let [sid (str (UUID/randomUUID))]
7091
(reset! session-id* sid)
@@ -75,8 +96,8 @@
7596
{:jsonrpc "2.0"
7697
:id (:id body)
7798
:result {:protocolVersion "2025-03-26"
78-
:capabilities {:tools {:listChanged false}
79-
:prompts {:listChanged false}}
99+
:capabilities {:tools {:listChanged true}
100+
:prompts {:listChanged true}}
80101
:serverInfo {:name "test-mcp-mock" :version "1.0.0"}
81102
:instructions (:instructions @config*)}})}))
82103

@@ -95,12 +116,31 @@
95116
{:content [{:type "text" :text (str (+ (double (:a arguments))
96117
(double (:b arguments))))}]})
97118

119+
(def ^:private dynamic-tool-registry
120+
{"multiply" {:name "multiply"
121+
:description "Multiplies two numbers"
122+
:inputSchema {:type "object"
123+
:properties {:a {:type "number" :description "First number"}
124+
:b {:type "number" :description "Second number"}}
125+
:required ["a" "b"]}}})
126+
127+
(defn ^:private call-add-tool [arguments]
128+
(let [tool-name (:name arguments)]
129+
(if-let [tool-def (get dynamic-tool-registry tool-name)]
130+
(do
131+
(swap! config* update :tools conj tool-def)
132+
(notify-tools-changed!)
133+
{:content [{:type "text" :text (str "Tool '" tool-name "' registered successfully")}]})
134+
{:content [{:type "text" :text (str "Unknown tool template: " tool-name)}]
135+
:isError true})))
136+
98137
(defn ^:private handle-tool-call [body]
99138
(let [tool-name (get-in body [:params :name])
100139
arguments (get-in body [:params :arguments])
101140
result (case tool-name
102141
"echo" (call-echo arguments)
103142
"add" (call-add arguments)
143+
"add-tool" (call-add-tool arguments)
104144
{:content [{:type "text" :text (str "Unknown tool: " tool-name)}]
105145
:isError true})]
106146
{:status 200
@@ -144,8 +184,9 @@
144184
:error {:code -32601 :message (str "Method not found: " method)}})})))
145185

146186
(defn ^:private handle-get
147-
"Handle GET requests for SSE stream. Opens a channel and keeps it alive
148-
for the plumcp client's background SSE connection."
187+
"Handle GET requests for SSE stream. Opens a channel, tracks it for
188+
server-initiated notifications, and keeps it alive for the plumcp
189+
client's background SSE connection."
149190
[req]
150191
(hk/as-channel
151192
req
@@ -154,7 +195,10 @@
154195
:headers {"Content-Type" "text/event-stream"
155196
"Cache-Control" "no-cache"
156197
"Connection" "keep-alive"}}
157-
false))}))
198+
false)
199+
(swap! sse-channels* conj ch))
200+
:on-close (fn [ch _status]
201+
(swap! sse-channels* disj ch))}))
158202

159203
(defn ^:private app [req]
160204
(let [{:keys [request-method]} req]
@@ -169,6 +213,7 @@
169213
(println "Starting MCP mock server on port" port "...")
170214
(reset! session-id* nil)
171215
(reset! requests* [])
216+
(reset! sse-channels* #{})
172217
(reset! server* (hk/run-server app {:port port})))
173218
:started)
174219

@@ -178,5 +223,6 @@
178223
(reset! server* nil)
179224
(reset! session-id* nil)
180225
(reset! requests* [])
226+
(reset! sse-channels* #{})
181227
(reset-config!)
182228
:stopped))

0 commit comments

Comments
 (0)