mcp-tkx is a Clojure / ClojureScript library for building MCP (Model Context Protocol) clients and servers. It is a fork of Metosin's mcp-toolkit with extended protocol support and a kebab-case-first developer experience.
What's distinctive about this fork:
2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25 negotiate automatically at the handshake. 2026-07-28 removed the handshake, so a session opts into it explicitly.:max-tokens / :input-schema / :list-changed; the wire format (maxTokens / inputSchema / listChanged) is produced and consumed at the transport layer via camel-snake-kebab. No bespoke per-field renaming inside handlers.mcp-toolkit.schema ships 31 schemas for protocol types (icons, sampling tools, elicitation requests, tasks, content blocks). valid? / validate / explain for sanity-checking your tool definitions and request shapes before you ship them.:read-fn: resources can compute :text / :blob / :contents on demand at resources/read time, returning a plain map or a Promesa promise.jsonista / nREPL or on Node.js via shadow-cljs with no transport-specific changes to your handler code.:send-message fn and feeding decoded messages into json-rpc/handle-message.add-tool / remove-tool / add-resource / notify-resource-updated mutate the running server's session and notify clients live, so you can iterate on tools while Claude Desktop / Claude Code is connected.This guide focuses on the reusable building blocks, the patterns that lift to other MCP servers and to other Clojure projects. For day-to-day API reference, see the docs/reference/ folder. Most of it (api-design.md, session.md, context.md, using-the-library.md, repl-story.md, and MIGRATION-2025-06-18.md) is preserved from the upstream Metosin docs. Two files are this fork's own work: introduction.md and MIGRATION-2025-11-25.md, both covering the 2025-11-25 protocol revision.
New to MCP or to this library? Start with Getting started for install, your first STDIO server, and a smoke test against the MCP Inspector. Then read Architecture for the session-atom + context-hashmap split, the message lifecycle, and the namespace map.
Trying to understand the kebab-case story? Kebab-case key transformation explains why the wire format and your code disagree on casing, where the conversion happens, and how to wire it up for STDIO and HTTP transports.
Bumping a server to the latest spec? Protocol versions is the version-by-version feature matrix and the negotiation algorithm. 2025-11-25 features walks through what that revision added. 2026-07-28: the stateless revision covers the redesign that followed, which removed the handshake and replaced server-initiated requests.
Validating tool / prompt / resource shapes before shipping? Schema validation covers the Malli schema namespace and the !-suffixed throwing constructors.
Resources whose content changes over time? Dynamic resources covers the :read-fn pattern and when to reach for it vs. static :text / :blob.
Plugging your server into Claude? Claude Desktop / Claude Code setup covers claude_desktop_config.json and claude mcp add, plus the STDIO vs SSE transport choices.
Serving your server over HTTP to modern / remote clients? Streamable HTTP transport covers the current 2025-03-26+ transport: the single /mcp endpoint, Mcp-Session-Id sessions, JSON-or-SSE responses, Last-Event-Id resumability, and how it differs from the older HTTP+SSE example.
Iterating on tools while a client is connected? REPL workflow is the live-development story: add-tool / remove-tool / notify-resource-updated from a REPL.
Lifting a piece of the toolkit into a different MCP server? Extraction recipes is the worked-example index: how to copy the kebab-case transport, the Malli schema registry, the dynamic-resource handler, the session lifecycle, and the cancellation pattern.
| Page | What you'll learn |
|---|---|
| Getting started | Install via deps.edn; first STDIO server in CLJC; smoke test against MCP Inspector + Claude Desktop |
| Architecture | session atom + context hashmap split, message lifecycle, namespace map (server / client / json-rpc / protocol / schema / impl/*), capability negotiation, cancellation |
| Kebab-case key transformation | Why kebab-case internally, where the camelCase ↔ kebab-case conversion sits (transport layer), jsonista + camel-snake-kebab setup for STDIO, equivalent for shadow-cljs / Node, the wire format reference table |
| Protocol versions | Version negotiation algorithm, what each of the four handshake revisions adds, breaking changes (JSON-RPC batching removed in 2025-06-18), backward-compat notes |
| Schema validation | Malli schema namespace tour (Icon, EnumSchema, SamplingRequest, FormElicitationRequest, UrlElicitationRequest, Task, ToolResultMessage, content-block schemas), valid? / validate / explain, !-suffix throwing constructors (enum-schema!, url-elicitation!, form-elicitation!, tool-result-message!) |
| Dynamic resources | :read-fn pattern for on-demand resource content, return-shape contract (:text / :blob / :contents / :error), Promesa async support, when to use vs. static :text / :blob |
| 2025-11-25 features | Elicitation (form + URL mode for OAuth), Tasks (experimental long-running state machines), Sampling with Tools (LLM tool use during sampling), Icons (data: URIs / https:// URLs), Server Description, JSON Schema 2020-12 dialect, capability detection helpers |
| 2026-07-28: the stateless revision | The handshake-free redesign: what was removed, creating a stateless session, server/discover, per-request context, Multi Round-Trip Requests, subscriptions/listen, the new result and error-code shapes, writing a transport, serving both eras on one session |
| Claude Desktop / Claude Code setup | claude_desktop_config.json STDIO config, claude mcp add for Claude Code, SSE transport for Claude Code (not yet supported by Desktop), Docker-based server config, MCP Inspector for debugging |
| Streamable HTTP transport | The current remote transport (2025-03-26+): single /mcp endpoint, Mcp-Session-Id sessions, JSON-or-SSE responses (the "flip"), Last-Event-Id resumability, Host/Origin + MCP-Protocol-Version security; how it differs from the HTTP+SSE example |
| REPL workflow | nREPL embedded in your STDIO server, add-tool / remove-tool / notify-resource-updated from the REPL while a client is connected, Claude Desktop log tailing for diagnostics |
| Docker + REPL smoke test | Standing the 2026-07-28 server up in a container and driving it from a REPL with this library's own client, rather than curl: what a clean room catches, and the _meta round trip that proves the key rules held |
| Extraction recipes | Worked recipes, kebab-case transport for any JSON-RPC service, Malli protocol-schema registry pattern, dynamic-resource :read-fn, multi-version handshake negotiation, cancellation via is-cancelled atom, REPL-aware notification helpers |
| Scenario | Pages to read |
|---|---|
| Build my first MCP server in Clojure | Getting started, Architecture |
| Build an MCP server in ClojureScript / Node.js | Getting started, Kebab-case key transformation (cljs section) |
| Plug my server into Claude Desktop | Claude Desktop / Claude Code setup |
| Plug my server into Claude Code via SSE | Claude Desktop / Claude Code setup (SSE section) |
| Serve my MCP server over Streamable HTTP (current remote transport) | Streamable HTTP transport |
| Add a tool that streams progress notifications | Architecture (notify-progress), REPL workflow |
| Add a resource whose content is computed at request time | Dynamic resources |
| Add a tool with structured output | Schema validation, Protocol versions (2025-06-18 §output-schema) |
| Add an icon to a tool / prompt / resource | 2025-11-25 features (Icons) |
| Request user input via a form | 2025-11-25 features (Elicitation §form mode) |
| Drive an OAuth flow from inside an MCP tool | 2025-11-25 features (Elicitation §url mode) |
| Track a long-running operation | 2025-11-25 features (Tasks) |
| Let an LLM use tools during a sampling request | 2025-11-25 features (Sampling with Tools) |
| Validate my tool definition before registering it | Schema validation |
| Negotiate down to an older client | Protocol versions |
| Iterate on tools while my MCP client is connected | REPL workflow |
| Lift the kebab-case transport into another JSON-RPC service | Extraction recipes (Recipe 1) |
| Lift the Malli schema-registry pattern into another project | Extraction recipes (Recipe 2) |
| Pattern | Where it lives | Closest sibling |
|---|---|---|
| Kebab-case keys internally + camelCase on the wire (transport-layer conversion) | example/cljc-server-stdio/src/example/my_server.cljc (the canonical wiring); protocol/encode-key + protocol/decode-key via jsonista | (none as a standalone library; Metosin upstream uses raw camelCase) |
Malli registry of MCP protocol schemas with !-suffix throwing constructors | src/mcp_toolkit/schema.cljc | (none, most MCP SDKs validate ad hoc rather than through a shared schema registry) |
Dynamic resources via :read-fn returning {:text} / {:blob} / {:contents} / {:error} (or a Promesa promise of any) | src/mcp_toolkit/impl/server/handler.cljc (resource-read-handler) | (none, most MCP SDKs only support static resource content) |
Multi-version automatic negotiation against :server-supported-protocol-versions, one of three lists chosen by session kind (["2024-11-05" "2025-03-26" "2025-06-18" "2025-11-25"] for a plain session) | src/mcp_toolkit/impl/server/handler.cljc (initialize-handler) | (none, most MCP SDKs hardcode a single version) |
MCP cancellation via per-request is-cancelled atom + notifications/cancelled handler | src/mcp_toolkit/json_rpc.cljc (route-message) + src/mcp_toolkit/impl/server/handler.cljc (cancelled-notification-handler) | (none, Promesa promises don't have a built-in cancel signal; this pattern lifts to any long-running async handler) |
If you're picking patterns:
impl/.See bb tasks for the full list. The 30-second version:
bb test # run all tests via kaocha
bb dev # nREPL on :7777 with CIDER middleware
bb example:server:stdio # run example STDIO server
bb example:server:sse # run example SSE server (clj-server-sse)
bb example:server:streamable-http # run example Streamable HTTP server (2025-03-26+)
bb example:server:streamable-http:2026 # run example Streamable HTTP server (2026-07-28, stateless)
bb example:client:stdio # run example STDIO client
bb info # grouped, categorised cheat-sheet of all tasks
bb tasks # list every task with its docstring
The library runs on Babashka, and bb bb:smoke proves it by driving a real tools/list and tools/call through a server session rather than just requiring the namespaces. Loading is the easy half, and both problems below sit behind a successful require. bb ci runs it as step 5, so it cannot rot quietly. On a Babashka too old to run it, the gate warns and carries on rather than failing, since this project's :min-bb-version is 1.3.0.
Two things are needed, and neither alone is enough. Babashka 1.13.220 or newer, because earlier versions have no java.util.concurrent.locks.ReentrantLock and promesa.util imports it unconditionally. And promesa 12, because promesa 11 has a deftype implementing java.util.function.Supplier that SCI rejects.
That promesa version is supplied by the :babashka alias in deps.edn rather than by the project pin, because promesa 12 carries a ClojureScript regression that json_rpc.cljc walks straight into. A p/handle whose function returns a Throwable rejects with promesa's own wrapper promise instead of the exception, so the p/catch after it receives a PromiseImpl, ex-message is nil, and every error response quietly loses its reason. A plain p/catch is unaffected, and so is the JVM. It takes that one shape on that one platform, which is how it reached a release. Reported as promesa#171. The two problems land on opposite sides of the platform split, so the override is scoped to the one runtime that needs it.
Consuming this library on Babashka means doing the same in your own deps.edn, since the pin you inherit is promesa 11:
{:aliases {:babashka {:override-deps {funcool/promesa {:mvn/version "12.0.1"}}}}}
prompts/list, resources/list or tools/list. Implementations leave a #_#_:next-cursor "next-page-cursor" placeholder in the handler.CHANGELOG.md: version history per release. Two unreleased entries: v2026-07-28 covers the stateless revision, and v2025-11-25 covers the 9 phases of the upstream-spec port (Phase 0 = kebab-case transport, Phase 1 = protocol negotiation, Phase 2 = server description, Phase 3 = icons, Phase 4 = Malli schemas, Phase 5 = sampling with tools, Phase 6 = elicitation, Phase 7 = tasks, Phase 9 = JSON Schema 2020-12 dialect).docs/reference/: preserved upstream Metosin docs (api-design.md, session.md, context.md, using-the-library.md, repl-story.md, MIGRATION-2025-06-18.md).MIGRATION-2025-06-18.md: the older migration writeup (added by upstream when bumping from 2025-03-26 → 2025-06-18).