with one click
cl-mcp-dev
Developer guide for contributing to cl-mcp
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Developer guide for contributing to cl-mcp
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
| name | cl-mcp-dev |
| description | Developer guide for contributing to cl-mcp |
| version | 0.2.0 |
| author | quasi |
| type | dev |
MCP server and client framework for Common Lisp.
cl-mcp): Expose CL functions as MCP tools. Three calls: make-server → register-tool → run-server.cl-mcp/client): Connect to MCP server subprocesses, list tools, call tools. Threaded reader loop with promise-based request/response correlation.Protocol: MCP 2025-06-18 (server) / 2025-11-25 (client). Implementation: SBCL. Build: ASDF. License: MIT.
Known consumers: cl-mcp-server (CL REPL tools), chatterbox, ghost.
# Load server
sbcl --load cl-mcp.asd --eval "(ql:quickload :cl-mcp)"
# Load client
sbcl --load cl-mcp.asd --eval "(ql:quickload :cl-mcp/client)"
# Test server (full suite)
sbcl --load cl-mcp.asd \
--eval "(ql:quickload :cl-mcp/tests)" \
--eval "(asdf:test-system :cl-mcp)"
# Test client (full suite)
sbcl --load cl-mcp.asd \
--eval "(ql:quickload :cl-mcp/client-tests)" \
--eval "(asdf:test-system :cl-mcp/client)"
| Package | File | Contents |
|---|---|---|
cl-mcp.conditions | src/conditions.lisp | JSON-RPC error conditions (5 discrete codes) |
cl-mcp.json-rpc | src/json-rpc.lisp | Message structs, parsing, encoding |
cl-mcp.transport | src/transport.lisp | NDJSON stdio read/write |
cl-mcp.tools | src/tools.lisp | Tool registry, validation, dispatch |
cl-mcp | src/server.lisp | Public API: make-server, register-tool, run-server |
Load order enforced by ASDF :serial t: conditions → json-rpc → transport → tools → server.
| Package | File | Contents |
|---|---|---|
cl-mcp.client.conditions + cl-mcp.client | src/client/packages.lisp | Both package definitions + all exports |
cl-mcp.client.conditions | src/client/conditions.lisp | in-package + condition define-condition forms only |
cl-mcp.client | src/client/protocol.lisp | encode-request, parse-client-message, pending-request promise machinery |
cl-mcp.client | src/client/client.lisp | mcp-client struct, connect/disconnect, list-tools, call-tool, with-mcp-client |
Client depends on cl-mcp (shared json-rpc structs) and bordeaux-threads.
cl-mcp.asd System definitions: cl-mcp, cl-mcp/client, cl-mcp/tests, cl-mcp/client-tests
src/packages.lisp Server package definitions
src/conditions.lisp JSON-RPC 2.0 error conditions (-32700, -32600, -32601, -32602, -32603)
src/json-rpc.lisp Message structs (json-rpc-request, json-rpc-response), parse-message, encode-response
src/transport.lisp read-message, write-message (NDJSON over stdio)
src/tools.lisp register-tool, call-tool, normalize-tool-result
src/server.lisp mcp-server struct, %handle-request, run-server
src/client/packages.lisp Client package definitions
src/client/conditions.lisp Client conditions (mcp-connect-error, mcp-session-closed, mcp-protocol-error, mcp-tool-error)
src/client/protocol.lisp encode-request, parse-client-message, pending-request promise
src/client/client.lisp mcp-client struct, connect, disconnect, list-tools, call-tool, with-mcp-client
tests/ FiveAM suites (server, 6 files)
tests/client/ FiveAM suites (client, 4 ASDF-loaded files: packages, conditions-tests, protocol-tests, client-tests)
tests/client/test-server.lisp Standalone subprocess fixture (loaded via sbcl --load by client-tests, NOT an ASDF component)
digraph cl_mcp {
rankdir=LR
"cl-mcp.conditions" -> "cl-mcp.json-rpc"
"cl-mcp.conditions" -> "cl-mcp.tools"
"cl-mcp.json-rpc" -> "cl-mcp.transport"
"cl-mcp.json-rpc" -> "cl-mcp"
"cl-mcp.transport" -> "cl-mcp"
"cl-mcp.tools" -> "cl-mcp"
"yason" -> "cl-mcp.json-rpc"
"opsis/conditions" -> "cl-mcp"
"cl-mcp" -> "cl-mcp/client"
"bordeaux-threads" -> "cl-mcp/client"
"cl-mcp.client.conditions" -> "cl-mcp.client"
"cl-mcp.client" -> "cl-mcp/client-tests"
"fiveam" -> "cl-mcp/client-tests"
}
Caller thread: connect → list-tools → call-tool → disconnect
Reader thread: %reader-loop → parse-client-message → %dispatch-response → fulfill-promise
Synchronization:
client-pending-lock — protects pending hash-table and next-id counterpending-request has its own lock+condvar for caller/reader rendezvous%abort-all-pending: fulfills all pending with error on EOF; collects under lock, fulfills outside to avoid lock-order inversion| Type | Convention | Examples |
|---|---|---|
| Predicates | -p suffix | notification-p, content-block-list-p |
| Constructors | make- prefix | make-server, make-client, make-request |
| Internal functions | % prefix | %handle-initialize, %send-request, %reader-loop |
| File header | ;;; ABOUTME: comment | ;;; ABOUTME: MCP client |
Every valid JSON-RPC request MUST receive exactly one response.
Applies to: run-server, %handle-request
Violation consequence: MCP client hangs indefinitely.
Agent action: Flag. Auto-fix forbidden.
Server MUST NOT terminate due to handler errors. All errors MUST be caught and returned as JSON-RPC error responses.
Applies to: run-server (outer loop), %handle-request (inner dispatch)
Violation consequence: Server process dies on first tool error.
Agent action: Flag. Auto-fix forbidden.
cl-mcp.tools:register-tool (internal): five positional args — (registry name description input-schema handler).
cl-mcp:register-tool (public): keyword args — (server name &key description schema handler).
These are different functions. The public wrapper calls the internal one.
Each mcp-server has its own tool registry (hash-table). No global *tools* or similar variable is permitted.
Handlers MUST accept exactly one argument (arguments alist) and MUST return a string or a list of MCP content blocks.
pending-request MUST be registered in the pending table BEFORE the request is written to server stdin.
Applies to: %send-request
Violation consequence: Race condition — response arrives before promise exists, dropped silently.
Agent action: Flag. Auto-fix forbidden.
client-pending-lock → pending-request lock. Never acquire in reverse order.
Applies to: %dispatch-response, %abort-all-pending
Violation consequence: Deadlock.
Agent action: Flag. Auto-fix forbidden.
On EOF or reader thread error, ALL pending requests MUST be fulfilled with an error response.
Applies to: %reader-loop, %abort-all-pending
Violation consequence: Calling threads block forever.
Agent action: Flag. Auto-fix forbidden.
| ID | Invariant |
|---|---|
| INV-001 | Every valid JSON-RPC request receives exactly one response |
| INV-002 | Server never terminates due to handler errors |
| INV-003 | Tool registries are per-server (no global state) |
| INV-004 | Handler results are always normalized to MCP content format before sending |
| INV-005 | All messages conform to JSON-RPC 2.0 ("jsonrpc": "2.0" field present) |
| INV-006 | Client: pending promise registered before request written to stdin |
| INV-007 | Client: all pending requests aborted on EOF or reader thread failure |
Framework: FiveAM.
Suite symbol: cl-mcp-tests in cl-mcp-tests.
| Suite file | Covers |
|---|---|
conditions-tests.lisp | Error codes, messages, condition hierarchy |
json-rpc-tests.lisp | Request/response creation, JSON parsing |
encoding-tests.lisp | Response encoding, null ID, error encoding |
transport-tests.lisp | NDJSON read/write, EOF handling |
tools-tests.lisp | Registration, listing, validation, dispatch, normalization |
server-tests.lisp | Full MCP session flows, error recovery (string streams simulate stdio) |
Suite symbol: cl-mcp-client-tests in cl-mcp-client-tests.
| Suite file | Covers |
|---|---|
conditions-tests.lisp | Client condition types, messages, accessors |
protocol-tests.lisp | encode-request, parse-client-message, pending-request promise |
client-tests.lisp | connect/disconnect, list-tools, call-tool, with-mcp-client |
test-server.lisp | In-process test server fixture for client integration tests |
Server emits events via opsis/c:emit at:
:server-started — on run-server entry:server-stopped — on EOF:request-received — each incoming request (data: :method):request-failed — unhandled errors (level: :error)Dependency: opsis/conditions (source: https://github.com/quasi/opsis).