Skip to content

Commit 79dda5f

Browse files
author
Bruce Hauman
committed
bring sse server upto speed
1 parent 88cd691 commit 79dda5f

File tree

2 files changed

+180
-67
lines changed

2 files changed

+180
-67
lines changed

src/clojure_mcp/core.clj

Lines changed: 122 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@
349349
(s/def ::parse-nrepl-port boolean?)
350350
(s/def ::nrepl-args (s/keys :req-un []
351351
:opt-un [::port ::host ::config-file ::project-dir ::nrepl-env-type
352-
::start-nrepl-cmd ::parse-nrepl-port]))
352+
::start-nrepl-cmd ::parse-nrepl-port]))
353353

354354
(def nrepl-client-atom (atom nil))
355355

@@ -374,6 +374,108 @@
374374
:spec-data (s/explain-data ::nrepl-args opts)})))
375375
opts)))
376376

377+
(defn ensure-port
378+
"Ensures the args map contains a :port key.
379+
Throws an exception with helpful context if port is missing.
380+
381+
Args:
382+
- args: Map that should contain :port
383+
384+
Returns: args unchanged if :port exists
385+
386+
Throws: ExceptionInfo if :port is missing"
387+
[args]
388+
(if (:port args)
389+
args
390+
(throw
391+
(ex-info
392+
"No nREPL port available - either provide :port or configure auto-start"
393+
{:provided-args args}))))
394+
395+
(defn register-components
396+
"Registers tools, prompts, and resources with the MCP server, applying config-based filtering.
397+
398+
Args:
399+
- mcp-server: The MCP server instance to add components to
400+
- nrepl-client-map: The nREPL client map containing config
401+
- components: Map with :tools, :prompts, and :resources sequences
402+
403+
Side effects:
404+
- Adds enabled components to the MCP server
405+
- Logs debug messages for enabled components
406+
407+
Returns: nil"
408+
[mcp-server nrepl-client-map {:keys [tools prompts resources]}]
409+
;; Register tools with filtering
410+
(doseq [tool tools]
411+
(when (config/tool-id-enabled? nrepl-client-map (:id tool))
412+
(log/debug "Enabling tool:" (:id tool))
413+
(add-tool mcp-server tool)))
414+
415+
;; Register resources with filtering
416+
(doseq [resource resources]
417+
(when (config/resource-name-enabled? nrepl-client-map (:name resource))
418+
(log/debug "Enabling resource:" (:name resource))
419+
(add-resource mcp-server resource)))
420+
421+
;; Register prompts with filtering
422+
(doseq [prompt prompts]
423+
(when (config/prompt-name-enabled? nrepl-client-map (:name prompt))
424+
(log/debug "Enabling prompt:" (:name prompt))
425+
(add-prompt mcp-server prompt)))
426+
nil)
427+
428+
(defn build-components
429+
"Builds tools, prompts, and resources using the provided factory functions.
430+
431+
Args:
432+
- nrepl-client-atom: Atom containing the nREPL client
433+
- working-dir: Working directory path
434+
- component-factories: Map with factory functions
435+
- :make-tools-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of tools
436+
- :make-prompts-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of prompts
437+
- :make-resources-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of resources
438+
439+
Returns: Map with :tools, :prompts, and :resources sequences"
440+
[nrepl-client-atom working-dir {:keys [make-tools-fn
441+
make-prompts-fn
442+
make-resources-fn]
443+
:as component-factories}]
444+
{:tools (when make-tools-fn
445+
(doall (make-tools-fn nrepl-client-atom working-dir)))
446+
:prompts (when make-prompts-fn
447+
(doall (make-prompts-fn nrepl-client-atom working-dir)))
448+
:resources (when make-resources-fn
449+
(doall (make-resources-fn nrepl-client-atom working-dir)))})
450+
451+
(defn setup-mcp-server
452+
"Sets up an MCP server by building components, creating the server, and registering components.
453+
454+
This function encapsulates the common pattern used by both stdio and SSE transports:
455+
1. Build components using factory functions
456+
2. Create the MCP server (transport-specific)
457+
3. Register components with filtering
458+
459+
Args:
460+
- nrepl-client-atom: Atom containing the nREPL client map
461+
- working-dir: Working directory path
462+
- component-factories: Map with factory functions (:make-tools-fn, :make-prompts-fn, :make-resources-fn)
463+
- server-thunk: Zero-argument function that creates and returns a map with :mcp-server
464+
465+
The server-thunk is called AFTER components are built but BEFORE they are registered,
466+
ensuring components are ready for immediate registration once the server starts.
467+
468+
Returns: The result map from server-thunk (containing at least :mcp-server)"
469+
[nrepl-client-atom working-dir component-factories server-thunk]
470+
;; Build components first to minimize latency
471+
(let [components (build-components nrepl-client-atom working-dir component-factories)
472+
;; Create server after components are ready
473+
server-result (server-thunk)
474+
mcp-server (:mcp-server server-result)]
475+
;; Register components with filtering
476+
(register-components mcp-server @nrepl-client-atom components)
477+
server-result))
478+
377479
(defn build-and-start-mcp-server-impl
378480
"Internal implementation of MCP server startup.
379481
@@ -392,7 +494,7 @@
392494
- :host (optional) - nREPL server host (defaults to localhost)
393495
- :project-dir (optional) - Root directory for the project (must exist)
394496
395-
- config: Map with factory functions
497+
- component-factories: Map with factory functions
396498
- :make-tools-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of tools
397499
- :make-prompts-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of prompts
398500
- :make-resources-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of resources
@@ -404,9 +506,7 @@
404506
- Starts the MCP server on stdio
405507
406508
Returns: nil"
407-
[nrepl-args {:keys [make-tools-fn
408-
make-prompts-fn
409-
make-resources-fn]}]
509+
[nrepl-args component-factories]
410510
;; the nrepl-args are a map with :port and optional :host
411511
;; Note: validation should be done by caller
412512
(let [_ (assert (:port nrepl-args) "Port must be provided for build-and-start-mcp-server-impl")
@@ -417,25 +517,13 @@
417517
(assoc nrepl-client-map :nrepl-process process)
418518
nrepl-client-map)
419519
_ (reset! nrepl-client-atom nrepl-client-with-process)
420-
resources (when make-resources-fn
421-
(doall (make-resources-fn nrepl-client-atom working-dir)))
422-
tools (when make-tools-fn
423-
(doall (make-tools-fn nrepl-client-atom working-dir)))
424-
prompts (when make-prompts-fn
425-
(doall (make-prompts-fn nrepl-client-atom working-dir)))
426-
mcp (mcp-server)]
427-
(doseq [tool tools]
428-
(when (config/tool-id-enabled? nrepl-client-map (:id tool))
429-
(log/debug "Enabling tool:" (:id tool))
430-
(add-tool mcp tool)))
431-
(doseq [resource resources]
432-
(when (config/resource-name-enabled? nrepl-client-map (:name resource))
433-
(log/debug "Enabling resource:" (:name resource))
434-
(add-resource mcp resource)))
435-
(doseq [prompt prompts]
436-
(when (config/prompt-name-enabled? nrepl-client-map (:name prompt))
437-
(log/debug "Enabling prompt:" (:name prompt))
438-
(add-prompt mcp prompt)))
520+
;; Setup MCP server with stdio transport
521+
server-result (setup-mcp-server nrepl-client-atom
522+
working-dir
523+
component-factories
524+
;; stdio server creation thunk returns map
525+
(fn [] {:mcp-server (mcp-server)}))
526+
mcp (:mcp-server server-result)]
439527
(swap! nrepl-client-atom assoc :mcp-server mcp)
440528
nil))
441529

@@ -460,22 +548,19 @@
460548
- :start-nrepl-cmd (optional) - Command to start nREPL server
461549
- :parse-nrepl-port (optional) - Parse port from command output (default true)
462550
463-
- config: Map with factory functions (same as
464-
build-and-start-mcp-server-impl)
551+
- component-factories: Map with factory functions
552+
- :make-tools-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of tools
553+
- :make-prompts-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of prompts
554+
- :make-resources-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of resources
465555
466556
Auto-start conditions (must satisfy ONE):
467557
1. Both :start-nrepl-cmd AND :project-dir provided in nrepl-args
468558
2. Current directory contains .clojure-mcp/config.edn with :start-nrepl-cmd
469559
470560
Returns: nil"
471-
[nrepl-args config]
472-
(let [validated-args (validate-options nrepl-args)
473-
args-with-port (nrepl-launcher/maybe-start-nrepl-process
474-
validated-args)]
475-
;; Ensure we have a port after auto-start or validation
476-
(when-not (:port args-with-port)
477-
(throw
478-
(ex-info
479-
"No nREPL port available - either provide :port or configure auto-start"
480-
{:provided-args nrepl-args})))
481-
(build-and-start-mcp-server-impl args-with-port config)))
561+
[nrepl-args component-factories]
562+
(-> nrepl-args
563+
validate-options
564+
nrepl-launcher/maybe-start-nrepl-process
565+
ensure-port
566+
(build-and-start-mcp-server-impl component-factories)))

src/clojure_mcp/sse_core.clj

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
[clojure-mcp.main :as main]
44
[clojure-mcp.core :as core]
55
[clojure-mcp.config :as config]
6+
[clojure-mcp.nrepl-launcher :as nrepl-launcher]
67
[clojure.tools.logging :as log])
78
(:import
89
[io.modelcontextprotocol.server.transport
@@ -52,19 +53,19 @@
5253
(println (str "Clojure tooling SSE MCP server started on port " port "."))
5354
(.join server)))
5455

55-
(defn build-and-start-mcp-server
56-
"Builds and starts an MCP server with SSE (Server-Sent Events) transport.
56+
(defn build-and-start-mcp-server-impl
57+
"Internal implementation of MCP server with SSE transport.
5758
58-
Similar to core/build-and-start-mcp-server but uses SSE transport instead
59-
of stdio, allowing web-based clients to connect over HTTP.
59+
Similar to core/build-and-start-mcp-server-impl but uses SSE transport
60+
instead of stdio, allowing web-based clients to connect over HTTP.
6061
6162
Args:
62-
- args: Map with connection and server settings
63+
- nrepl-args: Map with connection settings
6364
- :port (required) - nREPL server port
6465
- :host (optional) - nREPL server host (defaults to localhost)
6566
- :mcp-sse-port (optional) - HTTP port for SSE server (defaults to 8078)
6667
67-
- config: Map with factory functions
68+
- component-factories: Map with factory functions
6869
- :make-tools-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of tools
6970
- :make-prompts-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of prompts
7071
- :make-resources-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of resources
@@ -77,34 +78,61 @@
7778
- Starts a Jetty HTTP server on the specified port
7879
7980
Returns: nil"
80-
[args {:keys [make-tools-fn
81-
make-resources-fn
82-
make-prompts-fn]}]
83-
;; the args are a map with :port :host
84-
;; we also need an :mcp-sse-port so we'll default to 8078??
85-
(let [mcp-port (:mcp-sse-port args 8078)
86-
nrepl-client-map (core/create-and-start-nrepl-connection args)
81+
[nrepl-args component-factories]
82+
;; Note: validation should be done by caller
83+
(let [_ (assert (:port nrepl-args) "Port must be provided for build-and-start-mcp-server-impl")
84+
mcp-port (:mcp-sse-port nrepl-args 8078)
85+
nrepl-client-map (core/create-and-start-nrepl-connection nrepl-args)
8786
working-dir (config/get-nrepl-user-dir nrepl-client-map)
88-
_ (reset! core/nrepl-client-atom nrepl-client-map)
89-
resources (when make-resources-fn
90-
(doall (make-resources-fn
91-
core/nrepl-client-atom working-dir)))
92-
tools (when make-tools-fn
93-
(doall (make-tools-fn
94-
core/nrepl-client-atom working-dir)))
95-
prompts (when make-prompts-fn
96-
(doall (make-prompts-fn
97-
core/nrepl-client-atom working-dir)))
98-
{:keys [mcp-server provider-servlet]} (mcp-sse-server)]
99-
(doseq [tool tools]
100-
(core/add-tool mcp-server tool))
101-
(doseq [resource resources]
102-
(core/add-resource mcp-server resource))
103-
(doseq [prompt prompts]
104-
(core/add-prompt mcp-server prompt))
87+
;; Store nREPL process (if auto-started) in client map for cleanup
88+
nrepl-client-with-process (if-let [process (:nrepl-process nrepl-args)]
89+
(assoc nrepl-client-map :nrepl-process process)
90+
nrepl-client-map)
91+
_ (reset! core/nrepl-client-atom nrepl-client-with-process)
92+
{:keys [mcp-server provider-servlet]}
93+
(core/setup-mcp-server core/nrepl-client-atom working-dir component-factories mcp-sse-server)]
10594
;; hold onto this so you can shut it down if necessary
10695
(swap! core/nrepl-client-atom assoc :mcp-server mcp-server)
96+
;; Start the HTTP server with the servlet
10797
(host-mcp-servlet provider-servlet mcp-port)
10898
nil))
10999

100+
(defn build-and-start-mcp-server
101+
"Builds and starts an MCP server with SSE transport and optional automatic nREPL startup.
102+
103+
This function wraps build-and-start-mcp-server-impl with nREPL auto-start capability.
104+
105+
If auto-start conditions are met (see nrepl-launcher/should-start-nrepl?), it will:
106+
1. Start an nREPL server process using :start-nrepl-cmd
107+
2. Parse the port from process output (if :parse-nrepl-port is true)
108+
3. Pass the discovered port to the main MCP server setup
109+
110+
Otherwise, it requires a :port parameter.
111+
112+
Args:
113+
- nrepl-args: Map with connection settings and optional nREPL start configuration
114+
- :port (required if not auto-starting) - nREPL server port
115+
- :host (optional) - nREPL server host (defaults to localhost)
116+
- :mcp-sse-port (optional) - HTTP port for SSE server (defaults to 8078)
117+
- :project-dir (optional) - Root directory for the project
118+
- :start-nrepl-cmd (optional) - Command to start nREPL server
119+
- :parse-nrepl-port (optional) - Parse port from command output (default true)
120+
121+
- component-factories: Map with factory functions
122+
- :make-tools-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of tools
123+
- :make-prompts-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of prompts
124+
- :make-resources-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of resources
125+
126+
Auto-start conditions (must satisfy ONE):
127+
1. Both :start-nrepl-cmd AND :project-dir provided in nrepl-args
128+
2. Current directory contains .clojure-mcp/config.edn with :start-nrepl-cmd
129+
130+
Returns: nil"
131+
[nrepl-args component-factories]
132+
(-> nrepl-args
133+
core/validate-options
134+
nrepl-launcher/maybe-start-nrepl-process
135+
core/ensure-port
136+
(build-and-start-mcp-server-impl component-factories)))
137+
110138

0 commit comments

Comments
 (0)