|
5 | 5 | [taoensso.timbre :as log] |
6 | 6 | [clojure-mcp.nrepl :as nrepl] |
7 | 7 | [clojure-mcp.config :as config] |
8 | | - [clojure-mcp.dialects :as dialects] |
9 | 8 | [clojure-mcp.file-content :as file-content] |
10 | 9 | [clojure-mcp.nrepl-launcher :as nrepl-launcher]) |
11 | 10 | (:import [io.modelcontextprotocol.server.transport |
|
255 | 254 | (throw e))))) |
256 | 255 |
|
257 | 256 | (defn fetch-config [nrepl-client-map config-file cli-env-type env-type project-dir] |
258 | | - (let [user-dir (dialects/fetch-project-directory nrepl-client-map env-type project-dir)] |
| 257 | + (let [user-dir (nrepl/fetch-project-directory nrepl-client-map env-type project-dir)] |
259 | 258 | (when-not user-dir |
260 | 259 | (log/warn "Could not determine working directory") |
261 | 260 | (throw (ex-info "No project directory!!" {}))) |
|
269 | 268 | (assoc nrepl-client-map ::config/config (assoc config :nrepl-env-type final-env-type))))) |
270 | 269 |
|
271 | 270 | (defn create-and-start-nrepl-connection |
272 | | - "Convenience higher-level API function to create and initialize an nREPL connection. |
273 | | - |
274 | | - This function handles the complete setup process including: |
275 | | - - Creating the nREPL client connection |
276 | | - - Loading required namespaces and helpers (if Clojure environment) |
| 271 | + "Creates an nREPL client map and loads configuration. |
| 272 | +
|
| 273 | + This function handles: |
| 274 | + - Creating the nREPL client connection (if port provided) |
277 | 275 | - Setting up the working directory |
278 | 276 | - Loading configuration |
279 | | - |
280 | | - Takes initial-config map with :port and optional :host, :project-dir, :nrepl-env-type, :config-file. |
| 277 | +
|
| 278 | + REPL initialization (env detection, init expressions, helpers) happens lazily |
| 279 | + when the first eval-code call is made. |
| 280 | +
|
| 281 | + Takes initial-config map with optional :port, :host, :project-dir, :nrepl-env-type, :config-file. |
| 282 | + - If :project-dir is provided, uses it directly (no REPL query needed) |
| 283 | + - If :project-dir is NOT provided, queries REPL for project directory (requires :port) |
| 284 | +
|
281 | 285 | Returns the configured nrepl-client-map with ::config/config attached." |
282 | | - [{:keys [project-dir config-file] :as initial-config}] |
283 | | - (log/info "Creating nREPL connection with config:" initial-config) |
| 286 | + [{:keys [project-dir config-file port] :as initial-config}] |
| 287 | + (if port |
| 288 | + (log/info "Creating nREPL client for port" port) |
| 289 | + (log/info "Starting without nREPL connection (project-dir mode)")) |
284 | 290 | (try |
285 | 291 | (let [nrepl-client-map (nrepl/create (dissoc initial-config :project-dir :nrepl-env-type)) |
286 | 292 | cli-env-type (:nrepl-env-type initial-config) |
287 | | - _ (log/info "nREPL client map created") |
288 | | - ;; Detect environment type early |
289 | | - ;; TODO this needs to be sorted out |
290 | | - env-type (dialects/detect-nrepl-env-type nrepl-client-map) |
291 | | - nrepl-client-map-with-config (fetch-config nrepl-client-map |
292 | | - config-file |
293 | | - cli-env-type |
294 | | - env-type |
295 | | - project-dir) |
296 | | - nrepl-env-type' (config/get-config nrepl-client-map-with-config :nrepl-env-type)] |
297 | | - (log/debug "Initializing Clojure environment") |
298 | | - (dialects/initialize-environment nrepl-client-map-with-config nrepl-env-type') |
299 | | - (dialects/load-repl-helpers nrepl-client-map-with-config nrepl-env-type') |
300 | | - (log/debug "Environment initialized") |
301 | | - nrepl-client-map-with-config) |
| 293 | + _ (log/info "nREPL client map created")] |
| 294 | + (if project-dir |
| 295 | + ;; Project dir provided - load config directly, no REPL query needed |
| 296 | + (let [user-dir (.getCanonicalPath (io/file project-dir)) |
| 297 | + _ (log/info "Working directory set to:" user-dir) |
| 298 | + config (load-config-handling-validation-errors config-file user-dir) |
| 299 | + ;; Use cli-env-type or config's env-type, default to :clj |
| 300 | + final-env-type (or cli-env-type |
| 301 | + (:nrepl-env-type config) |
| 302 | + :clj)] |
| 303 | + (assoc nrepl-client-map ::config/config (assoc config :nrepl-env-type final-env-type))) |
| 304 | + ;; No project dir - need to query REPL (requires port) |
| 305 | + (let [;; Detect environment type (uses describe op, no full init needed) |
| 306 | + env-type (nrepl/detect-nrepl-env-type nrepl-client-map) |
| 307 | + _ (nrepl/set-port-env-type! nrepl-client-map env-type)] |
| 308 | + (fetch-config nrepl-client-map config-file cli-env-type env-type project-dir)))) |
302 | 309 | (catch Exception e |
303 | 310 | (log/error e "Failed to create nREPL connection") |
304 | 311 | (throw e)))) |
305 | 312 |
|
306 | 313 | (defn create-additional-connection |
| 314 | + "Creates a service map for an additional port. Initialization is lazy. |
| 315 | + The returned service shares the base client's state atom and config, |
| 316 | + but targets a different port. |
| 317 | +
|
| 318 | + Note: The third argument (initialize-fn) is deprecated and ignored. |
| 319 | + Initialization now happens lazily via nrepl/ensure-port-initialized!" |
307 | 320 | ([nrepl-client-atom initial-config] |
308 | | - (create-additional-connection nrepl-client-atom initial-config identity)) |
309 | | - ([nrepl-client-atom initial-config initialize-fn] |
310 | | - (log/info "Creating additional nREPL connection" initial-config) |
311 | | - (try |
312 | | - (let [nrepl-client-map (nrepl/create initial-config)] |
313 | | - ;; copy config |
314 | | - ;; maybe we should create this just like the normal nrelp connection? |
315 | | - ;; we should introspect the project and get a working directory |
316 | | - ;; and maybe add it to allowed directories for both |
317 | | - (when initialize-fn (initialize-fn nrepl-client-map)) |
318 | | - (assert (::config/config @nrepl-client-atom)) |
319 | | - ;; copy config over for now |
320 | | - (assoc nrepl-client-map ::config/config (::config/config @nrepl-client-atom))) |
321 | | - (catch Exception e |
322 | | - (log/error e "Failed to create additional nREPL connection") |
323 | | - (throw e))))) |
| 321 | + (create-additional-connection nrepl-client-atom initial-config nil)) |
| 322 | + ([nrepl-client-atom {:keys [port host]} _deprecated-initialize-fn] |
| 323 | + (log/info "Creating additional connection config for port" port) |
| 324 | + (let [base-client @nrepl-client-atom] |
| 325 | + (assert (::config/config base-client) "Base client must have config") |
| 326 | + (-> base-client |
| 327 | + (assoc :port port) |
| 328 | + (cond-> host (assoc :host host)) |
| 329 | + ;; Ensure port entry exists but don't initialize yet (lazy init) |
| 330 | + nrepl/ensure-port-entry!)))) |
324 | 331 |
|
325 | 332 | (defn close-servers |
326 | 333 | "Convenience higher-level API function to gracefully shut down MCP and nREPL servers. |
|
487 | 494 |
|
488 | 495 | (defn build-and-start-mcp-server-impl |
489 | 496 | "Internal implementation of MCP server startup. |
490 | | - |
| 497 | +
|
491 | 498 | Builds and starts an MCP server with the provided configuration. |
492 | | - |
| 499 | +
|
493 | 500 | This is the main entry point for creating custom MCP servers. It handles: |
494 | | - - Validating input options |
495 | | - - Creating and starting the nREPL connection |
| 501 | + - Creating the nREPL client and loading configuration |
496 | 502 | - Setting up the working directory |
497 | 503 | - Calling factory functions to create tools, prompts, and resources |
498 | 504 | - Registering everything with the MCP server |
499 | | - |
| 505 | +
|
| 506 | + REPL initialization happens lazily on first eval-code call. |
| 507 | +
|
500 | 508 | Args: |
501 | 509 | - nrepl-args: Map with connection settings |
502 | | - - :port (required) - nREPL server port |
| 510 | + - :port (required if no :project-dir) - nREPL server port |
503 | 511 | - :host (optional) - nREPL server host (defaults to localhost) |
504 | | - - :project-dir (optional) - Root directory for the project (must exist) |
505 | | - |
| 512 | + - :project-dir (optional) - Root directory for the project. If provided, port is optional. |
| 513 | +
|
506 | 514 | - component-factories: Map with factory functions |
507 | 515 | - :make-tools-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of tools |
508 | | - - :make-prompts-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of prompts |
| 516 | + - :make-prompts-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of prompts |
509 | 517 | - :make-resources-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of resources |
510 | | - |
| 518 | +
|
511 | 519 | All factory functions are optional. If not provided, that category won't be populated. |
512 | | - |
| 520 | +
|
513 | 521 | Side effects: |
514 | 522 | - Stores the nREPL client in core/nrepl-client-atom |
515 | 523 | - Starts the MCP server on stdio |
516 | | - |
| 524 | +
|
517 | 525 | Returns: nil" |
518 | 526 | [nrepl-args component-factories] |
519 | | - ;; the nrepl-args are a map with :port and optional :host |
520 | | - ;; Note: validation should be done by caller |
521 | | - (let [_ (assert (:port nrepl-args) "Port must be provided for build-and-start-mcp-server-impl") |
522 | | - nrepl-client-map (create-and-start-nrepl-connection nrepl-args) |
| 527 | + ;; Either :port or :project-dir must be provided (validated by ensure-port-if-needed) |
| 528 | + (let [nrepl-client-map (create-and-start-nrepl-connection nrepl-args) |
523 | 529 | working-dir (config/get-nrepl-user-dir nrepl-client-map) |
524 | 530 | ;; Store nREPL process (if auto-started) in client map for cleanup |
525 | 531 | nrepl-client-with-process (if-let [process (:nrepl-process nrepl-args)] |
|
536 | 542 | (swap! nrepl-client-atom assoc :mcp-server mcp) |
537 | 543 | nil)) |
538 | 544 |
|
| 545 | +(defn ensure-port-if-needed |
| 546 | + "Ensures port is present when project-dir is NOT provided. |
| 547 | + When project-dir IS provided, port is optional (REPL not needed for config loading)." |
| 548 | + [args] |
| 549 | + (if (:project-dir args) |
| 550 | + args ;; project-dir provided, port is optional |
| 551 | + (ensure-port args))) ;; no project-dir, port is required |
| 552 | + |
539 | 553 | (defn build-and-start-mcp-server |
540 | 554 | "Builds and starts an MCP server with optional automatic nREPL startup. |
541 | | - |
| 555 | +
|
542 | 556 | This function wraps build-and-start-mcp-server-impl with nREPL auto-start capability. |
543 | | - |
| 557 | +
|
544 | 558 | If auto-start conditions are met (see nrepl-launcher/should-start-nrepl?), it will: |
545 | 559 | 1. Start an nREPL server process using :start-nrepl-cmd |
546 | 560 | 2. Parse the port from process output (if no :port provided) |
547 | 561 | 3. Pass the discovered port to the main MCP server setup |
548 | | - |
549 | | - Otherwise, it requires a :port parameter. |
550 | | - |
| 562 | +
|
| 563 | + Port is only required when :project-dir is NOT provided (need REPL to discover project dir). |
| 564 | +
|
551 | 565 | Args: |
552 | 566 | - nrepl-args: Map with connection settings and optional nREPL start |
553 | 567 | configuration |
554 | | - - :port (required if not auto-starting) - nREPL server port |
555 | | - When provided with :start-nrepl-cmd, uses fixed port instead of parsing |
556 | | - - :host (optional) - nREPL server host (defaults to localhost) |
557 | | - - :project-dir (optional) - Root directory for the project |
| 568 | + - :port (required if no :project-dir) - nREPL server port |
| 569 | + - :host (optional) - nREPL server host (defaults to localhost) |
| 570 | + - :project-dir (optional) - Root directory for the project. If provided, port is optional. |
558 | 571 | - :start-nrepl-cmd (optional) - Command to start nREPL server |
559 | | - |
| 572 | +
|
560 | 573 | - component-factories: Map with factory functions |
561 | 574 | - :make-tools-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of tools |
562 | | - - :make-prompts-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of prompts |
| 575 | + - :make-prompts-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of prompts |
563 | 576 | - :make-resources-fn - (fn [nrepl-client-atom working-dir] ...) returns seq of resources |
564 | | - |
| 577 | +
|
565 | 578 | Auto-start conditions (must satisfy ONE): |
566 | 579 | 1. Both :start-nrepl-cmd AND :project-dir provided in nrepl-args |
567 | 580 | 2. Current directory contains .clojure-mcp/config.edn with :start-nrepl-cmd |
568 | | - |
| 581 | +
|
569 | 582 | Returns: nil" |
570 | 583 | [nrepl-args component-factories] |
571 | 584 | (-> nrepl-args |
572 | 585 | validate-options |
573 | 586 | nrepl-launcher/maybe-start-nrepl-process |
574 | | - ensure-port |
| 587 | + ensure-port-if-needed |
575 | 588 | (build-and-start-mcp-server-impl component-factories))) |
0 commit comments