|
4 | 4 | [clojure.data.json :as json] |
5 | 5 | [mcp-clj.http-server.ring-adapter :as http] |
6 | 6 | [mcp-clj.json-rpc.protocol :as protocol] |
7 | | - [ring.util.response :as response]) |
8 | | - (:import |
9 | | - [com.sun.net.httpserver HttpServer] |
10 | | - [java.io OutputStreamWriter BufferedWriter])) |
11 | | - |
12 | | -(defn- write-sse-message |
13 | | - "Write a Server-Sent Event message" |
14 | | - [^BufferedWriter writer message] |
15 | | - (doto writer |
16 | | - (.write (str "data: " message "\n\n")) |
17 | | - (.flush))) |
| 7 | + [ring.util.response :as response])) |
18 | 8 |
|
19 | 9 | (defn- handle-json-rpc |
20 | | - "Process a single JSON-RPC request and return response" |
| 10 | + "Process a JSON-RPC request and return response" |
21 | 11 | [handlers request] |
22 | 12 | (if-let [validation-error (protocol/validate-request request)] |
23 | | - validation-error |
| 13 | + (assoc validation-error :id (:id request)) |
24 | 14 | (let [{:keys [method params id]} request |
25 | 15 | handler (get handlers method)] |
26 | 16 | (if handler |
27 | 17 | (try |
28 | | - (let [result (handler params)] |
29 | | - (protocol/result-response id result)) |
| 18 | + {:jsonrpc "2.0" |
| 19 | + :id id |
| 20 | + :result (handler params)} |
30 | 21 | (catch Exception e |
31 | | - (protocol/error-response |
32 | | - (get protocol/error-codes :internal-error) |
33 | | - (.getMessage e) |
34 | | - {:id id}))) |
35 | | - (protocol/error-response |
36 | | - (get protocol/error-codes :method-not-found) |
37 | | - (str "Method not found: " method) |
38 | | - {:id id}))))) |
| 22 | + {:jsonrpc "2.0" |
| 23 | + :id id |
| 24 | + :error {:code (get protocol/error-codes :internal-error) |
| 25 | + :message (.getMessage e)}})) |
| 26 | + {:jsonrpc "2.0" |
| 27 | + :id id |
| 28 | + :error {:code (get protocol/error-codes :method-not-found) |
| 29 | + :message (str "Method not found: " method)}})))) |
| 30 | + |
| 31 | +(defn- handle-post |
| 32 | + "Handle JSON-RPC POST request" |
| 33 | + [handlers {:keys [body] :as request}] |
| 34 | + (try |
| 35 | + (let [request-data (json/read-str (slurp body) :key-fn keyword) |
| 36 | + response (handle-json-rpc handlers request-data) |
| 37 | + response-str (json/write-str response)] |
| 38 | + (-> (response/response response-str) |
| 39 | + (response/status 200) |
| 40 | + (response/content-type "application/json"))) |
| 41 | + (catch Exception e |
| 42 | + (-> (response/response |
| 43 | + (json/write-str |
| 44 | + {:jsonrpc "2.0" |
| 45 | + :error {:code (get protocol/error-codes :internal-error) |
| 46 | + :message (str "Unexpected error: " (ex-message e))}})) |
| 47 | + (response/status 500) |
| 48 | + (response/content-type "application/json"))))) |
39 | 49 |
|
40 | | -(defn- handle-sse-request |
41 | | - "Handle SSE stream setup and message processing" |
42 | | - [handlers message-stream] |
43 | | - (fn [^java.io.OutputStream output-stream] |
44 | | - (with-open [writer (-> output-stream |
45 | | - (OutputStreamWriter. "UTF-8") |
46 | | - BufferedWriter.)] |
| 50 | +(defn- handle-sse |
| 51 | + "Handle SSE stream setup" |
| 52 | + [response-stream] |
| 53 | + (fn [output] |
| 54 | + (let [writer (java.io.OutputStreamWriter. output "UTF-8")] |
47 | 55 | (try |
48 | | - (let [control-ch message-stream] |
49 | | - (loop [] |
50 | | - (when-let [message @control-ch] |
51 | | - (let [[request parse-error] (protocol/parse-json message) |
52 | | - response (if parse-error |
53 | | - parse-error |
54 | | - (handle-json-rpc handlers request)) |
55 | | - [json-response error] (protocol/write-json response)] |
56 | | - (when json-response |
57 | | - (write-sse-message writer json-response)) |
58 | | - (when error |
59 | | - (write-sse-message writer |
60 | | - (json/write-str |
61 | | - (protocol/error-response |
62 | | - (get protocol/error-codes :internal-error) |
63 | | - "Response encoding error"))))) |
64 | | - (reset! control-ch nil) |
65 | | - (recur)))) |
| 56 | + (loop [] |
| 57 | + (when-let [response @response-stream] |
| 58 | + (.write writer (str "data: " (json/write-str response) "\n\n")) |
| 59 | + (.flush writer) |
| 60 | + (reset! response-stream nil) |
| 61 | + (recur))) |
66 | 62 | (catch Exception e |
67 | | - (write-sse-message writer |
68 | | - (json/write-str |
69 | | - (protocol/error-response |
70 | | - (get protocol/error-codes :internal-error) |
71 | | - "Stream error")))))))) |
72 | | - |
73 | | -(defn get-server-port |
74 | | - "Get the actual port a server is listening on" |
75 | | - [^HttpServer server] |
76 | | - (.getPort (.getAddress server))) |
| 63 | + (.printStackTrace e)))))) |
77 | 64 |
|
78 | 65 | (defn create-server |
79 | | - "Create a new JSON-RPC SSE server. |
| 66 | + "Create JSON-RPC server with SSE support. |
80 | 67 |
|
81 | 68 | Configuration options: |
82 | | - - :port Port number (default: 0 for auto-assignment) |
83 | | - - :handlers Map of method names to handler functions |
84 | | - - :message-stream Shared atom for message passing |
85 | | -
|
86 | | - Returns map with: |
87 | | - - :server The server instance |
88 | | - - :port The actual port the server is running on |
89 | | - - :stop Function to stop the server" |
90 | | - [{:keys [port handlers message-stream] |
91 | | - :or {port 0} |
92 | | - :as config}] |
| 69 | + - :port Port number (default: 0 for auto-assignment) |
| 70 | + - :handlers Map of method names to handler functions" |
| 71 | + [{:keys [port handlers] |
| 72 | + :or {port 0}}] |
93 | 73 | (when-not (map? handlers) |
94 | | - (throw (ex-info "Handlers must be a map" {:config config}))) |
95 | | - (when-not (instance? clojure.lang.Atom message-stream) |
96 | | - (throw (ex-info "Message stream must be an atom" {:config config}))) |
| 74 | + (throw (ex-info "Handlers must be a map" {:handlers handlers}))) |
| 75 | + |
| 76 | + (let [response-stream (atom nil) |
| 77 | + handler (fn [{:keys [request-method uri] :as request}] |
| 78 | + (case [request-method uri] |
| 79 | + [:post "/message"] |
| 80 | + (handle-post handlers request) |
| 81 | + |
| 82 | + [:get "/sse"] |
| 83 | + (-> (response/response (handle-sse response-stream)) |
| 84 | + (response/status 200) |
| 85 | + (response/content-type "text/event-stream") |
| 86 | + (response/header "Cache-Control" "no-cache") |
| 87 | + (response/header "Connection" "keep-alive")) |
| 88 | + |
| 89 | + (-> (response/response "Not Found") |
| 90 | + (response/status 404) |
| 91 | + (response/content-type "text/plain")))) |
97 | 92 |
|
98 | | - (let [{:keys [server stop]} (http/run-server |
99 | | - (fn [request] |
100 | | - (-> (response/response |
101 | | - (handle-sse-request handlers message-stream)) |
102 | | - (response/content-type "text/event-stream") |
103 | | - (response/header "Cache-Control" "no-cache") |
104 | | - (response/header "Connection" "keep-alive"))) |
105 | | - {:port port})] |
106 | | - {:server server |
107 | | - :port (get-server-port server) |
108 | | - :stop stop})) |
| 93 | + {:keys [server stop]} (http/run-server handler {:port port})] |
| 94 | + {:server server |
| 95 | + :response-stream response-stream |
| 96 | + :port (.getPort (.getAddress server)) |
| 97 | + :stop stop})) |
0 commit comments