mcp-tkx, User guide

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:

  • Five protocol revisions in one library: 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.
  • Kebab-case keys end-to-end: your Clojure code uses :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.
  • Malli-based schema validation: 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.
  • Dynamic resources via :read-fn: resources can compute :text / :blob / :contents on demand at resources/read time, returning a plain map or a Promesa promise.
  • 2025-11-25 spec features wired up: Elicitation (form + URL mode for OAuth flows), Tasks (long-running operation state machines), Sampling with Tools (LLM tool use during sampling), Icons (data: URIs or https:// URLs), Server Description, JSON Schema 2020-12 dialect.
  • CLJC: runs on the JVM via jsonista / nREPL or on Node.js via shadow-cljs with no transport-specific changes to your handler code.
  • I/O-agnostic core: the library does no I/O itself; you wire STDIO, HTTP/SSE, or any other transport by providing a :send-message fn and feeding decoded messages into json-rpc/handle-message.
  • REPL-driven development: 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.

How to read this guide

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.

Guide map

PageWhat you'll learn
Getting startedInstall via deps.edn; first STDIO server in CLJC; smoke test against MCP Inspector + Claude Desktop
Architecturesession atom + context hashmap split, message lifecycle, namespace map (server / client / json-rpc / protocol / schema / impl/*), capability negotiation, cancellation
Kebab-case key transformationWhy 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 versionsVersion negotiation algorithm, what each of the four handshake revisions adds, breaking changes (JSON-RPC batching removed in 2025-06-18), backward-compat notes
Schema validationMalli 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 featuresElicitation (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 revisionThe 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 setupclaude_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 transportThe 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 workflownREPL 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 testStanding 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 recipesWorked 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

Find your scenario

ScenarioPages to read
Build my first MCP server in ClojureGetting started, Architecture
Build an MCP server in ClojureScript / Node.jsGetting started, Kebab-case key transformation (cljs section)
Plug my server into Claude DesktopClaude Desktop / Claude Code setup
Plug my server into Claude Code via SSEClaude 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 notificationsArchitecture (notify-progress), REPL workflow
Add a resource whose content is computed at request timeDynamic resources
Add a tool with structured outputSchema validation, Protocol versions (2025-06-18 §output-schema)
Add an icon to a tool / prompt / resource2025-11-25 features (Icons)
Request user input via a form2025-11-25 features (Elicitation §form mode)
Drive an OAuth flow from inside an MCP tool2025-11-25 features (Elicitation §url mode)
Track a long-running operation2025-11-25 features (Tasks)
Let an LLM use tools during a sampling request2025-11-25 features (Sampling with Tools)
Validate my tool definition before registering itSchema validation
Negotiate down to an older clientProtocol versions
Iterate on tools while my MCP client is connectedREPL workflow
Lift the kebab-case transport into another JSON-RPC serviceExtraction recipes (Recipe 1)
Lift the Malli schema-registry pattern into another projectExtraction recipes (Recipe 2)

What's distinctive about this fork

PatternWhere it livesClosest 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 constructorssrc/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 handlersrc/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:

  • Building a Clojure MCP server: start at Getting started, then Architecture. Most server work fits the prompt / resource / tool registration pattern; you don't need to touch impl/.
  • Wiring an MCP server into a different JSON-RPC framework: the Extraction recipes Recipe 1 lifts the kebab-case transport pattern out of the toolkit. The schema validation in Recipe 2 is independent of MCP.

Build & dev

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

Babashka

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"}}}}}

Not covered yet

  • Pagination: not wired through prompts/list, resources/list or tools/list. Implementations leave a #_#_:next-cursor "next-page-cursor" placeholder in the handler.

See also

  • Project README: protocol and feature support table, the dynamic-resources capability row, other-MCP-libs comparison.
  • 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).