|
1 | 1 | (ns eca.features.tools.mcp |
2 | 2 | (:require |
| 3 | + [cheshire.core :as json] |
| 4 | + [cheshire.factory :as json.factory] |
| 5 | + [clojure.core.memoize :as memoize] |
3 | 6 | [clojure.java.browse :as browse] |
| 7 | + [clojure.java.io :as io] |
4 | 8 | [clojure.string :as string] |
5 | 9 | [eca.config :as config] |
6 | 10 | [eca.db :as db] |
|
421 | 425 | (on-server-updated (->server name server-config :failed @db*)))}) |
422 | 426 | (browse/browse-url authorization-endpoint))))) |
423 | 427 |
|
| 428 | +(defn ^:private restart-server! |
| 429 | + "Stop the server if running, then spawn a daemon thread to re-initialize it." |
| 430 | + [name db* config metrics on-server-updated] |
| 431 | + (when (get-in @db* [:mcp-clients name :client]) |
| 432 | + (stop-server! name db* config {:on-server-updated on-server-updated})) |
| 433 | + (let [t (Thread. |
| 434 | + (fn [] |
| 435 | + (try |
| 436 | + (initialize-server! name db* config metrics on-server-updated) |
| 437 | + (finally |
| 438 | + (deregister-init-thread! name)))))] |
| 439 | + (.setName t (str "mcp-init-" name)) |
| 440 | + (.setDaemon t true) |
| 441 | + (register-init-thread! name t) |
| 442 | + (.start t))) |
| 443 | + |
424 | 444 | (defn logout-server! |
425 | 445 | "Logout from an MCP server by clearing stored OAuth credentials and restarting it." |
426 | 446 | [name db* config metrics {:keys [on-server-updated]}] |
427 | 447 | (when (get-in config [:mcpServers name]) |
428 | 448 | (swap! db* update :mcp-auth dissoc name) |
429 | 449 | (db/update-global-cache! @db* metrics) |
430 | | - (when (get-in @db* [:mcp-clients name :client]) |
431 | | - (stop-server! name db* config {:on-server-updated on-server-updated})) |
432 | | - (let [t (Thread. |
433 | | - (fn [] |
434 | | - (try |
435 | | - (initialize-server! name db* config metrics on-server-updated) |
436 | | - (finally |
437 | | - (deregister-init-thread! name)))))] |
438 | | - (.setName t (str "mcp-init-" name)) |
439 | | - (.setDaemon t true) |
440 | | - (register-init-thread! name t) |
441 | | - (.start t)))) |
| 450 | + (restart-server! name db* config metrics on-server-updated))) |
| 451 | + |
| 452 | +(defn ^:private parse-json-with-comments [^String s] |
| 453 | + (binding [json.factory/*json-factory* (json.factory/make-json-factory {:allow-comments true})] |
| 454 | + (json/parse-string s))) |
| 455 | + |
| 456 | +(defn ^:private find-server-config-source |
| 457 | + "Returns {:source :local :workspace-root-uri uri} or {:source :global} |
| 458 | + indicating where the MCP server `server-name` is defined. |
| 459 | + Checks local workspace configs first (highest priority), then global." |
| 460 | + [server-name db] |
| 461 | + (let [roots (:workspace-folders db)] |
| 462 | + (or (some (fn [{:keys [uri]}] |
| 463 | + (let [config-file (io/file (shared/uri->filename uri) ".eca" "config.json")] |
| 464 | + (when (.exists ^java.io.File config-file) |
| 465 | + (let [local-config (parse-json-with-comments (slurp config-file))] |
| 466 | + (when (get-in local-config ["mcpServers" server-name]) |
| 467 | + {:source :local :workspace-root-uri uri}))))) |
| 468 | + roots) |
| 469 | + (let [global-file (config/global-config-file)] |
| 470 | + (when (.exists global-file) |
| 471 | + (let [global-config (parse-json-with-comments (slurp global-file))] |
| 472 | + (when (get-in global-config ["mcpServers" server-name]) |
| 473 | + {:source :global})))) |
| 474 | + {:source :global}))) |
| 475 | + |
| 476 | +(defn ^:private replace-server-in-config-file! |
| 477 | + "Replace a single MCP server entry in a JSON config file using assoc-in |
| 478 | + instead of deep-merge, so old keys (e.g. :command when switching to :url) |
| 479 | + are removed. Note: comments in the original file are stripped since JSON |
| 480 | + output cannot preserve them." |
| 481 | + [^java.io.File config-file server-name new-server-config] |
| 482 | + (let [raw (when (.exists config-file) |
| 483 | + (parse-json-with-comments (slurp config-file))) |
| 484 | + updated (assoc-in (or raw {}) ["mcpServers" server-name] |
| 485 | + (json/parse-string (json/generate-string new-server-config)))] |
| 486 | + (io/make-parents config-file) |
| 487 | + (spit config-file (json/generate-string updated {:pretty true})))) |
| 488 | + |
| 489 | +(defn update-server! |
| 490 | + "Update an MCP server's connection config (command/args/url), persist to the |
| 491 | + correct config file (local or global), clear the config cache, then restart." |
| 492 | + [server-name server-fields db* config metrics {:keys [on-server-updated]}] |
| 493 | + (let [db @db* |
| 494 | + {:keys [source workspace-root-uri]} (find-server-config-source server-name db) |
| 495 | + current-server-config (get-in config [:mcpServers server-name]) |
| 496 | + ;; Build clean server entry: preserve env/disabled/headers, replace connection fields |
| 497 | + preserved-keys (select-keys current-server-config [:env :disabled :headers]) |
| 498 | + new-server-config (merge preserved-keys server-fields) |
| 499 | + config-file (if (= source :local) |
| 500 | + (io/file (shared/uri->filename workspace-root-uri) ".eca" "config.json") |
| 501 | + (config/global-config-file))] |
| 502 | + (replace-server-in-config-file! config-file server-name new-server-config) |
| 503 | + (memoize/memo-clear! config/all) |
| 504 | + (let [fresh-config (config/all @db*)] |
| 505 | + (restart-server! server-name db* fresh-config metrics on-server-updated)))) |
442 | 506 |
|
443 | 507 | (defn all-tools [db] |
444 | 508 | (into [] |
|
474 | 538 | nil)} |
475 | 539 | call-future (future (pmc/call-tool mcp-client name arguments call-opts)) |
476 | 540 | result (if needs-reinit?* |
477 | | - (loop [elapsed 0] |
| 541 | + (loop [elapsed (long 0)] |
478 | 542 | (cond |
479 | 543 | (realized? call-future) |
480 | 544 | (deref call-future) |
481 | 545 |
|
482 | 546 | @needs-reinit?* |
483 | 547 | (do (future-cancel call-future) nil) |
484 | 548 |
|
485 | | - (>= elapsed tool-call-timeout-ms) |
| 549 | + (>= elapsed (long tool-call-timeout-ms)) |
486 | 550 | (do (future-cancel call-future) nil) |
487 | 551 |
|
488 | 552 | :else |
489 | | - (do (Thread/sleep reinit-poll-interval-ms) |
490 | | - (recur (+ elapsed reinit-poll-interval-ms))))) |
| 553 | + (do (Thread/sleep (long reinit-poll-interval-ms)) |
| 554 | + (recur (+ elapsed (long reinit-poll-interval-ms)))))) |
491 | 555 | (deref call-future))] |
492 | 556 | (if result |
493 | 557 | {:error (:isError result) |
|
0 commit comments