Skip to content

Commit c696c8b

Browse files
committed
feat: add server implementation
1 parent 9500d6c commit c696c8b

File tree

4 files changed

+172
-56
lines changed

4 files changed

+172
-56
lines changed

components/json-rpc/src/mcp_clj/json_rpc/protocol.clj

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
(ns mcp-clj.json-rpc.protocol
22
"JSON-RPC 2.0 protocol constants and utilities"
3-
(:require [clojure.data.json :as json]))
3+
(:require
4+
[clojure.data.json :as json]))
45

56
;;; Protocol version
67
(def ^:const version "2.0")
@@ -45,11 +46,11 @@
4546
(not= jsonrpc version)
4647
(error-response (:invalid-request error-codes)
4748
"Invalid JSON-RPC version")
48-
49+
4950
(not (string? method))
5051
(error-response (:invalid-request error-codes)
5152
"Method must be a string")
52-
53+
5354
:else nil))
5455

5556
;;; JSON conversion
@@ -78,4 +79,4 @@
7879
[(json/write-str data write-json-options) nil]
7980
(catch Exception e
8081
[nil (error-response (:internal-error error-codes)
81-
"JSON conversion error")])))
82+
"JSON conversion error")])))
Lines changed: 99 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,102 @@
11
(ns mcp-clj.json-rpc.server
22
"JSON-RPC 2.0 server implementation with EDN/JSON conversion"
3-
(:require [aleph.http :as http]
4-
[clojure.data.json :as json]
5-
[mcp-clj.json-rpc.protocol :as protocol]))
3+
(:require
4+
[mcp-clj.json-rpc.protocol :as protocol])
5+
(:import
6+
[com.sun.net.httpserver HttpServer HttpHandler HttpExchange]
7+
[java.net InetSocketAddress]
8+
[java.io InputStreamReader BufferedReader]
9+
[java.util.concurrent Executors]))
610

7-
;;; Server creation and lifecycle
11+
(defn- read-request-body
12+
"Read the request body from an HttpExchange"
13+
[^HttpExchange exchange]
14+
(with-open [reader (-> exchange
15+
.getRequestBody
16+
InputStreamReader.
17+
BufferedReader.)]
18+
(let [length (-> exchange .getRequestHeaders (get "Content-length") first Integer/parseInt)
19+
chars (char-array length)]
20+
(.read reader chars 0 length)
21+
(String. chars))))
22+
23+
(defn- send-response
24+
"Send a response through the HttpExchange"
25+
[^HttpExchange exchange ^String response]
26+
(let [bytes (.getBytes response)
27+
headers (.getResponseHeaders exchange)]
28+
(.add headers "Content-Type" "application/json")
29+
(.sendResponseHeaders exchange 200 (count bytes))
30+
(with-open [os (.getResponseBody exchange)]
31+
(.write os bytes)
32+
(.flush os))))
33+
34+
(defn- handle-json-rpc
35+
"Process a single JSON-RPC request and return response"
36+
[handlers request]
37+
(if-let [validation-error (protocol/validate-request request)]
38+
validation-error
39+
(let [{:keys [method params id]} request
40+
handler (get handlers method)]
41+
(if handler
42+
(try
43+
(let [result (handler params)]
44+
(protocol/result-response id result))
45+
(catch Exception e
46+
(protocol/error-response
47+
(get protocol/error-codes :internal-error)
48+
(.getMessage e)
49+
{:id id})))
50+
(protocol/error-response
51+
(get protocol/error-codes :method-not-found)
52+
(str "Method not found: " method)
53+
{:id id})))))
54+
55+
(defn- handle-request
56+
"Handle an incoming HTTP request"
57+
[handlers exchange]
58+
(try
59+
(let [body (read-request-body exchange)
60+
[request parse-error] (protocol/parse-json body)
61+
response (if parse-error
62+
parse-error
63+
(cond
64+
(map? request)
65+
(handle-json-rpc handlers request)
66+
67+
(sequential? request)
68+
(mapv #(handle-json-rpc handlers %) request)
69+
70+
:else
71+
(protocol/error-response
72+
(get protocol/error-codes :invalid-request)
73+
"Invalid request format")))
74+
[json-response json-error] (protocol/write-json response)]
75+
(if json-error
76+
(send-response exchange
77+
(protocol/write-json
78+
(protocol/error-response
79+
(get protocol/error-codes :internal-error)
80+
"Response encoding error")))
81+
(send-response exchange json-response)))
82+
(catch Exception e
83+
(send-response exchange
84+
(protocol/write-json
85+
(protocol/error-response
86+
(get protocol/error-codes :internal-error)
87+
"Internal server error"))))))
888

989
(defn create-server
1090
"Create a new JSON-RPC server.
11-
91+
1292
Configuration options:
1393
- :port Required. Port number to listen on
1494
- :handlers Required. Map of method names to handler functions
15-
95+
1696
Returns a map containing:
1797
- :server The server instance
1898
- :stop Function to stop the server
19-
99+
20100
Example:
21101
```clojure
22102
(create-server
@@ -28,29 +108,17 @@
28108
(throw (ex-info "Port is required" {:config config})))
29109
(when-not (map? handlers)
30110
(throw (ex-info "Handlers must be a map" {:config config})))
31-
32-
;; TODO: Implement server creation
33-
)
34111

35-
;;; Request handling
112+
(let [server (HttpServer/create (InetSocketAddress. port) 0)
113+
executor (Executors/newFixedThreadPool 10)
114+
handler (reify HttpHandler
115+
(handle [_ exchange]
116+
(handle-request handlers exchange)))]
36117

37-
(defn- handle-request
38-
"Handle a single JSON-RPC request.
39-
Converts JSON to EDN, dispatches to handler, converts response to JSON."
40-
[handlers request]
41-
;; TODO: Implement request handling
42-
)
43-
44-
(defn- handle-batch
45-
"Handle a batch of JSON-RPC requests."
46-
[handlers requests]
47-
;; TODO: Implement batch handling
48-
)
49-
50-
;;; Handler dispatch
51-
52-
(defn- dispatch-request
53-
"Dispatch a request to its handler and return the response."
54-
[handlers {:keys [method params] :as request}]
55-
;; TODO: Implement handler dispatch
56-
)
118+
(.setExecutor server executor)
119+
(.createContext server "/" handler)
120+
(.start server)
121+
122+
{:server server
123+
:stop #(do (.stop server 1)
124+
(.shutdown executor))}))
Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
(ns mcp-clj.json-rpc.protocol-test
2-
(:require [clojure.test :refer :all]
3-
[mcp-clj.json-rpc.protocol :as protocol]))
2+
(:require
3+
[clojure.test :refer :all]
4+
[mcp-clj.json-rpc.protocol :as protocol]))
45

56
(deftest json-conversion
67
(testing "EDN to JSON conversion"
78
(let [[json err] (protocol/write-json {:a 1 :b [1 2 3]})]
89
(is (nil? err))
910
(is (= "{\"a\":1,\"b\":[1,2,3]}" json))))
10-
11+
1112
(testing "JSON to EDN conversion"
1213
(let [[data err] (protocol/parse-json "{\"a\":1,\"b\":[1,2,3]}")]
1314
(is (nil? err))
@@ -17,12 +18,12 @@
1718
(testing "Valid request"
1819
(is (nil? (protocol/validate-request
1920
{:jsonrpc "2.0"
20-
:method "test"
21-
:params {:a 1}}))))
22-
21+
:method "test"
22+
:params {:a 1}}))))
23+
2324
(testing "Invalid version"
2425
(let [response (protocol/validate-request
25-
{:jsonrpc "1.0"
26-
:method "test"})]
26+
{:jsonrpc "1.0"
27+
:method "test"})]
2728
(is (= "Invalid JSON-RPC version"
28-
(get-in response [:error :message]))))))
29+
(get-in response [:error :message]))))))
Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,71 @@
11
(ns mcp-clj.json-rpc.server-test
2-
(:require [clojure.test :refer :all]
3-
[mcp-clj.json-rpc.server :as server]
4-
[mcp-clj.json-rpc.protocol :as protocol]))
2+
(:require
3+
[clojure.test :refer :all]
4+
[mcp-clj.json-rpc.server :as server]
5+
[mcp-clj.json-rpc.protocol :as protocol])
6+
(:import
7+
[java.net HttpURLConnection URL]))
8+
9+
(defn- send-request
10+
"Helper function to send a JSON-RPC request to the server"
11+
[url request-data]
12+
(let [conn (.openConnection (URL. url))
13+
json-data (first (protocol/write-json request-data))]
14+
(doto conn
15+
(.setRequestMethod "POST")
16+
(.setRequestProperty "Content-Type" "application/json")
17+
(.setRequestProperty "Content-Length" (str (count (.getBytes json-data))))
18+
(.setDoOutput true))
19+
20+
(with-open [os (.getOutputStream conn)]
21+
(.write os (.getBytes json-data))
22+
(.flush os))
23+
24+
(let [response-code (.getResponseCode conn)
25+
response-body (slurp (.getInputStream conn))]
26+
{:status response-code
27+
:body (first (protocol/parse-json response-body))})))
528

629
(deftest server-creation
730
(testing "Server creation validation"
831
(is (thrown-with-msg? clojure.lang.ExceptionInfo
9-
#"Port is required"
10-
(server/create-server {})))
11-
32+
#"Port is required"
33+
(server/create-server {})))
34+
1235
(is (thrown-with-msg? clojure.lang.ExceptionInfo
13-
#"Handlers must be a map"
14-
(server/create-server {:port 8080}))))
36+
#"Handlers must be a map"
37+
(server/create-server {:port 8080}))))
1538

1639
(testing "Successful server creation"
17-
(let [handlers {"echo" (fn [params] {:result params})}
40+
(let [handlers {"echo" (fn [params] params)}
1841
{:keys [server stop]} (server/create-server
19-
{:port 8080
20-
:handlers handlers})]
42+
{:port 8080
43+
:handlers handlers})]
2144
(try
2245
(is (some? server))
2346
(is (fn? stop))
2447
(finally
25-
(stop))))))
48+
(stop))))))
49+
50+
(deftest server-functionality
51+
(testing "Echo method response"
52+
(let [test-port 8081
53+
test-data {:message "Hello" :number 42}
54+
handlers {"echo" (fn [params] params)}
55+
{:keys [stop]} (server/create-server
56+
{:port test-port
57+
:handlers handlers})]
58+
(try
59+
(let [request {:jsonrpc "2.0"
60+
:method "echo"
61+
:params test-data
62+
:id 1}
63+
{:keys [status body]} (send-request
64+
(str "http://localhost:" test-port)
65+
request)]
66+
(is (= 200 status))
67+
(is (= "2.0" (:jsonrpc body)))
68+
(is (= 1 (:id body)))
69+
(is (= test-data (:result body))))
70+
(finally
71+
(stop))))))

0 commit comments

Comments
 (0)