MCP Server in Babashka/Clojure
The year went WHOOSH
Things in the AI world are moving at an incredible pace and it's hard to catch up. I've been a late bloomer and have been working my way through catching up. The landscape is vast and there's so much thrown under the umbrella of AI that it becomes a blur.
Once one has mastered, or at least learned how to help yourself with understanding models and being smarter about prompts, you'll find that things moved on to tools and agents and MCP, or Model Context Protocol. Diving in and swimming around for long enough, it starts to look like something that's been around before... RPC, SOAP, REST and the next, and the next. Communications change and evolve all the time. The battle for the ideal AI protocol is but just the next.
In my exploration I've stumbled upon an interesting article that confirmed my observations. Is MCP the New REST or the Next Betamax? In the article it highlights the current developments between MCP, A2A and other developments.
Protocols
It basically boils down to JSON-RPC 2.0 as the communication standard for both, but they may have slight differences and/or intentions.
My journey had my path crossing with the following good leads:
- https://mcpcat.io/guides/understanding-json-rpc-protocol-mcp/ The first real useful summary of MCP that I found without hiding everything behind one of the TypeScript or Python SDKs for MCP. I don't want to watch the magic show, I want to understand the sleight of hand!
 - https://www.a2aprotocol.org/en/docs/json-rpc-2-0 Following the MCPcat article, this was the first solid reference to specifications that I laid eyes on. MPC's spec evaded me for a while...
 - https://modelcontextprotocol.io/specification/2025-06-18 And then finally I tripped over the MCP specification!
 
Tooling
The Continue extension for VS Code is my preferred way to interact with local LLMs. It supports agents, but when I went down the MCP path, I only used Chat features to accomplish what I needed. The next bit of evolution covered Agents and Tools, because Agents are the mediators between LLMs and Tools, and it in turn uses MCP to do the mediation.
Continue and the Agent is effectively an MCP Client, and each Tool is an MCP Server.
It's good to understand that, but it doesn't quite connect the dots on who does
what or what the message exchange looks like. This is where @modelcontextprotocol/inspector
enters the scene. I realise now that @modelcontextprotocol/inspector and MCPcat
are similar, but MCPcat is a service that allows greater insight. Inspector is
good for simple development, and free.
Example: BB MCP HTTP
This is an example MCP Server written in Clojure/Babashka (Babashka because it has batteries included).
(ns my-mcp
  "Playground to figure out MCP at grassroots-level."
  (:require
   [cheshire.core :as json]
   [clojure.java.io :as io]
   [org.httpkit.server :as httpd])
  (:import
   [java.io PushbackInputStream]
   [java.time Instant]))
(def log-file "info.log")
(defn log [data & {:keys [level] :or {level :info}}] ,,,)
(defn
  ;; TODO: The metadata could likely be inferred from the meta, but later.
  ^{:mcp-tool? true
    :description "Calculate compound interest over time."
    :inputSchema
    {:type :object
     :properties
     {:currency {:type :string :description "The currency identifier, such as $ for USD or R for ZAR"}
      :principal {:type :number :minimum 1 :description "The initial value to start with."}
      :rate {:type :double :minimum 0, :maximum 1.0 :description "The interest rate."}
      :years {:type :integer :minimum 1 :description "How many years the principal value will accrue."}}
     :required
     [:currency :principal :rate :years]}}
  calculate-compound-interest
  [_name {:keys [principal rate years currency] :as _arguments}]
  (let [final-amount (* principal (Math/pow (+ 1 (/ rate 1)) (* years 1)))]
    {:content [{:type "text" :text (format "Final amount: %s %2f" currency final-amount)}]
     :isError false}))
(defn tool-list
  "Trawls 'my-cp for defined tools and prepares a response shape."
  []
  {:tools
   (->> (ns-interns 'my-mcp)
        (filter (comp :mcp-tool? meta second))
        (map (fn [[k v]]
               (-> (meta v)
                   (select-keys [:description :inputSchema])
                   (assoc :name (name k)))))
        (vec))})
(defn decode-body
  "Decodes the body input stream to Clojure data."
  [{:keys [body] :as req}]
  (json/parse-stream (io/reader (PushbackInputStream. body)) keyword))
(defn handler [{:keys [request-method headers] :as req}]
  (condp = request-method
    :options
    {:status 204
     :headers
     {"Access-Control-Allow-Origin" (get headers "origin")
      "Access-Control-Allow-Methods" "POST, OPTIONS"
      "Access-Control-Allow-Headers" "Content-Type,mcp-protocol-version"}}
    
    :post
    (let [{:keys [method id] :as msg} (decode-body req)]
      (log msg)
      {:status 200
       :headers
       {"content-type" "application/json"
        "Access-Control-Allow-Origin" (get headers "origin")}
       :body
       (json/encode
        {:jsonrpc "2.0"
         :id id
         :result
         (condp = method
           "initialize"
           {:protocolVersion "2025-03-26"
            :capabilities {:tools {:listChanged true}} ;ServerCapabilities
            :serverInfo {:name "BB MCP HTTP" :version "0.0.1-dev"} ;Implementation
            :instructions "Be very very careful."}
           "ping"
           {}
           "notifications/initialized"
           {}
           "tools/list"
           (let [resp (tool-list)]
             (log {:tools/list-response resp})
             resp)
           "tools/call"
           (let [{{:keys [name arguments]} :params} msg
                 tool-key (symbol name)
                 tool (tool-key (ns-interns 'my-mcp))
                 response
                 (cond tool (apply tool [name arguments])
                       :else
                       {:content [{:type "text" :text "Unknown tool"}]
                        :isError true})]
             (log {:tools/call-response response})
             response))})})))
#_(def stop (httpd/run-server #'handler {:port 8083}))
(defn serve [{:keys [port] :or {port 8083} :as opts}]
  (let [stop (httpd/run-server #'handler {:port port})]
    (spit log-file "started" :append true)
    (log "started")
    @(promise)))
Running inspector
To see it working in the simplest form, without setting up Continue and models,
inspector is a good tool to just test simple functionality.
It can be started by running the following in a terminal:
npx @modelcontextprotocol/inspector:latest
Running the server
- Start a REPL and evaluate the file, then the line 
#_(def stop (httpd/run-server #'handler {:port 8083}))- to stop the server from the REPL, evaluate 
(stop) 
 - to stop the server from the REPL, evaluate 
 - Or, from the cli, run 
bb serve 
Results
Starting up BB MCP HTTP:
 {:timestamp "2025-10-24T11:43:28.272972504Z",
  :level :info,
  :message "loaded namespace: my-mcp"}
 {:timestamp "2025-10-24T11:43:28.274874840Z",
  :level :info,
  :message "started"}
Connecting @modelcontextprotocol/inspector:
 {:timestamp "2025-10-24T11:43:42.295680351Z",
  :level :info,
  :method "initialize",
  :params
  {:protocolVersion "2025-06-18",
   :capabilities
   {:sampling {}, :elicitation {}, :roots {:listChanged true}},
   :clientInfo {:name "inspector-client", :version "0.17.2"}},
  :jsonrpc "2.0",
  :id 0}
 {:timestamp "2025-10-24T11:43:42.305549309Z",
  :level :info,
  :method "notifications/initialized",
  :jsonrpc "2.0"}
Listing the tools:
 {:timestamp "2025-10-24T11:44:06.350057953Z",
  :level :info,
  :method "tools/list",
  :params {:_meta {:progressToken 1}},
  :jsonrpc "2.0",
  :id 1}
 {:timestamp "2025-10-24T11:44:06.350513277Z",
  :level :info,
  :tools/list-response
  {:tools
   [{:description "Calculate compound interest over time.",
     :inputSchema
     {:type :object,
      :properties
      {:currency
       {:type :string,
        :description
        "The currency identifier, such as $ for USD or R for ZAR"},
       :principal
       {:type :number,
        :minimum 1,
        :description "The initial value to start with."},
       :rate
       {:type :double,
        :minimum 0,
        :maximum 1.0,
        :description "The interest rate."},
       :years
       {:type :integer,
        :minimum 1,
        :description
        "How many years the principal value will accrue."}},
      :required [:currency :principal :rate :years]},
     :name "calculate-compound-interest"}]}}
Calling the calculate-compound-interest tool:
 {:timestamp "2025-10-24T11:44:25.042188479Z",
  :level :info,
  :method "tools/call",
  :params
  {:name "calculate-compound-interest",
   :arguments
   {:currency "ZAR", :principal 10000, :rate 0.0975, :years 5},
   :_meta {:progressToken 2}},
  :jsonrpc "2.0",
  :id 2}
 {:timestamp "2025-10-24T11:44:25.043238811Z",
  :level :info,
  :tools/call-response
  {:content [{:type "text", :text "Final amount: ZAR 15922.917487"}],
   :isError false}}