Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
32f2387
chore: update task management configuration
hugoduncan Oct 23, 2025
e7acdca
build: replace clojure.data.json with cheshire in deps.edn
hugoduncan Oct 23, 2025
47c7eb0
refactor: migrate json_protocol.clj to cheshire API
hugoduncan Oct 23, 2025
1c3c2d0
refactor: migrate stdio JSON I/O from clojure.data.json to cheshire
hugoduncan Oct 23, 2025
36d9efb
refactor: migrate HTTP modules from clojure.data.json to cheshire
hugoduncan Oct 23, 2025
3843141
refactor: migrate test files to cheshire JSON API
hugoduncan Oct 23, 2025
2c0ff84
fix: add cheshire compatibility layer for JSON parsing
hugoduncan Oct 23, 2025
0e9668b
fix: complete migration to cheshire in remaining 6 files
hugoduncan Oct 23, 2025
1395687
refactor: create json component to centralize JSON handling
hugoduncan Oct 23, 2025
9c4a92e
fix: complete json component API migration in 3 remaining files
hugoduncan Oct 23, 2025
d6780b9
fix: normalize JSON-RPC request IDs to Long for HashMap lookups
hugoduncan Oct 23, 2025
1fa8b5c
fix: complete JSON component API migration and add normalization
hugoduncan Oct 23, 2025
b084b01
docs: document cheshire migration and normalization rationale
hugoduncan Oct 23, 2025
240628c
test: add comprehensive JSON parse error handling tests
hugoduncan Oct 23, 2025
9fb962e
chore: remove task tracking file
hugoduncan Oct 23, 2025
5748018
perf: eliminate reflection warnings in stdio and json components
hugoduncan Oct 23, 2025
e6f7b00
perf: optimize JSON parsing with parse-string-strict
hugoduncan Oct 23, 2025
81c5539
fix: add Long coercion for request IDs in pending request tracking
hugoduncan Oct 23, 2025
699bdfe
fix: handle empty/nil JSON input and cheshire nil return value
hugoduncan Oct 23, 2025
df2f732
fix: correct JSON null handling and parse error HTTP status codes
hugoduncan Oct 23, 2025
58ede9b
test: fix stdio JSON parse error test cases with proper line endings
hugoduncan Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .mcp-tasks.edn
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{:story-branch-management? true}
{:branch-management? true}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
(let [^Process process (:process process-info)]
(try
(.destroy process)
(when-not (.waitFor process 5000 TimeUnit/MILLISECONDS)
(when-not (.waitFor process (long 5000) TimeUnit/MILLISECONDS)
(log/warn :client/process-force-kill))
(catch Exception e
(log/error :client/process-close-error {:error e})))))
Expand Down
2 changes: 1 addition & 1 deletion components/http-client/deps.edn
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{:paths ["src"]
:deps {org.clojure/clojure {:mvn/version "1.12.0"}
org.clojure/data.json {:mvn/version "2.5.0"}}
poly/json {:local/root "../json"}}
:aliases
{:test {:extra-paths ["test"]
:extra-deps {}}}}
6 changes: 3 additions & 3 deletions components/http-client/src/mcp_clj/http_client/core.clj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
(ns mcp-clj.http-client.core
"HTTP client implementation using JDK HttpClient"
(:require
[clojure.data.json :as json]
[clojure.string :as str])
[clojure.string :as str]
[mcp-clj.json :as json])
(:import
(java.net
URI)
Expand Down Expand Up @@ -98,7 +98,7 @@
:headers headers
:body (case as
:json (when (and body (not (str/blank? body)))
(json/read-str body :key-fn keyword))
(json/parse body))
:stream body
:string body
body)}))
Expand Down
4 changes: 3 additions & 1 deletion components/http/deps.edn
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
{}
{:paths ["src"]
:deps {org.clojure/clojure {:mvn/version "1.12.0"}
poly/json {:local/root "../json"}}}
8 changes: 4 additions & 4 deletions components/http/src/mcp_clj/http.clj
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
(ns mcp-clj.http
(:require
[clojure.data.json :as json]))
[mcp-clj.json :as json]))

(defn response
"Return a minimal status 200 response map with the given body."
[body]
{:status 200
{:status 200
:headers {}
:body body})
:body body})

(defn status
"Returns an updated response map with the given status."
Expand All @@ -27,7 +27,7 @@
(defn json-response
"Create a JSON response with given status"
[data status-code]
(-> (response (json/write-str data))
(-> (response (json/write data))
(status status-code)
(content-type "application/json")))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,29 @@
;; Pending requests management

(defn add-pending-request!
"Add a pending request future"
"Add a pending request future.

Coerces request-id to Long for consistent map key type."
[shared-transport request-id future]
(swap! (:pending-requests shared-transport) assoc request-id future))
(swap! (:pending-requests shared-transport) assoc (long request-id) future))

(defn remove-pending-request!
"Remove and return a pending request future"
"Remove and return a pending request future.

Coerces request-id to Long to handle JSON parsing returning Integer."
^CompletableFuture [shared-transport request-id]
(let [requests (:pending-requests shared-transport)
future (get @requests request-id)]
(swap! requests dissoc request-id)
id (long request-id)
future (get @requests id)]
(swap! requests dissoc id)
future))

(defn get-pending-request
"Get a pending request future without removing it"
"Get a pending request future without removing it.

Coerces request-id to Long to handle JSON parsing returning Integer."
[shared-transport request-id]
(get @(:pending-requests shared-transport) request-id))
(get @(:pending-requests shared-transport) (long request-id)))

;; Server handler management

Expand Down
12 changes: 6 additions & 6 deletions components/json-rpc/deps.edn
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{:paths ["src"]
:deps {org.clojure/clojure {:mvn/version "1.12.0"}
org.clojure/data.json {:mvn/version "2.5.0"}
poly/http-client {:local/root "../http-client"}}
:deps {org.clojure/clojure {:mvn/version "1.12.0"}
poly/http-client {:local/root "../http-client"}
poly/json {:local/root "../json"}}
:aliases
{:dev {:extra-paths ["test"]
:extra-deps {hato/hato {:mvn/version "1.0.0"}}}
{:dev {:extra-paths ["test"]
:extra-deps {hato/hato {:mvn/version "1.0.0"}}}
:test {:extra-paths ["test"]
:extra-deps {hato/hato {:mvn/version "1.0.0"}}}}}
:extra-deps {hato/hato {:mvn/version "1.0.0"}}}}}
50 changes: 26 additions & 24 deletions components/json-rpc/src/mcp_clj/json_rpc/http_client.clj
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
(ns mcp-clj.json-rpc.http-client
"JSON-RPC client for HTTP transport with SSE support"
(:require
[clojure.data.json :as json]
[clojure.string :as str]
[mcp-clj.http-client.core :as http-client]
[mcp-clj.json :as json]
[mcp-clj.json-rpc.executor :as executor]
[mcp-clj.json-rpc.protocols :as protocols]
[mcp-clj.log :as log])
Expand Down Expand Up @@ -71,12 +71,14 @@
"Handle JSON-RPC response by completing the corresponding future"
[client {:keys [id result error] :as response}]
(if id
(if-let [future (.remove ^ConcurrentHashMap (:pending-requests client) id)]
(if error
(.completeExceptionally ^CompletableFuture future
(ex-info "JSON-RPC error" error))
(.complete ^CompletableFuture future result))
(log/warn :rpc/orphan-response {:response response}))
;; Normalize ID to Long to handle Integer/Long mismatch from JSON parsing
(let [normalized-id (long id)]
(if-let [future (.remove ^ConcurrentHashMap (:pending-requests client) normalized-id)]
(if error
(.completeExceptionally ^CompletableFuture future
(ex-info "JSON-RPC error" error))
(.complete ^CompletableFuture future result))
(log/warn :rpc/orphan-response {:response response})))
;; No ID means it's a notification
(when-let [handler (:notification-handler client)]
(handler response))))
Expand Down Expand Up @@ -122,7 +124,7 @@
;; Empty line signals end of event
(when-let [event (parse-sse-event event-lines)]
(try
(let [data (json/read-str (:data event) :key-fn keyword)]
(let [data (json/parse (:data event))]
(log/debug :sse/event {:event event :data data})
(handle-response client data))
(catch Exception e
Expand Down Expand Up @@ -185,11 +187,11 @@
"Send JSON-RPC request and return CompletableFuture with response"
[client method params timeout-ms]
(let [request-id (generate-request-id client)
request {:jsonrpc "2.0"
:id request-id
:method method
:params params}
future (CompletableFuture.)]
request {:jsonrpc "2.0"
:id request-id
:method method
:params params}
future (CompletableFuture.)]

;; Store the future for this request
(.put ^ConcurrentHashMap (:pending-requests client) request-id future)
Expand All @@ -200,17 +202,17 @@
(fn []
(try
(log/debug :client/send "Send request" {:method method})
(let [url (str (:base-url client) "/")
headers (make-headers client)
(let [url (str (:base-url client) "/")
headers (make-headers client)
response (http-client/http-post url
{:headers headers
:body (json/write-str request)
:content-type :json
:accept :json
:as :json
{:headers headers
:body (json/write request)
:content-type :json
:accept :json
:as :json
:throw-exceptions false})]
(log/debug :client/send
{:msg "Receive response"
{:msg "Receive response"
:response response})
(if (= 200 (:status response))
(do
Expand Down Expand Up @@ -239,13 +241,13 @@
(log/error
:http/request-failed
{:status (:status response)
:body (:body response)})
:body (:body response)})
(.completeExceptionally
future
(ex-info
error-msg
{:status (:status response)
:body (:body response)})))))
:body (:body response)})))))
(catch Exception e
(log/error :http/request-error {:error e :method method})
(.completeExceptionally future e)))))
Expand Down Expand Up @@ -273,7 +275,7 @@
response (http-client/http-post
url
{:headers headers
:body (json/write-str notification)
:body (json/write notification)
:content-type :json
:throw-exceptions false})]
(when (not= 200 (:status response))
Expand Down
89 changes: 56 additions & 33 deletions components/json-rpc/src/mcp_clj/json_rpc/http_server.clj
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
(ns mcp-clj.json-rpc.http-server
"JSON-RPC 2.0 server with MCP Streamable HTTP transport (2025-03-26 spec)"
(:require
[clojure.data.json :as json]
[clojure.string :as str]
[mcp-clj.http :as http]
[mcp-clj.http-server.adapter :as http-server]
[mcp-clj.json :as json]
[mcp-clj.json-rpc.executor :as executor]
[mcp-clj.json-rpc.json-protocol :as json-protocol]
[mcp-clj.json-rpc.protocols :as protocols]
Expand Down Expand Up @@ -91,7 +91,7 @@
(let [session-id (extract-session-id request)
session (get @session-id->session session-id)
body-str (slurp (:body request))
rpc-data (json/read-str body-str :key-fn keyword)]
rpc-data (json/parse body-str)]
(log/info :rpc/http-post
{:session-id session-id
:has-session (some? session)
Expand All @@ -111,13 +111,36 @@
:internal-error
(.getMessage e))))]
(http/json-response result http/Ok)))))
(catch com.fasterxml.jackson.core.JsonParseException e
(log/warn :rpc/json-parse-error {:error (.getMessage e)})
(http/json-response
(json-protocol/json-rpc-error
:parse-error
(str "Invalid JSON: " (.getMessage e)))
http/BadRequest))
(catch clojure.lang.ExceptionInfo e
(if (= :parse-error (:type (ex-data e)))
(do
(log/warn :rpc/json-parse-error {:error (.getMessage e)})
(http/json-response
(json-protocol/json-rpc-error
:parse-error
(.getMessage e))
http/BadRequest))
(do
(log/error :rpc/post-error {:error (.getMessage e)})
(http/json-response
(json-protocol/json-rpc-error
:internal-error
(.getMessage e))
http/InternalServerError))))
(catch Exception e
(log/error :rpc/post-error {:error (.getMessage e)})
(http/json-response
(json-protocol/json-rpc-error
:parse-error
"Invalid JSON")
http/BadRequest))))
:internal-error
(.getMessage e))
http/InternalServerError))))

(defn- handle-sse-get
"Handle SSE stream setup via GET"
Expand All @@ -135,7 +158,7 @@
session-id
(fn [message]
(let [event-id (.incrementAndGet event-counter)]
(reply! (assoc (sse/message (json/write-str message))
(reply! (assoc (sse/message (json/write message))
:id (str event-id)))))
(fn []
(log/info :http/sse-close {:session-id session-id})
Expand Down Expand Up @@ -176,19 +199,19 @@
(if (str/includes? (get (:headers request) "accept" "") "text/event-stream")
(handle-sse-get session-id->session allowed-origins request)
(http/json-response
{"transport" "streamable-http"
"version" "2025-03-26"
"capabilities" {"sse" true
"batch" true
{"transport" "streamable-http"
"version" "2025-03-26"
"capabilities" {"sse" true
"batch" true
"resumable" true}}
http/Ok))

;; 404 for unknown endpoints
(do
(log/warn :http/not-found
{:method request-method
:uri uri
:request request
{:method request-method
:uri uri
:request request
:handlers handlers})
(http/text-response "Not Found" http/NotFound))))

Expand All @@ -201,35 +224,35 @@
allowed-origins
on-connect
on-disconnect]
:or {num-threads (* 2 (.availableProcessors (Runtime/getRuntime)))
port 0
allowed-origins []
on-connect (fn [& _])
on-disconnect (fn [& _])}}]
:or {num-threads (* 2 (.availableProcessors (Runtime/getRuntime)))
port 0
allowed-origins []
on-connect (fn [& _])
on-disconnect (fn [& _])}}]
{:pre [(ifn? on-connect) (ifn? on-disconnect) (coll? allowed-origins)]}
(let [executor (executor/create-executor num-threads)
session-id->session (atom {})
handlers (atom nil)
handler (partial handle-request
executor
session-id->session
handlers
allowed-origins)
(let [executor (executor/create-executor num-threads)
session-id->session (atom {})
handlers (atom nil)
handler (partial handle-request
executor
session-id->session
handlers
allowed-origins)
{:keys [server port stop]} (http-server/run-server
handler
{:executor executor :port port})]

(log/info :http/server-created
{:port port :allowed-origins allowed-origins})

{:server server
:port port
:handlers handlers
{:server server
:port port
:handlers handlers
:session-id->session session-id->session
:stop (fn []
(log/info :http/server-stopping)
(stop)
(executor/shutdown-executor executor))}))
:stop (fn []
(log/info :http/server-stopping)
(stop)
(executor/shutdown-executor executor))}))

;; Server Operations

Expand Down
Loading
Loading