Skip to content

Add client support and complete the pending specification changes - #5

Open
vedang wants to merge 48 commits into
mainfrom
client-support
Open

vedang wants to merge 48 commits into
mainfrom
client-support

Conversation

@vedang

@vedang vedang commented Jun 12, 2026

Copy link
Copy Markdown
Contributor
  • The client support is inspired by the integration-test format used by clojure-lsp
  • The specification changes are done keeping the java-sdk as the comparison target

vedang and others added 30 commits December 26, 2025 16:00
Use these example servers for integration testing against an imaginary
client that I am in the process of building.
…tion framework

- Copy the entrypoint and client.clj files with minor changes
- Modify lsp.clj to mcp.clj, with minor changes
- Create an empty integration.helper file for compilation purposes

Right now, there is no integration test running, but the plan is to
run the servers and do basic integration testing from the point of
view of the client.
Modify the integration.client framework to use our example servers
when running integration tests against the client.
Implement previously stubbed requests: resources/templates/list,
resources/subscribe|unsubscribe, logging/setLevel, completion/complete,
and the cancelled/progress notification receivers. Add server->client
senders (list_changed x3, resources/updated, logging message with
RFC-5424 level filtering, progress) and request helpers (roots/list,
sampling/createMessage). Advertise full default capabilities. Fix ping
to return {} per schema (Result must be an object).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add notify-progress! (mirrors the server-side sender) and
add-root!/remove-root! which update client state and automatically send
notifications/roots/list_changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Server (http_server.clj): Pedestal + Jetty on a single /mcp endpoint.
POST routes JSON-RPC requests into a per-session jsonrpc4clj endpoint
and answers with the matching response; notifications/responses get
202. GET opens an SSE stream for server-initiated messages; DELETE
terminates the session. Sessions are keyed by the Mcp-Session-Id header
assigned on initialize.

Client (http_client.clj): clj-http sender loop POSTing outgoing
messages, session id capture from the initialize response, and an SSE
listener feeding server-initiated messages into the client input
channel. run!/shutdown! mirror the stdio client API.

io_chan.clj gains public message->json-str/json-str->message so HTTP
and stdio share identical wire conventions. Pedestal pinned to 0.8.1
to match the logger's pedestal.log. JSON-RPC batching is intentionally
unsupported (removed in the 2025-06-18 spec revision).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vedang and others added 13 commits June 12, 2026 12:37
Some JSON providers (cheshire under Babashka, jsonista without an
explicit provider) return nil for nil input instead of throwing. At
stream EOF readLine returns nil, the nil leaked through json-str->message
and crashed the reader thread with 'Can't put nil on channel' — leaving
the input channel open, so stdio servers never shut down when their
client closed stdin (integration harness then hung on process deref).

Verified: bb integration suite against the calculator server now passes
(4 tests, 11 assertions) and the server process exits cleanly on EOF.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README gains Client Usage and Streamable HTTP Transport sections;
CHANGELOG records the new protocol surface, transports, dependencies
and the io-chan EOF fix; todo.org closes the client, roots, sampling,
integration-test and HTTP transport items. Task plan/progress notes in
.agents/plans/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Version is maj.min.x where x is the git commit count at build time
(unchanged). maj.min moves from a hardcoded string in build.clj to a
VERSION file, bumped by 'make release-major' (maj+1, min reset to 0)
and 'make release-minor' (min+1). Each release target requires a clean
working copy and commits the bump on its own (jj, with git fallback).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/call, prompts/get, resources/read and completion/complete now run
inside a promesa thread (eventually macro), so a slow handler never
blocks the jsonrpc message loop. jsonrpc4clj resolves future-returning
handlers on completion, which gives concurrent request processing:
pings, list requests and parallel tool calls are served while a slow
tool runs. Matches mcp-java-sdk's async server semantics that
clojure-mcp relies on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add unregister-tool!/unregister-resource!/unregister-prompt!. The
context now carries a :server* atom that start! fills; while a server
is attached, register-*!/unregister-*! automatically send the matching
list_changed notification (mcp-java-sdk add/remove parity). Registration
during create-context! happens before start! and stays silent. The
Streamable HTTP transport (one endpoint per session) intentionally does
not set :server*; broadcast explicitly there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Validate tool :annotations in the ::tool spec (title plus the four
boolean hints). Allow overriding advertised capabilities through the
:capabilities key of the server spec. Negotiate protocol version
2025-06-18, preferring the latest. Closes the remaining small parity
gaps with mcp-java-sdk usage in clojure-mcp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bench/ holds two identical stdio MCP servers (mcp-java-sdk 1.1.3 using
clojure-mcp's exact interop pattern, and this SDK) driven by one
SDK-neutral raw-JSON client. Measures startup-to-initialize, serial
RTT (ping / 24B echo / 64KB echo) and 100 pipelined 20ms tool calls.
Run with 'make bench' (AOT-compiles the cljc server first).

Headline results (full report in .agents/plans/...benchmark__done/):
- java-sdk silently LOSES concurrent async tool responses (299/300 at
  depth 100): unicast Sinks.Many tryEmitNext is not concurrency-safe
  (StdioServerTransportProvider.java:158) and the error is swallowed.
  Ours: 0 lost, 34ms wall.
- startup: 893ms java vs 977ms ours AOT-compiled (3012ms from source;
  core.async compile-at-load accounts for ~1.6s).
- serial RTT and 64KB payloads at parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A handler return value that is a map with :content passes through
coerce-tool-response untouched, so handlers can set :isError (tool
errors the LLM should see, per the spec) and :structuredContent
directly. Needed by the clojure-mcp port, whose tools signal isError
through their continuation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A resource read handler can return either a single contents map or a
sequence of them, matching the schema (ReadResourceResult.contents is a
list). Needed by the clojure-mcp port, whose resource-fns return a list
of strings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vedang added a commit that referenced this pull request Jun 12, 2026
CI installs zprint 'latest' (now 1.3.0), which wraps two long forms in
validate-spec! and the :tool/properties spec that older zprint left on
one line — so 'make format' produced changes and the formatting gate
failed (also blocking PR #5, whose CI runs on the merge into main).
Format those forms, and port the explicit $(ZPRINT_CONFIG) pass-through
to the Makefile so CI (no ~/.zprint.edn) applies the repo's :fn-map/:map
config deterministically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vedang and others added 5 commits June 12, 2026 22:06
CI installs zprint 'latest' (now 1.3.0), which wraps two long forms in
validate-spec! and the :tool/properties spec that older zprint left on
one line — so 'make format' produced changes and the formatting gate
failed (also blocking PR #5, whose CI runs on the merge into main).
Format those forms, and port the explicit $(ZPRINT_CONFIG) pass-through
to the Makefile so CI (no ~/.zprint.edn) applies the repo's :fn-map/:map
config deterministically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
make check runs check-tagref first, which calls the tagref binary.
The GitHub runner never installed tagref, so PR CI failed with
'tagref not found'. Download a pinned tagref release (v1.12.1,
x86_64-unknown-linux-gnu) to /usr/local/bin before the check steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
make check runs check-cljkondo, which calls the clj-kondo binary.
The GitHub runner never installed clj-kondo, so PR CI failed with
'clj-kondo: command not found'. Add the clj-kondo input to the
setup-clojure step so the action provisions it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
clojure.org's install guide recommends Temurin 25, so bump the JDK
from 17 to 25 (and setup-java v4 -> v5.2.0).

Pin every action to a full commit SHA instead of a mutable tag, a
supply-chain hardening measure: a repointed tag can no longer pull
unexpected code into CI. Each pin keeps the human-readable version
in a trailing comment. Also bumps checkout v4 -> v6.0.3 and
setup-clojure 12.5 -> 13.6.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant