About Cl MCP
Cl MCP is an MCP server published by cl-ai-project in the Other category.
The repository has 88 stars and 18 forks, with the last commit 4 days ago. Recent commit activity is the strongest signal that reported issues will get attention.
Install
git clone https://github.com/cl-ai-project/cl-mcpThis server has no ConduID identity, so agent calls to it are not receipted. Pin the version you install and review the source before granting it credentials.
Ask AI
Ask AI about Cl MCP
Powered by Claude · Grounded in docs
Security checks
Static checks derived from the repository 3 days ago. Heuristics on the source, not an audit.
- ✓README presentThe repository ships documentation.
- ✓License declaredUsable under a stated license.
- ✓Tests presentA test suite exists in the repository.
- ✓Dependencies pinnedA lockfile pins dependency versions.
2 checks have not been run yet.
Releases
README
cl-mcp
A Model Context Protocol (MCP) server for Common Lisp, providing JSON-RPC 2.0 over stdio, TCP, and HTTP (Streamable HTTP). It enables AI agents to interact with Common Lisp environments through structured tools for REPL evaluation, system loading, file operations, code introspection, and structure-aware editing.
Features
- REPL evaluation with object inspection — evaluate forms, drill down into complex results (CLOS instances, hash-tables, etc.), and capture structured error context with stack frames and local variables
- Sandboxed file operations — read/write/list files restricted to the project root and ASDF system source directories, preventing accidental access outside the project
- Structure-aware Lisp editing — replace, insert, and patch top-level forms using Eclector CST parsing with automatic parinfer repair, preserving formatting and comments
- Code intelligence — symbol lookup and description; callers with their exact call sites and the tests that exercise them; CLOS class hierarchies, slots and generic-function methods; macroexpansion of a form straight from its file; Lisp-aware search and file viewing with collapsed signatures; CLHS lookup
- Structured test runner — run Rove, FiveAM and prove tests with pass/fail counts, failure details, and source locations
- Worker pool isolation — eval-dependent tools run in isolated child SBCL processes with automatic crash recovery, circuit breaker, and per-session affinity; an optional init hook can start your app inside the worker
- Three transports — stdio, TCP (multi-client), and Streamable HTTP (for Claude Code)
- Project scaffolding — generate a throwaway package-inferred project with Rove or FiveAM tests
- Optional contract checking — the
cl-spectool group (off by default) lists, reads and checks cl-spec contracts; see Optional tool groups
Tools at a glance
| Category | Tool | Purpose | Runs in |
|---|---|---|---|
| Project & files | fs-set-project-root |
Set this session's project root | parent (the worker follows) |
fs-get-project-info |
Show the root and working directory | parent | |
fs-read-file / fs-write-file / fs-list-directory |
Text I/O inside the allowed roots | parent | |
project-scaffold |
Generate a new project skeleton | parent | |
| Lisp source | lisp-read-file |
Collapsed or pattern-expanded view of a file | parent |
lisp-edit-form / lisp-patch-form |
Replace, insert or delete a top-level form; scoped text patch | parent | |
lisp-check-parens |
Diagnose unbalanced delimiters and suggest the fix | parent | |
clgrep-search |
Lisp-aware grep that reports the enclosing form; no loading needed | parent | |
lisp-macroexpand |
Expand a macro call from a file or a code string | parent locates, worker expands | |
| REPL | repl-eval |
Evaluate forms, with backtraces and locals on error | worker |
inspect-object |
Drill into a result object | worker | |
load-system |
Load or reload an ASDF system | worker | |
run-tests |
Run Rove/FiveAM/prove tests with structured results | worker | |
| Introspection | code-find / code-describe |
Definition location; signature and documentation | worker |
code-find-references |
Callers, call sites and affected tests | worker (xref) + parent (source scan) | |
clos-describe |
Classes, slots and generic-function methods | worker, verified against source by the parent | |
| Reference | clhs-lookup |
Common Lisp HyperSpec | parent |
| Pool | pool-status / pool-kill-worker |
Diagnose or replace the session's worker | parent |
cl-spec (opt-in) |
spec-list / spec-symbol / spec-describe / spec-check |
List, read and check contracts | worker |
For every tool's input/output schema, see docs/tools.md.
Requirements
- SBCL 2.x (developed with SBCL 2.5.x)
- Quicklisp (optional; pure ASDF also works)
- Dependencies (via ASDF/Quicklisp): runtime —
alexandria,cl-ppcre,yason,usocket,bordeaux-threads,eclector,hunchentoot,named-readtables; tests —rove; optional —fiveam(for running FiveAM suites),clhs(loaded on-demand byclhs-lookuptool),swank(only forMCP_WORKER_SWANK),cl-spec(only for thecl-spectool group).
Quick Start
Install
# Install from Roswell
ros install cl-ai-project/cl-mcp
# or fetch from GitHub
cd ~/common-lisp
git clone https://github.com/cl-ai-project/cl-mcp.git
Load and run from an existing REPL:
(asdf:load-system :cl-mcp) ; or (ql:quickload :cl-mcp) if using Quicklisp
;; Start the HTTP server on port 3000 (the default)
(cl-mcp:start-http-server :port 3000)
;; Or start the TCP transport on port 12345, accepting clients until stopped
(cl-mcp:ensure-tcp-server-thread :port 12345)
Or run a minimal stdio loop (one JSON-RPC line per request):
# With Roswell
ros run -s cl-mcp -e "(cl-mcp:run :transport :stdio)"
# With a plain sbcl command
sbcl --eval '(require :asdf)' \
--eval '(asdf:load-system :cl-mcp)' \
--eval '(cl-mcp:run :transport :stdio)'
Project root: File operations require a project root to be set. The root
belongs to the session that sets it, with fs-set-project-root or with
initialize's rootPath/rootUri: several agents can share one server and
work on different projects, and none of them moves another's root.
MCP_PROJECT_ROOT, when set, is the default for a session that sets none. If it
is not set, a session's first file access returns an error prompting it to call
fs-set-project-root; AI agents handle this by setting it to their working
directory. A session's own root lasts as long as the session, so a reconnect -- a
new HTTP session or TCP connection -- starts from MCP_PROJECT_ROOT when it is set,
and otherwise without a root, and has to set its own again.
System Prompts
The prompts/ directory contains system prompts that teach AI agents how to
use cl-mcp tools effectively:
repl-driven-development.md— Tool selection guide, REPL-driven workflow (explore → experiment → persist → verify), editing patterns, and troubleshooting recipescommon-lisp-expert.md— Production-quality Common Lisp coding standards, CLOS/conditions/restarts conventions, and TDD with Rovecl-spec-driven-development.md— Contract-driven workflow for the optionalcl-spectool group: finding and writing Function Specs and Properties, baselines, seed replay, and readingspec-checkverdicts. Supplements the two prompts above rather than replacing them
cl-mcp also sends a short summary of the first, and of the third when the
cl-spec group is enabled, as its MCP server instructions (see Server
instructions), so a client that honours
them gets the essentials without any setup. Reference
the full prompts from your project's CLAUDE.md (or equivalent) when you want
the complete guide, or when your client ignores server instructions:
# CLAUDE.md
@prompts/repl-driven-development.md
@prompts/common-lisp-expert.md
If you enable the cl-spec tool group (see
Optional tool groups), add the third
prompt as well. Leave it out otherwise: without the group, the tools it
describes are not in the agent's tool list.
@prompts/cl-spec-driven-development.md
Claude Code
HTTP transport (recommended)
Start the HTTP server from your REPL and keep using it alongside Claude Code:
(asdf:load-system :cl-mcp) ; or (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)
Register it with Claude Code:
claude mcp add --transport http cl-mcp http://127.0.0.1:3000/mcp
or write the entry yourself, in the project's .mcp.json (or ~/.claude.json for all projects):
{
"mcpServers": {
"cl-mcp": {
"type": "http",
"url": "http://127.0.0.1:3000/mcp"
}
}
}
start-http-server also takes :token :generate (or a token string) to require an
Authorization: Bearer <token> header on every request; the generated token is printed
to *error-output*.
This approach lets both you and Claude Code share the same Lisp runtime. You can inspect state from SLIME/Sly while Claude Code works through MCP.
Stdio transport
For a simpler setup where Claude Code manages the server process directly:
{
"mcpServers": {
"cl-mcp": {
"type": "stdio",
"command": "ros",
"args": ["run", "-s", "cl-mcp", "-e", "(cl-mcp:run)"],
"env": {
"MCP_ENABLE_TOOL_GROUPS": "cl-spec"
}
}
}
}
The env entry is optional; it turns on the cl-spec tool group (see
Optional tool groups). Any variable from
Environment Variables can be passed the same way.
Stdio is easy to configure but the Lisp process is owned by Claude Code, making manual intervention from SLIME or another REPL difficult.
Codex
Configure in ~/.codex/config.toml.
Stdio transport
The simplest setup — Codex spawns and manages the server process:
[mcp_servers.cl-mcp]
command = "ros"
args = ["run", "-s", "cl-mcp", "-e", "(cl-mcp:run)"]
As with Claude Code's stdio mode, the Lisp process is owned by Codex and not easily accessible from SLIME or another REPL.
TCP transport with stdio bridge
Start a TCP server from your REPL, then point Codex at it via the bundled Python bridge that translates stdio ↔ TCP:
;; In your REPL: keep accepting clients, so Codex can reconnect after a restart
(asdf:load-system :cl-mcp)
(cl-mcp:ensure-tcp-server-thread :port 12345)
[mcp_servers.cl-mcp]
command = "python3"
# Codex starts the bridge from your project, not from the cl-mcp checkout,
# so give the script's absolute path.
args = ["/path/to/cl-mcp/scripts/stdio_tcp_bridge.py", "--host", "127.0.0.1", "--port", "12345"]
--host and --port default to 127.0.0.1 and 12345; the bridge also reads
LISP_MCP_HOST and LISP_MCP_PORT. (cl-mcp:start-tcp-server-thread serves a single
client and then stops unless given :accept-once nil.)
This gives you the same shared-REPL workflow as Claude Code's HTTP mode — you keep your SLIME/Sly session while Codex works through the bridge.
Worker Pool Isolation
Eval-dependent tools (repl-eval, load-system, run-tests, code-*,
clos-describe, lisp-macroexpand, inspect-object, and the optional spec-* tools)
run in isolated child SBCL processes. Each session gets
a dedicated worker with automatic crash recovery and circuit breaker protection.
File-system and editing tools run inline in the parent process, each request under
its own session's project root.
With the worker pool enabled, a worker crash loses all worker-local Lisp state
(loaded systems, definitions, packages, and REPL state). Any in-flight call
that observes the crash fails. The pool normally attempts recovery; if it
supplies a replacement, that worker starts with fresh Lisp state. The circuit
breaker can halt automatic recovery after repeated crashes. In diagnostic logs,
worker.crashed can initially report exit_status=running after TCP EOF; that
is a provisional snapshot. Use the later worker.reaped event for the final
process exit status.
For a pooled SBCL worker request, a condition that no existing handler resolves and that reaches the debugger path is observed and aborted as that request's structured failure. This preserves the worker for later requests, but it does not roll back side effects made before the failure. A process exit such as SB-EXT:EXIT remains a real worker crash and follows normal EOF/reaper recovery.
Disable the worker pool with MCP_NO_WORKER_POOL=1 or the :worker-pool keyword:
(cl-mcp:run :transport :stdio :worker-pool nil)
Environment Variables
| Variable | Purpose | Default |
|---|---|---|
MCP_PROJECT_ROOT |
Default project root for sessions that set none of their own | (not set: each session sets its own) |
MCP_LOG_LEVEL |
Log level: debug, info, warn (or warning), error; an unrecognized value is ignored |
debug |
MCP_LOG_FILE |
Also log to a file, named <stem>-<timestamp>-<pid>.<ext> beside the given path; workers do not inherit it |
(stderr only) |
MCP_NO_WORKER_POOL |
Any non-empty value (even 0) disables worker pool isolation |
(not set = pool enabled) |
MCP_ENABLE_TOOL_GROUPS |
Optional tool groups to enable, separated by commas or spaces, case-insensitive (cl-spec); read when the system loads. The :tool-groups argument of run, start-http-server, serve-tcp, start-tcp-server-thread and ensure-tcp-server-thread overrides it |
(empty: all groups off) |
MCP_WORKER_SWANK |
Any non-empty value makes each worker (ql:quickload :swank) and start a Swank server on a free port, reported in the worker.swank.started log event (needs Quicklisp in the worker; without it the worker runs without Swank) |
(off) |
CL_MCP_WORKER_POOL_WARMUP |
Number of standby workers to maintain (non-negative integer) | 1 |
CL_MCP_MAX_POOL_SIZE |
Maximum total workers, bound + standby (positive integer) | 16 |
MCP_WORKER_INIT_SYSTEM |
ASDF system to load in the elected owner worker at bind (master gate for the init hook) | (unset = off) |
MCP_WORKER_INIT_ENTRY |
PKG:SYMBOL nullary thunk run after the load (preferred activation) |
(none) |
MCP_WORKER_INIT_EVAL |
Lisp form run via repl-eval as an escape hatch | (none) |
MCP_WORKER_INIT_PACKAGE |
Package used to read/eval MCP_WORKER_INIT_EVAL |
CL-USER |
MCP_WORKER_INIT_MAX_FAILURES |
Soft init failures before init auto-retry latches off | 1 |
MCP_WORKER_INIT_MODE |
singleton (one owner binds a fixed port). v1 supports singleton only |
singleton |
Tuning warmup for cold-start handshakes
The worker pool warms its standbys asynchronously once cl-mcp:run
returns control to the MCP transport, so the stdio handshake is not
blocked on subprocess launches. If you still see your MCP client
report Failed to connect on a cold FASL cache — for example the
first cl-mcp invocation after a system upgrade — set
CL_MCP_WORKER_POOL_WARMUP=0 to skip pre-spawning entirely. Workers
are then created on demand the first time a session needs one,
trading first-eval latency for the smallest possible startup
footprint. Once your client establishes the session you can ignore
the warmup; the pool will grow as sessions arrive.
Running an app inside a worker (init hook)
Set MCP_WORKER_INIT_SYSTEM (and optionally MCP_WORKER_INIT_ENTRY) to have
the pool run a startup routine inside the single worker elected as the
"runtime owner" when a session binds it. This lets an app (e.g. a web server)
run in the same process that serves repl-eval/load-system, so hot-reload
lands in the app's process, while the parent keeps the persistent /mcp
endpoint. pool-kill-worker restarts the runtime without dropping /mcp.
- The init runs on a background thread under a worker-global ASDF load lock,
so it never races a
load-systemRPC. MCP_WORKER_INIT_SYSTEMis loaded the way theload-systemtool loads: a file that compiles with a fullWARNINGis refused, asasdf:load-systemrefuses it, and the init fails.ql:quickloadmuffles such warnings, so an app that has only ever been quickloaded can meet this the first time its files are compiled here;load-systemon it names the warning and its place.- Init failures never trip the crash circuit breaker; a failed init leaves a
plain, usable REPL worker. Check
pool-status(init_owner_session,init_disabled,init_failures) to see runtime state. - Requires the pool enabled (do not set
MCP_NO_WORKER_POOL=1); cl-mcp warns at startup if the init vars are set while the pool is disabled. - The init form must be bind-robust and idempotent, and should pass
:address "127.0.0.1"for a localhost-only dev server. Do not embed secrets inMCP_WORKER_INIT_EVAL. - Reloading code with
load-system :force twhile the app is serving live requests can race in-flight request threads (class/method redefinition); reload between requests or quiesce the app's acceptor first.
Example (recurya): add a nullary recurya/dev:start-dev-runtime! thunk that
starts the DB and web server, then set MCP_WORKER_INIT_SYSTEM=recurya/dev
and MCP_WORKER_INIT_ENTRY=recurya/dev:start-dev-runtime!.
Known v1 limitations:
- Single dev runtime / single session.
singletonmode is designed for one developer session driving one fixed-port app. Ownership is bound to the owner session and never migrates to a different live session, so hot-reload stays in the app's process. A narrow multi-session race remains (if the owner's worker crashes during the same window that a second session binds), so two concurrent sessions against one init runtime is not supported in v1; a future per-worker/ephemeral-port mode is planned for that. - Reset is not instantaneous.
pool-kill-workerre-arms the runtime, but the app is re-initialized lazily on the session's next tool call (not eagerly), so the app is briefly down after a reset until the next MCP request. The parent/mcpendpoint stays up throughout. - Soft-failure retry is worker-scoped.
MCP_WORKER_INIT_MAX_FAILUREScounts init failures across a session's worker (re)binds (e.g. successive hard-crash recoveries). A soft init failure (the app reports an error but the worker stays alive) keeps the runtime owned by that session (no migration) but is not auto-retried on the same worker — recover it withpool-kill-worker.
Security Model
cl-mcp is a trusted, localhost-only development tool. repl-eval executes
arbitrary Common Lisp code in the host image, so any client with MCP access has
full system privileges. Do not expose the endpoint beyond localhost. File
operation restrictions (project root enforcement) are convenience guardrails
to prevent accidental mistakes, not security boundaries.
Because a request that reaches it is code execution, the HTTP transport refuses
what a web page could send it, as the MCP Streamable HTTP transport requires:
a request whose Origin is not a loopback origin (403), a request whose Host
does not name the loopback interface while the server listens only on it
(403; this is what stops DNS rebinding), and a POST that is not
Content-Type: application/json (415). Clients that are not browsers send no
Origin and are unaffected. A server started on a non-loopback address was
exposed on purpose, and its Host is not checked; its Origin still is, since
a page rebound to its LAN address sends an Origin matching that Host.
Input that would drive a recursive parser off the end of a thread's stack is
refused before any parser sees it. SBCL cannot always recover from an exhausted
stack, and when it cannot, the whole server stops. A JSON message nested more
than 1000 levels deep is a parse error (-32700). A worker's answer that deep
comes back as an error instead of being read: ask for a shallower preview_max_depth or max_depth. Lisp source
that would make the reader recurse more than 500 levels is refused by the tools
that read it, whether it is a file, tool content or repl-eval code. That
covers nested lists and also chains of prefixes such as ', #' and #+.
The HTTP transport has no authentication by default. start-http-server's
:token argument (:generate or a string) makes every request other than
OPTIONS carry Authorization: Bearer <token>.
Workers are child processes of the server and inherit its whole environment,
API keys and other secrets included: code run through repl-eval can read them.
Only the MCP_WORKER_INIT_* variables and MCP_LOG_FILE are withheld (the worker's own
MCP_* control variables are set fresh). A worker
listens on 127.0.0.1 and answers only the parent, which proves itself with a
per-worker shared secret.
License
MIT
README fetched from the repository 3 days ago. The original is authoritative.