Skip to content

Commit 6703b3b

Browse files
committed
Add mcp/updateServer to support change mcp commands/urls from client UI.
1 parent 76be3a6 commit 6703b3b

7 files changed

Lines changed: 156 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- Delete chats older than 7 days on server startup.
66
- Use human-readable workspace cache directory names (e.g. `my-project_a1b2c3d4`), with automatic migration from old hash-only format.
7+
- Show MCP server URL in the details page and allow editing command/args/url inline with `mcp/updateServer` endpoint.
8+
- Add `mcp/updateServer` to support change mcp commands/urls from client UI.
79

810
## 0.113.1
911

docs/protocol.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2138,9 +2138,39 @@ interface MCPLogoutServerParams {
21382138
}
21392139
```
21402140

2141-
### Add MCP (↩️)
2141+
### Update MCP Server (↩️)
21422142

2143-
Soon
2143+
Updates an MCP server's connection configuration (command/args or url), persists the change to the appropriate config file (local or global), and restarts the server.
2144+
2145+
_Request:_
2146+
2147+
* method: `mcp/updateServer`
2148+
* params: `MCPUpdateServerParams` defined as follows:
2149+
2150+
```typescript
2151+
interface MCPUpdateServerParams {
2152+
/**
2153+
* The MCP server name.
2154+
*/
2155+
name: string;
2156+
/**
2157+
* The command to run (for stdio servers).
2158+
*/
2159+
command?: string;
2160+
/**
2161+
* The command arguments (for stdio servers).
2162+
*/
2163+
args?: string[];
2164+
/**
2165+
* The URL (for remote/HTTP servers).
2166+
*/
2167+
url?: string;
2168+
}
2169+
```
2170+
2171+
_Response:_
2172+
2173+
* result: `{}`
21442174

21452175
## General features
21462176

src/eca/config.clj

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@
171171
:outputTruncation {:lines 2000 :sizeKb 50}}
172172
:variantsByModel {".*sonnet[-._]4[-._]6|opus[-._]4[-._][56]" {:variants anthropic-variants}
173173
".*gpt[-._]5(?:[-._](?:2|4)(?!\\d)|[-._]3[-._]codex)" {:variants openai-variants
174-
:excludeProviders ["github-copilot"]}}
174+
:excludeProviders ["github-copilot"]}}
175175
:mcpTimeoutSeconds 60
176176
:lspTimeoutSeconds 30
177177
:mcpServers {}
@@ -507,14 +507,14 @@
507507
(merge-config $ plugin-config))
508508
;; Append plugin commands/rules (vector concat, not deep-merge replace)
509509
(cond->
510-
(seq plugin-commands) (update :commands #(vec (concat % plugin-commands)))
511-
(seq plugin-rules) (update :rules #(vec (concat % plugin-rules))))
510+
(seq plugin-commands) (update :commands #(vec (concat % plugin-commands)))
511+
(seq plugin-rules) (update :rules #(vec (concat % plugin-rules))))
512512
migrate-legacy-config
513513
;; Merge markdown-defined agents (lowest priority — JSON config agents win)
514514
;; Plugin agents merge at same level as markdown agents
515515
(as-> config
516516
(let [md-agent-configs (when-not pure-config?
517-
(agents/all-md-agents (:workspace-folders db)))]
517+
(agents/all-md-agents (:workspace-folders db)))]
518518
(if (or (seq md-agent-configs) (seq plugin-agents))
519519
(update config :agent (fn [existing]
520520
(merge md-agent-configs plugin-agents existing)))
@@ -622,3 +622,18 @@
622622
new-config-json (json/generate-string new-config {:pretty true})]
623623
(io/make-parents global-config-file)
624624
(spit global-config-file new-config-json)))
625+
626+
(defn update-local-config!
627+
"Deep-merges `config` into the local `.eca/config.json` for `workspace-root-uri`."
628+
[workspace-root-uri config]
629+
(let [config-dir (io/file (shared/uri->filename workspace-root-uri) ".eca")
630+
config-file (io/file config-dir "config.json")
631+
current-config (when (.exists config-file)
632+
(normalize-fields normalization-rules
633+
(safe-read-json-string (slurp config-file) (var *local-config-error*))))
634+
new-config (deep-merge (or current-config {})
635+
(normalize-fields normalization-rules config))
636+
new-config (assoc new-config "$schema" config-schema-url)
637+
new-config-json (json/generate-string new-config {:pretty true})]
638+
(io/make-parents config-file)
639+
(spit config-file new-config-json)))

src/eca/features/tools.clj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,16 @@
310310
metrics
311311
{:on-server-updated (partial notify-server-updated metrics messenger tool-status-fn)})))
312312

313+
(defn update-server! [name server-fields db* messenger config metrics]
314+
(let [tool-status-fn (make-tool-status-fn config nil)]
315+
(f.mcp/update-server!
316+
name
317+
server-fields
318+
db*
319+
config
320+
metrics
321+
{:on-server-updated (partial notify-server-updated metrics messenger tool-status-fn)})))
322+
313323
(defn tool-call-summary [all-tools full-name args config db]
314324
(when-let [summary-fn (:summary-fn (first (filter #(= full-name (:full-name %))
315325
all-tools)))]

src/eca/features/tools/mcp.clj

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
(ns eca.features.tools.mcp
22
(:require
3+
[cheshire.core :as json]
4+
[cheshire.factory :as json.factory]
5+
[clojure.core.memoize :as memoize]
36
[clojure.java.browse :as browse]
7+
[clojure.java.io :as io]
48
[clojure.string :as string]
59
[eca.config :as config]
610
[eca.db :as db]
@@ -421,24 +425,84 @@
421425
(on-server-updated (->server name server-config :failed @db*)))})
422426
(browse/browse-url authorization-endpoint)))))
423427

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+
424444
(defn logout-server!
425445
"Logout from an MCP server by clearing stored OAuth credentials and restarting it."
426446
[name db* config metrics {:keys [on-server-updated]}]
427447
(when (get-in config [:mcpServers name])
428448
(swap! db* update :mcp-auth dissoc name)
429449
(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))))
442506

443507
(defn all-tools [db]
444508
(into []
@@ -474,20 +538,20 @@
474538
nil)}
475539
call-future (future (pmc/call-tool mcp-client name arguments call-opts))
476540
result (if needs-reinit?*
477-
(loop [elapsed 0]
541+
(loop [elapsed (long 0)]
478542
(cond
479543
(realized? call-future)
480544
(deref call-future)
481545

482546
@needs-reinit?*
483547
(do (future-cancel call-future) nil)
484548

485-
(>= elapsed tool-call-timeout-ms)
549+
(>= elapsed (long tool-call-timeout-ms))
486550
(do (future-cancel call-future) nil)
487551

488552
: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))))))
491555
(deref call-future))]
492556
(if result
493557
{:error (:isError result)

src/eca/handlers.clj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,16 @@
212212
(metrics/task metrics :eca/mcp-logout-server
213213
(f.tools/logout-server! (:name params) db* messenger config metrics)))
214214

215+
(defn mcp-update-server [{:keys [db* messenger metrics config]} params]
216+
(metrics/task metrics :eca/mcp-update-server
217+
(let [server-name (:name params)
218+
server-fields (cond-> {}
219+
(:command params) (assoc :command (:command params))
220+
(:args params) (assoc :args (:args params))
221+
(:url params) (assoc :url (:url params)))]
222+
(f.tools/update-server! server-name server-fields db* messenger config metrics)
223+
{})))
224+
215225
(defn ^:private update-agent-model-and-variants!
216226
"Updates the selected model and variants based on agent configuration."
217227
[agent-config config messenger db*]

src/eca/server.clj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@
8989
(defmethod jsonrpc.server/receive-notification "mcp/logoutServer" [_ components params]
9090
(handlers/mcp-logout-server (with-config components) params))
9191

92+
(defmethod jsonrpc.server/receive-request "mcp/updateServer" [_ components params]
93+
(handlers/mcp-update-server (with-config components) params))
94+
9295
(defmethod jsonrpc.server/receive-notification "chat/selectedAgentChanged" [_ components params]
9396
(handlers/chat-selected-agent-changed (with-config components) params))
9497

0 commit comments

Comments
 (0)