-
-
Notifications
You must be signed in to change notification settings - Fork 40
Description
This issue it to request integration with plumcp for MCP functionality. Below are my notes w.r.t. the existing implementation in eca.features.tools.mcp ns:
Integrating plumcp
Add this dependency in deps.edn (because ECA already uses Cheshire):
io.github.plumce/plumcp.core-json-cheshire {:mvn/version "0.2.0-alpha1"}
Assume the following namespaces required:
[plumcp.core.api.capability-support :as pcs]
[plumcp.core.api.entity-support :as pes]
[plumcp.core.api.mcp-client :as pmc]
[plumcp.core.client.http-client-transport :as phct]
[plumcp.core.client.stdio-client-transport :as psct]
[plumcp.core.client.http-client-transport-auth :as phcta]
[plumcp.core.support.http-client :as phc]
The sections below suggest how to refactor existing code in eca.features.tools.mcp ns 1-to-1 to use
Plumcp, avoiding unnecessary changes.
Fn: ->transport
No support for SSE (deprecated)
Plumcp does not support the SSE transport, which was deprecated in the 2025-03-26 spec.
Streamable HTTP transport
-
The purpose of
customizeris to modify the outgoing HTTP request. The
equivalent in plumcp is a request-middleware, defined as follows:rm (fn [request] ; request middleware applied before request goes out (-> request (update :headers merge config-headers) (update :headers merge (if-let [access-token (get-in db [:mcp-auth server-name :access-token])] {"Authorization" (str "Bearer " access-token)} {})))) hc (phc/make-http-client url {:request-middleware rm})
Here we define a request middleware, and an HTTP Client.
-
Finally, create and return a client Streamable HTTP transport:
(phct/make-streamable-http-transport hc)
STDIO transport
The client STDIO transport is created by spawning a process (i.e. run a
command):
(psct/run-command
{:command-tokens (->> (or args [])
(mapv replace-env-vars)
(cons command))
:dir work-dir
:env (-> env
(update-keys name))
:on-stderr-text (fn [msg]
(->> (format "[%s] %s" server-name msg)
(logger/info logger-tag)))})Fn: ->client
The plumcp client construction is more explicit:
(let [make-root-item (fn [ws]
(pcs/make-root-item (:uri ws)
{:name (:name ws)}))
tools-consumer (fn [tools]
(logger/info
logger-tag
(-> "[%s] Tools list changed, received %d tools"
(format name (count tools))))
(on-tools-change tools))
tools-nhandler (fn [jsonrpc-notification]
(->> {:on-tools tools-consumer}
(pmc/fetch-tools jsonrpc-notification)))
client (pmc/make-client
{:info (pes/make-info name "current")
:client-transport transport
:primitives {:roots (mapv make-root-item workspaces)}
:notification-handlers {pmc/on-tools-list-changed
tools-nhandler}
:print-banner? false})]
;; handshake: initialize the client and notify
(->> {:timeout-millis (* 1000 init-timeout)}
(pmc/initialize-and-notify! client))
client)Here the handshake (initialize+notify) is done when creating the client.
You may want to do it together or later. To disconnect the client later:
(pmc/disconnect! client)Fn: ->content
Assumption: Argument content-client implies TextContent in the MCP spec.
Straight-forward translation:
(case (:type content-client)
"text" {:type :text
:text (:text content-client)}
nil)Fn: ->resource-content
Assumption: Argument resource-content-client is TextResourceContents in
MCP spec. Again, straight-forward translation:
{:type :text
:uri (:uri resource-content-client)
:text (:text resource-content-client)}Fn: tool-client->tool
May not be required with plumcp, because plumcp already returns this data
in the list-tools call.
Function: list-server-tools
Get a vector of tools declaration:
(when (-> (pmc/get-initialize-result client)
(get-in [:capabilities :tools]))
(->> {:on-error (fn [jsonrpc-error-response]
(->> [:error :message]
(get-in jsonrpc-error-response)
(logger/warn logger-tag "Could not list tools:"))
[])}
(pmc/list-tools client)))Fn: list-server-prompts
It is a copy-paste of list-server-tools with tools replaced with
prompts. Perhaps the :on-error fn may be extracted out as a common fn
with arity [jsonrpc-error-response].
Fn: list-server-resources
It may be yet again a copy paste of list-server-tools. However, be aware
that in plumcp:
list-resourcesandlist-resource-templatesare distinct functions- Both fns share same capability, so check
[:capabilities :resources]
Fns: initialize-mcp-oauth, token-expired?, try-refresh-token!, initialize-server!
Plumcp already implements the client-side OAuth PKCE flow, which includes:
- starting a local HTTP server for callback
- opening authorization URL in browser
- stopping the server after verying callback result
So, only some of the existing functionality in initialize-mcp-oauth may
be required. The client OAuth integration is disabled by default in Plumcp.
To enable client OAuth you should pass :auth-options when making Streaming
HTTP transport.
Make auth-options:
plumcp.core.client.http-client-transport-auth/make-client-auth-options
Make client Streamable HTTP transport
plumcp.core.client.http-client-transport/make-streamable-http-transport
To use custom OAuth handling, you need to pass the options below:
{:auth-enabled? true
:get-auth-tokens (fn [headers-lower-map auth-options]
...)}Example
If you check the plumcp.core.main.client ns in the Plumcp repo under
src/main path, you would find this:
(ns plumcp.core.main.client
(:require
[plumcp.core.api.entity-support :as es]
[plumcp.core.client.http-client-transport :as hct]
[plumcp.core.client.http-client-transport-auth :as hcta]
[plumcp.core.dev.api :as dev]
[plumcp.core.support.http-client :as hc]
[plumcp.core.support.http-server :as hs]
[plumcp.core.support.traffic-logger :as stl]
[plumcp.core.util :as u]))
(def client-options (-> {:info (es/make-info "Test client" "0.1.0"
"Test client v0.1.0")}
(merge dev/client-options)))
(defn make-http-transport
[endpoint-uri & {:keys [traffic-logger
auth-options]
:or {traffic-logger stl/compact-client-traffic-logger
auth-options {}}
:as options}]
(let [http-client (->> {:traffic-logger traffic-logger}
(hc/make-http-client endpoint-uri))
auth-options (-> {:http-client http-client
:on-error u/eprintln
:redirect-uris ["http://localhost:6277/"]
:mcp-server "http://localhost:3000"
:callback-redirect-uri "http://localhost:6277/"
:callback-start-server #(hs/run-http-server %
{:port 6277})
;:token-cache hcta/local-token-cache
;;
}
;; for :client-name
(u/copy-keys client-options [:info])
(merge auth-options)
hcta/make-client-auth-options)]
(->> {:auth-options auth-options}
(hct/make-streamable-http-transport http-client))))You should leave the plumcp.core.dev.api usage out, because it is not in
plumcp.core module. Rest of the config applies. By default, it uses an
in-memory token cache keyed by the auth server endpoint, so it can take in
multiple auth servers with ease. To cache the tokens on disk (consider the
SECURITY RISK) there is hcta/local-token-cache - this may survive ECA
restarts (better UX) but again, consider the security risk.
Fn: stop-server!
The line
(.closeGracefully ^McpSyncClient client)needs to become
(pmc/disconnect! client)Fn: call-tool!
Binding result to the call-tool result and processing that for return
value may be re-written as:
(locking mcp-client
(->> {:on-result (fn [result]
{:error (:isError result)
:contents (:content result)})
:on-error (fn [jsonrpc-error-response]
(->> [:error :message]
(get-in jsonrpc-error-response)
(logger/warn logger-tag "Error calling tool:"))
{:error true
:contents nil})}
(pmc/call-tool mcp-client name arguments)))Here :on-error is optional to keep parity with the existing version.
Fn: get-prompt!
Binding prompt to the get-prompt result and processing that for return
value may be re-written as:
(->> {:on-result (fn [prompt]
{:description (:description prompt)
:messages (mapv (fn [each-message]
{:role (string/lower-case
(:role each-message))
:content [(-> each-message
:content
->content)]})
(:messages prompt))})
:on-error (fn [jsonrpc-error-response]
(->> [:error :message]
(get-in jsonrpc-error-response)
(logger/warn logger-tag "Error getting prompt:")))}
(pmc/get-prompt mcp-client name arguments))Fn: get-resource!
Binding resource to the read-resource result and processing that for return
value may be re-written as:
(->> {:on-result (fn [resource]
{:contents (->> (:contents resource)
(mapv ->resource-content))})
:on-error (fn [jsonrpc-error-response]
(->> [:error :message]
(get-in jsonrpc-error-response)
(logger/warn logger-tag
"Error reading resource:")))}
(pmc/read-resource mcp-client uri))Metadata
Metadata
Assignees
Labels
Type
Projects
Status