A minimal Model Context Protocol (MCP) server for Common Lisp. It provides a newline‑delimited JSON‑RPC 2.0 transport over stdio or TCP, a small protocol layer (initialize, ping, tools/list, tools/call), and a REPL tool that evaluates forms and returns the last value.
This repo is intentionally small and test-first. It’s designed for editor/agent clients to drive Common Lisp development via MCP.
- JSON‑RPC 2.0 request/response framing (one message per line)
- MCP initialize handshake with capability discovery
- Tools API
repl-eval— evaluate formsfs-read-file/fs-write-file/fs-list-directory— project-scoped file access with allow‑listfs-get-project-info— report project root and cwd info for path normalizationfs-set-project-root— set the server's project root and working directorylisp-read-file— Lisp-aware file viewer with collapsed/expanded modescode-find/code-describe/code-find-references— sb-introspect based symbol lookup/metadata/xreflisp-edit-form— structure-aware edits to top-level forms using Eclector CSTlisp-check-parens— detect mismatched delimiters in code slices
- Transports:
:stdio,:tcp, and:http(Streamable HTTP for Claude Code) - Structured JSON logs with level control via env var
- Rove test suite wired through ASDF
test-op
- Protocol versions recognized:
2025-06-18,2025-03-26,2024-11-05- On
initialize, if the client’sprotocolVersionis supported it is echoed back; if it is not supported the server returnserror.code = -32602withdata.supportedVersions.
- On
- SBCL 2.x (developed with SBCL 2.5.x)
- Quicklisp (for dependencies)
- Dependencies (via ASDF/Quicklisp): runtime —
alexandria,cl-ppcre,yason,usocket,bordeaux-threads,eclector,hunchentoot; tests —rove.
IMPORTANT: Set the MCP_PROJECT_ROOT environment variable before starting:
export MCP_PROJECT_ROOT=/path/to/your/projectLoad and run from an existing REPL:
(ql:quickload :cl-mcp)
;; Start TCP transport on port 12345 in a new thread.
(cl-mcp:start-tcp-server-thread :port 12345)Or run a minimal stdio loop (one JSON‑RPC line per request):
# With environment variable
export MCP_PROJECT_ROOT=$(pwd)
ros run -s cl-mcp -e "(cl-mcp:run :transport :stdio)"Alternative: If you don't set MCP_PROJECT_ROOT, you must call fs-set-project-root
tool immediately after connecting to initialize the project root.
Start the HTTP server and continue using your REPL:
(ql:quickload :cl-mcp)
;; Start HTTP server on port 3000 (default)
(cl-mcp:start-http-server :port 3000)
;; Server is now running at http://127.0.0.1:3000/mcp
;; You can continue using your REPL normally
(+ 1 2) ; => 3
;; Stop the server when done
(cl-mcp:stop-http-server)Configure Claude Code to connect (in ~/.claude/settings.json or project .mcp.json):
{
"mcpServers": {
"cl-mcp": {
"type": "url",
"url": "http://127.0.0.1:3000/mcp"
}
}
}This is the recommended approach for Common Lisp development:
- Start your REPL as usual
- Load cl-mcp and start the HTTP server
- Configure Claude Code to connect
- Both you and Claude Code can use the same Lisp runtime simultaneously
- Python TCP one‑shot client (initialize):
python3 scripts/client_init.py --host 127.0.0.1 --port 12345 --method initialize --id 1- Stdio↔TCP bridge (connect editor’s stdio to the TCP server):
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | \
python3 scripts/stdio_tcp_bridge.py --host 127.0.0.1 --port 12345The bridge uses a bounded connect timeout but disables read timeouts after connecting, so it can stay idle indefinitely (until stdin closes).
Evaluate one or more forms and return the last value as a text item.
Input schema (JSON):
code(string, required): one or more s‑expressionspackage(string, optional): package to evaluate in (defaultCL-USER)printLevel(integer|null): binds*print-level*printLength(integer|null): binds*print-length*timeoutSeconds(number|null): abort evaluation after this many secondsmaxOutputLength(integer|null): truncatecontent/stdout/stderrto this many characterssafeRead(boolean|null): whentrue, disables*read-eval*while reading forms Output fields:content: last value as text (existing)stdout: concatenated standard output from evaluationstderr: concatenated standard error from evaluation
Example JSON‑RPC request:
{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"repl-eval","arguments":{"code":"(+ 1 2)"}}}Response (excerpt):
{"result":{"content":[{"type":"text","text":"3"}]}}asdf-system-info and asdf-list-systems are temporarily disabled.
Read text from an allow‑listed path.
Input:
path(string, required): project‑relative or absolute inside a registered ASDF system’s source treeoffset/limit(integer, optional): substring window
Policy: reads are allowed only when the resolved path is under the project root or under asdf:system-source-directory of a registered system.
Dependency libs: reading source in Quicklisp/ASDF dependencies is permitted only via fs-read-file; do not shell out for metadata (wc, stat, etc.). File length is intentionally not returned—page through content with limit/offset when needed.
Write text to a file under the project root (directories auto-created).
Input:
path(string, required): must be relative to the project rootcontent(string, required)
Policy: writes outside the project root are rejected.
List entries in a directory (files/directories only, skips hidden and build artifacts).
Input:
path(string, required): project root or an ASDF system source dir.
Returns: entries array plus human-readable content.
Report project root and current working directory information for clients that need to normalize relative paths.
Output:
project_root(string): resolved project rootcwd(string|null): current working directoryproject_root_source(string): one ofenvorexplicitrelative_cwd(string|null): cwd relative to project root when inside it
Synchronize the server's project root and working directory with the client's location.
Input:
path(string, required): path to the project root directory (absolute preferred; relative is resolved to an absolute directory)
This tool allows AI agents to explicitly set the server's working directory, ensuring
path resolution works correctly. The server updates both *project-root* and the
current working directory (via uiop:chdir).
Output:
project_root(string): new project root pathcwd(string): new current working directoryprevious_root(string): previous project root pathstatus(string): confirmation message
Best Practice for AI Agents: Call fs-set-project-root at the beginning of your
session with your current working directory to ensure file operations work correctly.
Read a file with Lisp-aware collapsing and optional pattern-based expansion.
Inputs:
path(string, required): absolute path or project-relative.collapsed(boolean, defaulttrue): whentrueand the file is Lisp source (.lisp,.asd,.ros,.cl,.lsp), return only top-level signatures (e.g.,(defun name (args) ...)) while keepingin-packageforms fully shown.namePattern(string, optional): CL-PPCRE regex; matching definition names are expanded even in collapsed mode.contentPattern(string, optional): CL-PPCRE regex applied to form bodies; if it matches, the full form is expanded. For non-Lisp files, this triggers a grep-like text filter with ±5 lines of context.offset/limit(integer, optional): slice window used whencollapsedisfalse; defaults tooffset=0,limit=2000lines.
Output fields:
content: formatted text (collapsed Lisp view, raw slice, or filtered text).path: normalized native pathname.mode: one oflisp-collapsed,raw,text-filtered,lisp-snippet,text-snippetdepending on inputs and file type.meta: includestotal_forms/expanded_formsfor collapsed Lisp, ortotal_linesplustruncatedflag for slices/filters.
Check balanced parentheses/brackets in a file slice or provided code; returns the first mismatch position.
Input:
path(string, optional): absolute path inside the project or registered ASDF system (mutually exclusive withcode)code(string, optional): raw code string (mutually exclusive withpath)offset/limit(integer, optional): window when reading frompath
Output:
ok(boolean)- when not ok:
kind(extra-close|mismatch|unclosed|too-large),expected,found, andposition(offset,line,column).
Notes:
- Uses the same read allow-list and 2 MB cap as
fs-read-file. - Ignores delimiters inside strings,
;line comments, and#| ... |#block comments.
Perform structure-aware edits to a top-level form using Eclector CST parsing while preserving surrounding formatting and comments.
Input:
file_path(string, required): absolute path or project-relative pathform_type(string, required): form constructor to match, e.g.,defun,defmacro,defmethodform_name(string, required): name/specializers to match; fordefmethodinclude specializers such as"print-object (my-class t)"operation(string, required): one ofreplace,insert_before,insert_aftercontent(string, required): full form text to insert
Output:
path,operation,form_type,form_namebytes: size of the updated file contentcontent: human-readable summary string of the applied change
Return definition location (path, line) for a symbol using SBCL sb-introspect.
Input:
symbol(string, required): prefer package-qualified, e.g.,"cl-mcp:version"package(string, optional): used whensymbolis unqualified; must exist
Output:
path(relative when inside project, absolute otherwise)line(integer or null if unknown)
Return symbol metadata (name, type, arglist, documentation).
Input:
symbol(string, required)package(string, optional): must exist whensymbolis unqualified
Output:
type("function" | "macro" | "variable" | "unbound")arglist(string)documentation(string|null)
Return cross-reference locations for a symbol using SBCL sb-introspect.
Input:
symbol(string, required)package(string, optional): package used whensymbolis unqualifiedprojectOnly(boolean, defaulttrue): limit results to files under the project root
Output:
refs(array): each element includespath,line,type(call,macro,bind,reference,set), andcontextcount(integer): number of references returnedsymbol: echoed symbol nameprojectOnly: whether results were filtered to the projectcontent: newline-separated human-readable summary of references
- Structured JSON line logs to
*error-output*. - Control level via env var
MCP_LOG_LEVELwith one of:debug,info,warn,error.
Example:
MCP_LOG_LEVEL=debug sbcl --eval '(ql:quickload :cl-mcp)' ...ros install fukamachi/rove
rove cl-mcp.asdCI / sandbox note:
- Socket-restricted environments may fail
tests/tcp-test.lisp. Run core suites without TCP via:rove tests/core-test.lisp tests/protocol-test.lisp tests/tools-test.lisp tests/repl-test.lisp tests/fs-test.lisp tests/code-test.lisp tests/logging-test.lisp
- Run TCP-specific tests only where binding to localhost is permitted:
rove tests/tcp-test.lisp
What’s covered:
- Version/API surface sanity
- REPL evaluation semantics (reader eval enabled)
- Protocol handshake (
initialize,ping, tools listing/calls) - Logging of RPC dispatch/results
- TCP server accept/respond (newline‑delimited JSON)
- Stdio↔TCP bridge stays alive on idle and exits cleanly when stdin closes
Note: Running tests compiles FASLs into ~/.cache/.... Ensure your environment
allows writing there or configure SBCL’s cache directory accordingly.
src/— packages, logging, REPL, protocol, TCP, run entrypointtests/— Rove test suites invoked by ASDFtest-opscripts/— helper clients and a stdio↔TCP bridgecl-mcp.asd— main and test systems (delegatestest-opto Rove)
- Reader and runtime evaluation are both enabled. Treat this as a trusted, local-development tool; untrusted input can execute arbitrary code in the host Lisp image.
- File access:
- Reads: project root, or
asdf:system-source-directoryof registered systems. - Writes: project root only; absolute paths are rejected.
- Reads: project root, or
- If exposure beyond trusted usage is planned, add allowlists, resource/time limits, and output caps.
- Bridge exits after a few seconds of inactivity: ensure you’re using the
bundled
scripts/stdio_tcp_bridge.py(it disables read timeouts after connect) and that your stdin remains open. - Permission errors compiling FASLs during tests: allow writes under
~/.cacheor reconfigure SBCL’s cache path. - No output on stdio: remember the protocol is one JSON‑RPC message per line. Each request must end with a newline and the server will answer with exactly one line (or nothing for notifications).
The prompts/ directory contains recommended system prompts for AI agents working with cl-mcp.
File: prompts/repl-driven-development.md
This comprehensive guide teaches AI agents how to effectively use cl-mcp's tools for interactive Common Lisp development. It covers:
- Initial setup: Project root configuration and session initialization
- Core philosophy: The "Tiny Steps with Rich Feedback" approach (EXPLORE → DEVELOP → EDIT → VERIFY)
- Tool usage guidelines: When to use
lisp-edit-formvsfs-write-file,lisp-read-filevsfs-read-file, andrepl-evalbest practices - Common Lisp specifics: Package handling, dependency loading, pathname resolution
- Recommended workflows: Step-by-step guides for common tasks (modifying functions, debugging, running tests, adding features)
- Troubleshooting: Diagnosis and solutions for common errors
- Performance considerations: Token-efficient strategies for large codebases
Usage:
-
For Claude Code users: Reference this prompt in your
CLAUDE.mdorAGENTS.md:@/path/to/cl-mcp/prompts/repl-driven-development.md
-
For other AI agents: Include the prompt content in your agent's system instructions or configuration file.
-
For MCP client configuration: Add instructions field to your
mcp.json:{ "mcpServers": { "cl-mcp": { "instructions": "Follow the guidelines in prompts/repl-driven-development.md for Common Lisp development with this server." } } }
The prompt is designed to help AI agents make optimal use of cl-mcp's structural editing and introspection capabilities, avoiding common pitfalls like overwriting files or working in the wrong package context.
When using cl-mcp with AI agents like Claude Code, you should configure the agent to synchronize the project root at the start of each session.
Add server-specific instructions to your MCP client configuration. For Claude Code,
edit your mcp.json or configuration file:
{
"mcpServers": {
"cl-mcp": {
"command": "ros",
"args": ["run", "-l", "cl-mcp", "-e", "(cl-mcp:run)"],
"env": {
"MCP_PROJECT_ROOT": "${workspaceFolder}"
},
"instructions": "IMPORTANT: At the start of your session, call fs-set-project-root with the absolute path of your current working directory (e.g., /home/user/project) to synchronize the server's project root. This ensures all file operations work correctly."
}
}
}You can also set MCP_PROJECT_ROOT environment variable before starting the server:
export MCP_PROJECT_ROOT=/path/to/your/project
ros run -s cl-mcp -e "(cl-mcp:run)"The server will use this path during initialization, though calling fs-set-project-root
explicitly is still recommended for dynamic project switching.
- Error taxonomy as condition types mapped to JSON‑RPC errors
- Bounds/quotas for tool outputs (content length caps)
MIT