|
21 | 21 |
|
22 | 22 | (defonce ^:private requests* (atom [])) |
23 | 23 |
|
| 24 | +(defonce ^:private sse-channels* (atom #{})) |
| 25 | + |
24 | 26 | (def ^:private default-tools |
25 | 27 | [{:name "echo" |
26 | 28 | :description "Echoes back the message" |
|
32 | 34 | :inputSchema {:type "object" |
33 | 35 | :properties {:a {:type "number" :description "First number"} |
34 | 36 | :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"]}}]) |
36 | 43 |
|
37 | 44 | (def ^:private default-instructions |
38 | 45 | "This is a test MCP server for integration testing.") |
|
65 | 72 | (reset! config* {:tools default-tools |
66 | 73 | :instructions default-instructions})) |
67 | 74 |
|
| 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 | + |
68 | 89 | (defn ^:private handle-initialize [body] |
69 | 90 | (let [sid (str (UUID/randomUUID))] |
70 | 91 | (reset! session-id* sid) |
|
75 | 96 | {:jsonrpc "2.0" |
76 | 97 | :id (:id body) |
77 | 98 | :result {:protocolVersion "2025-03-26" |
78 | | - :capabilities {:tools {:listChanged false} |
79 | | - :prompts {:listChanged false}} |
| 99 | + :capabilities {:tools {:listChanged true} |
| 100 | + :prompts {:listChanged true}} |
80 | 101 | :serverInfo {:name "test-mcp-mock" :version "1.0.0"} |
81 | 102 | :instructions (:instructions @config*)}})})) |
82 | 103 |
|
|
95 | 116 | {:content [{:type "text" :text (str (+ (double (:a arguments)) |
96 | 117 | (double (:b arguments))))}]}) |
97 | 118 |
|
| 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 | + |
98 | 137 | (defn ^:private handle-tool-call [body] |
99 | 138 | (let [tool-name (get-in body [:params :name]) |
100 | 139 | arguments (get-in body [:params :arguments]) |
101 | 140 | result (case tool-name |
102 | 141 | "echo" (call-echo arguments) |
103 | 142 | "add" (call-add arguments) |
| 143 | + "add-tool" (call-add-tool arguments) |
104 | 144 | {:content [{:type "text" :text (str "Unknown tool: " tool-name)}] |
105 | 145 | :isError true})] |
106 | 146 | {:status 200 |
|
144 | 184 | :error {:code -32601 :message (str "Method not found: " method)}})}))) |
145 | 185 |
|
146 | 186 | (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." |
149 | 190 | [req] |
150 | 191 | (hk/as-channel |
151 | 192 | req |
|
154 | 195 | :headers {"Content-Type" "text/event-stream" |
155 | 196 | "Cache-Control" "no-cache" |
156 | 197 | "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))})) |
158 | 202 |
|
159 | 203 | (defn ^:private app [req] |
160 | 204 | (let [{:keys [request-method]} req] |
|
169 | 213 | (println "Starting MCP mock server on port" port "...") |
170 | 214 | (reset! session-id* nil) |
171 | 215 | (reset! requests* []) |
| 216 | + (reset! sse-channels* #{}) |
172 | 217 | (reset! server* (hk/run-server app {:port port}))) |
173 | 218 | :started) |
174 | 219 |
|
|
178 | 223 | (reset! server* nil) |
179 | 224 | (reset! session-id* nil) |
180 | 225 | (reset! requests* []) |
| 226 | + (reset! sse-channels* #{}) |
181 | 227 | (reset-config!) |
182 | 228 | :stopped)) |
0 commit comments