Contents

Building a Zero-Dependency MCP Client & Server Natively in Coni

The Model Context Protocol (MCP) is rapidly becoming the gold standard for connecting Large Language Models to external tools and data sources. Historically, if you wanted to build an MCP client or server, you needed to reach for heavy, bloated SDKs in Node.js or Python, dealing with complex asynchronous state and dependency hell.

We thought: What if we could do it entirely natively in Coni?

In this post, we’re going to walk through how we brought a complete, zero-dependency MCP Client and Server directly into the Coni standard library (mcp.coni). It’s fast, highly concurrent, and ridiculously easy to use.

Let’s dive right into how you can use it in your own projects!


Building the MCP Server

Building an MCP Server in Coni is incredibly straightforward. You don’t need to manually define tedious JSON Schemas for your tools. Thanks to Coni’s native closure introspection, the server automatically reads your function names, docstrings, and parameters to expose them to clients.

Here is a complete example of exposing local functions as an MCP Server:

(require "libs/mcp/src/mcp.coni" :as mcp)

;; 1. Define your standard Coni functions. 
;; The docstring and parameters are automatically exposed as the JSON Schema!
(defn get-weather "Returns the weather for a given city" [city]
  (str "It is sunny and 75F in " city "!"))

(defn calculate-sum "Calculates the sum of two numbers" [a b]
  (+ (num a) (num b)))

;; 2. Start the MCP Server on port 3005 and pass in the functions.
(mcp/serve 3005 [get-weather calculate-sum])

;; 3. Keep the script alive
(let [c (chan)] (<! c))

That’s it! In exactly four lines of code, you have a fully compliant MCP Server running over Server-Sent Events (SSE). It handles the handshakes, multiplexes the JSON-RPC endpoints, and executes your Coni code natively whenever a client requests it.


Connecting with the MCP Client

Now that we have a server running, let’s connect to it using the native Coni MCP Client. The client handles all the messy SSE streams asynchronously behind the scenes using Coni’s native chan (channels) and spawn (goroutines) primitives.

Here’s how you connect and seamlessly wrap the remote tools into local callable functions:

(require "libs/mcp/src/mcp.coni" :as mcp)

;; 1. Connect to the SSE endpoint
(println "Connecting to Coni MCP server...")
(def client (mcp/connect-sse "http://localhost:3005/sse"))

;; 2. Fetch all available tools and wrap them in local closures
(def tools (mcp/as-tools client))

(println "Found" (count tools) "tools from the server!")
(doseq [t tools]
  (println " - " (:name t) ":" (:description t) "Args:" (:args t)))

;; 3. Execute the remote tool just like a local function!
(let [weather-tool (first tools)
      f (:fn weather-tool)]
  (println "Tool result:" (f "Tokyo")))

When you run this, the client negotiates the protocol, fetches the tools (get-weather and calculate-sum), and natively executes (f "Tokyo") over JSON-RPC. The server maps "Tokyo" to the [city] parameter, executes the function, and streams the result back!


Unstoppable Swarms with Agent Studio

The absolute best part about (mcp/as-tools client) is that it outputs the exact schema structure required by Coni’s built-in AI agents.

This means you can instantly give your LLM swarms access to any MCP server in the world just by passing the tools array into make-agent.

(require "libs/mcp/src/mcp.coni" :as mcp)

;; Connect to any external MCP server (e.g. a database interface)
(def db-client (mcp/connect-sse "http://localhost:3005/sse"))
(def db-tools (mcp/as-tools db-client))

;; Give your Agent access to the external world!
(def assistant 
  (make-agent {:model "gpt-4o"
               :system "You are a helpful assistant with database access."
               :tools db-tools}))

;; The LLM can now autonomously decide to trigger the MCP tools!
(assistant "What is the weather in Tokyo?")

The Result

Coni now ships with a fully functional, highly concurrent, and ridiculously ergonomic MCP implementation. It bridges the gap between local functions and remote execution, turning Agent Studio into an unstoppable swarm capable of integrating with the entire MCP ecosystem.

No heavy SDKs. No dependency hell. Just pure, native Coni.