|
11 | 11 | LinkedBlockingQueue |
12 | 12 | TimeUnit])) |
13 | 13 |
|
14 | | -(def valid-client-info |
15 | | - {:clientInfo {:name "test-client" :version "1.0"} |
16 | | - :protocolVersion @#'mcp/server-protocol-version |
17 | | - :capabilities {:tools {:listChanged false}}}) |
| 14 | +(def test-tool |
| 15 | + "Test tool for server testing" |
| 16 | + {:name "test-tool" |
| 17 | + :description "A test tool for server testing" |
| 18 | + :inputSchema {:type "object" |
| 19 | + :properties {"value" {:type "string"}} |
| 20 | + :required ["value"]} |
| 21 | + :implementation (fn [{:keys [value]}] |
| 22 | + {:content [{:type "text" |
| 23 | + :text (str "test-response:" value)}]})}) |
| 24 | + |
| 25 | +(def error-test-tool |
| 26 | + "Test tool that always returns an error" |
| 27 | + {:name "error-test-tool" |
| 28 | + :description "A test tool that always returns an error" |
| 29 | + :inputSchema {:type "object" |
| 30 | + :properties {"value" {:type "string"}} |
| 31 | + :required ["value"]} |
| 32 | + :implementation (fn [_] |
| 33 | + {:content [{:type "text" |
| 34 | + :text "test-error"}] |
| 35 | + :isError true})}) |
| 36 | + |
| 37 | +(def test-prompt |
| 38 | + {:name "test-prompt" |
| 39 | + :description "A test prompt for server testing" |
| 40 | + :messages [{:role "system" |
| 41 | + :content {:type "text" |
| 42 | + :text "Hello"}} |
| 43 | + {:role "user" |
| 44 | + :content {:type "text" |
| 45 | + :text "Please say {{reply}}"}}] |
| 46 | + :arguments [{:name "reply" |
| 47 | + :description "something" |
| 48 | + :required true}]}) |
| 49 | + |
18 | 50 |
|
19 | 51 | #_{:clj-kondo/ignore [:uninitialized-var]} |
20 | 52 | (def ^:private ^:dynamic *server*) |
21 | 53 |
|
22 | 54 | (defn with-server |
23 | 55 | "Test fixture for server lifecycle" |
24 | 56 | [f] |
25 | | - (let [server (mcp/create-server {:port 0 :threads 2 :queue-size 10})] |
| 57 | + (let [server (mcp/create-server |
| 58 | + {:port 0 |
| 59 | + :threads 2 |
| 60 | + :queue-size 10 |
| 61 | + :tools {"test-tool" test-tool |
| 62 | + "error-test-tool" error-test-tool} |
| 63 | + :prompts {"test-prompt" test-prompt}})] |
26 | 64 | (try |
27 | 65 | (binding [*server* server] |
28 | 66 | (f)) |
|
211 | 249 | (future-cancel f)))))) |
212 | 250 |
|
213 | 251 | (deftest tools-test |
214 | | - (testing "server lifecycle with SSE" |
| 252 | + (testing "A server with tools" |
215 | 253 | (let [port (port) |
216 | 254 | url (format "http://localhost:%d" port) |
217 | 255 | queue (LinkedBlockingQueue.) |
|
234 | 272 | [state' result] (run-plan state)] |
235 | 273 | (is (= :passed result)) |
236 | 274 | (testing "tool interactions" |
237 | | - (let [state (assoc |
238 | | - state' |
239 | | - :plan |
240 | | - [{:action :send |
241 | | - :msg (json-request |
242 | | - "tools/list" |
243 | | - {} |
244 | | - 0)} |
245 | | - {:action :receive |
246 | | - :data |
247 | | - {:event "message" |
248 | | - :data |
249 | | - (json-result |
250 | | - {:tools |
251 | | - [{:name "clj-eval", |
252 | | - :description |
253 | | - "Evaluates a Clojure expression and returns the result", |
254 | | - :inputSchema |
255 | | - {:type "object", |
256 | | - :properties {:code {:type "string"}}, |
257 | | - :required ["code"]}}]} |
258 | | - {} |
259 | | - 0)}}]) |
| 275 | + (let [state |
| 276 | + (assoc |
| 277 | + state' |
| 278 | + :plan |
| 279 | + [{:action :send |
| 280 | + :msg (json-request |
| 281 | + "tools/list" |
| 282 | + {} |
| 283 | + 0)} |
| 284 | + {:action :receive |
| 285 | + :data |
| 286 | + {:event "message" |
| 287 | + :data |
| 288 | + (json-result |
| 289 | + {:tools |
| 290 | + [{:name "test-tool", |
| 291 | + :description "A test tool for server testing", |
| 292 | + :inputSchema |
| 293 | + {:type "object", |
| 294 | + :properties {:value {:type "string"}}, |
| 295 | + :required ["value"]}} |
| 296 | + {:name "error-test-tool", |
| 297 | + :description "A test tool that always returns an error", |
| 298 | + :inputSchema |
| 299 | + {:type "object", |
| 300 | + :properties {:value {:type "string"}}, |
| 301 | + :required ["value"]}}]} |
| 302 | + {} |
| 303 | + 0)}}]) |
260 | 304 | [state' result] (run-plan state) |
261 | 305 | _ (testing "tools/list" |
262 | 306 | (is (= :passed result) (pr-str state)) |
|
267 | 311 | [{:action :send |
268 | 312 | :msg (json-request |
269 | 313 | "tools/call" |
270 | | - {:name "clj-eval" |
271 | | - :arguments {:code "(+ 1 2)"}} |
272 | | - 0)} |
273 | | - {:action :receive |
274 | | - :data {:event "message" |
275 | | - :data |
276 | | - (json-result |
277 | | - {:content |
278 | | - [{:type "text" |
279 | | - :text "3"}]} |
280 | | - nil |
281 | | - 0)}}]) |
282 | | - [state' result] (run-plan state) |
283 | | - _ (testing "successful tools/call" |
284 | | - (is (= :passed result)) |
285 | | - (is (not (:failed state')))) |
286 | | - state (assoc |
287 | | - state' |
288 | | - :plan |
289 | | - [{:action :send |
290 | | - :msg (json-request |
291 | | - "tools/call" |
292 | | - {:name "clj-eval" |
| 314 | + {:name "test-tool" |
293 | 315 | :arguments |
294 | | - {:code "(/ 1 0)"}} |
| 316 | + {:value "me"}} |
295 | 317 | 0)} |
296 | 318 | {:action :receive |
297 | 319 | :data |
|
300 | 322 | (json-result |
301 | 323 | {:content |
302 | 324 | [{:type "text" |
303 | | - :text "Error: Divide by zero"}] |
304 | | - :isError true} |
| 325 | + :text "test-response:me"}]} |
305 | 326 | nil |
306 | 327 | 0)}}]) |
307 | | - [state' result] (run-plan state) |
308 | | - _ (testing "tools/call with eval error" |
| 328 | + [state' result] (testing "makes a successful tools/call" |
| 329 | + (run-plan state)) |
| 330 | + _ (testing "makes a successful tools/call" |
309 | 331 | (is (= :passed result)) |
310 | 332 | (is (not (:failed state')))) |
311 | 333 | state (assoc |
|
314 | 336 | [{:action :send |
315 | 337 | :msg (json-request |
316 | 338 | "tools/call" |
317 | | - {:name "clj-eval" |
| 339 | + {:name "error-test-tool" |
318 | 340 | :arguments |
319 | | - {:code "(/ 1 0"}})} |
| 341 | + {:value "me"}} |
| 342 | + 0)} |
320 | 343 | {:action :receive |
321 | 344 | :data |
322 | 345 | {:event "message" |
323 | 346 | :data |
324 | 347 | (json-result |
325 | 348 | {:content |
326 | 349 | [{:type "text" |
327 | | - :text "Error: EOF while reading"}] |
| 350 | + :text "test-error"}] |
328 | 351 | :isError true} |
329 | 352 | nil |
330 | 353 | 0)}}]) |
331 | | - [state' result] (run-plan state) |
332 | | - _ (testing "tools/call with invalid clojure" |
| 354 | + [state' result] (testing "tools/call with an error" |
| 355 | + (run-plan state)) |
| 356 | + _ (testing "tools/call with an error" |
333 | 357 | (is (= :passed result)) |
334 | 358 | (is (not (:failed state')))) |
335 | 359 | state (assoc |
|
567 | 591 | [state' result] (run-plan state)] |
568 | 592 | (is (= :passed result)) |
569 | 593 | (testing "prompt interactions" |
570 | | - (let [state (assoc |
571 | | - state' |
572 | | - :plan |
573 | | - [{:action :send |
574 | | - :msg (json-request |
575 | | - "prompts/list" |
576 | | - {} |
577 | | - 0)} |
578 | | - {:action :receive |
579 | | - :data |
580 | | - {:event "message" |
581 | | - :data (json-result |
582 | | - {:prompts []} |
583 | | - nil |
584 | | - 0)}}]) |
| 594 | + (let [state |
| 595 | + (assoc |
| 596 | + state' |
| 597 | + :plan |
| 598 | + [{:action :send |
| 599 | + :msg (json-request |
| 600 | + "prompts/list" |
| 601 | + {} |
| 602 | + 0)} |
| 603 | + {:action :receive |
| 604 | + :data |
| 605 | + {:event "message" |
| 606 | + :data |
| 607 | + (json-result |
| 608 | + {:prompts |
| 609 | + [{:name "test-prompt", |
| 610 | + :description "A test prompt for server testing", |
| 611 | + :arguments |
| 612 | + [{:name "reply" |
| 613 | + :description "something" |
| 614 | + :required true}]}]} |
| 615 | + nil |
| 616 | + 0)}}]) |
585 | 617 | [state' result] (run-plan state) |
586 | 618 | _ (testing "prompts/list" |
587 | 619 | (is (= :passed result)))])))) |
|
0 commit comments