MCP Server Guide¶
When to use this: You want to query a little-loops project's state — issues, dependency health, session history — from an MCP-capable client such as Claude Code, Codex, or Claude Desktop. This guide covers installing, registering, verifying, and troubleshooting the
ll-mcpstdio server. For the authoritative tool parameter and response schemas, see CLI Reference §ll-mcp— this guide deliberately does not restate them.
Contents¶
- What
ll-mcpIs - Install
- Pointing the Server at a Project
- Registering the Server
- Verifying with
mcp-call - Resources and Prompts in Practice
- Adding a Tool
- The Mutation Surface and Its Guards
- Polling and Stopping a Run:
tasks/* - Troubleshooting
- See Also
What ll-mcp Is¶
ll-mcp is an MCP server (stdio by default, streamable HTTP with --http) that exposes
a little-loops project over the Model Context Protocol. It advertises three surfaces:
| Surface | What it gives a client |
|---|---|
| Tools (read) | issues_query, issue_get, history_search, deps_check, capabilities, queue_list, queue_get, loop_list, skills_list |
| Tools (write) | issue_capture, issue_set_status, issue_link, issue_append_log, queue_add, queue_remove, queue_requeue — dry-run by default, see below |
| Resources | Issue files, .ll/ll-goals.md, and docs/**/*.md under an ll:// scheme; one interactive ui://issues/view MCP Apps resource (ENH-3306) |
| Prompts | Every discovered SKILL.md, listed as an invocable MCP prompt |
| Tasks | tasks/get / tasks/cancel — poll or stop an in-flight ll-loop run, see below |
It is launched by a host, never by hand — it speaks JSON-RPC on stdin/stdout and prints
nothing useful to a terminal. Each tool wraps a little_loops library call directly: no
subprocess, no ll-* CLI shelling out, no orchestration.
Install¶
The MCP server lives behind an optional extra, because the mcp SDK is a heavyweight
dependency that most little-loops users never need:
Without the extra, ll-mcp exits 2 with ll-mcp requires themcpextra rather than
an ImportError traceback. Because hosts usually swallow a server's stderr, this failure
typically surfaces only as "server failed to start" in the client — run ll-mcp directly
in a terminal to see the real message (it will hang waiting for JSON-RPC input if the
extra is installed; Ctrl-D to exit).
Pointing the Server at a Project¶
ll-mcp resolves the project it serves in this order:
--project-root /abs/path— an argument on the server command.LL_MCP_PROJECT_ROOT=/abs/path— the equivalent for hosts whose server config exposes anenvblock but notargs.- The process's current working directory — the fallback when neither is set.
This matters because MCP hosts vary in what cwd they spawn a server with. A client that
launches its servers from $HOME starts ll-mcp against $HOME unless told otherwise,
and every tool then answers truthfully about a project that does not exist there —
issues_query returns [], deps_check reports a clean graph, resources/list is empty,
history_search finds nothing. Setting the root explicitly is the reliable fix:
Claude Code and Codex both launch project-scoped servers from the project root, so the
ll-adapt-generated configs below need no root argument. For other clients, --project-root
is the recommended form; a cwd field in the server config, or a shell wrapper
({"command": "sh", "args": ["-c", "cd /abs/path && exec ll-mcp"]}), also work if you
prefer them.
When the root is wrong, the server says so. A resolved root with neither a .ll/ nor
an .issues/ directory produces a warning on stderr at startup, naming the root it
resolved. Because hosts commonly swallow a server's stderr, the same signal is also carried
by the capabilities tool — the first thing worth calling when verifying a new
registration:
resolved: false means the tool surface will answer empty about everything.
Binding the HTTP transport¶
The streamable HTTP transport (--http, or LL_MCP_TRANSPORT=http) binds
127.0.0.1:8765 by default. Override it with --host / --port, or persistently in
.ll/ll-config.json; the flags win over the config block:
No path here defaults to 0.0.0.0 — a non-loopback bind is always something you asked
for. When you do ask for one, the server widens TransportSecuritySettings'
allowed_hosts/allowed_origins to that host (the SDK auto-fills that allow-list only for
loopback, and would otherwise reject every request's Host/Origin header). That is
DNS-rebinding protection, not authentication — the HTTP transport ships with none, which
is why mcp.transport_policy.http denies mutations and tasks by default regardless of what
you bind to. See Guard 2.
Registering the Server¶
Claude Code¶
This merges an ll-mcp entry into .mcp.json at the project root, preserving any
existing mcpServers content:
Codex¶
The same run that bridges skills, commands, and agent personas into Codex also merges an
[mcp_servers.ll-mcp] table into Codex's global ~/.codex/config.toml (or
$CODEX_HOME/config.toml) — Codex has no project-local MCP config read path, so this is
the one emitter that writes outside the project directory:
Claude Desktop and other MCP clients¶
No emitter exists for these yet — ll-adapt --host <other> skips the MCP artefact — so
write the config by hand. Claude Desktop reads
~/Library/Application Support/Claude/claude_desktop_config.json on macOS:
{
"mcpServers": {
"ll-mcp": {
"command": "/abs/path/to/venv/bin/ll-mcp",
"args": ["--project-root", "/abs/path/to/project"]
}
}
}
Use an absolute path to the ll-mcp executable. A GUI-launched client does not
inherit your shell's PATH, so a bare "ll-mcp" that works from a terminal will fail
there with a spawn error. which ll-mcp gives you the path to paste.
One entry per project: a server instance serves exactly one root, so a second project
means a second mcpServers key (ll-mcp-otherproject) with its own --project-root.
Verifying with mcp-call¶
mcp-call is a thin JSON-RPC client that ships with little-loops. It reads .mcp.json
from the current directory, spawns the named server, performs the handshake, calls one
tool, and prints the response envelope — the fastest way to confirm the server works
before blaming the host.
Its exit code tells you which layer failed: 0 success, 1 tool error, 124 timeout,
127 server or tool not found, 2 config/usage error.
The nine read tools, end to end¶
# Open issues, filtered and sorted by priority
mcp-call ll-mcp/issues_query '{"issue_type": "EPIC", "limit": 2}'
[
{
"id": "EPIC-2790",
"priority": "P1",
"type": "EPIC",
"title": "Subprocess and MCP Robustness",
"path": "/abs/path/.issues/epics/P1-EPIC-2790-subprocess-and-mcp-robustness.md",
"status": "open",
"parent": null,
"labels": []
}
]
# One issue's full summary card — accepts 3122, FEAT-3122, or P3-FEAT-3122
mcp-call ll-mcp/issue_get '{"issue_id": "3122"}'
{
"issue_id": "FEAT-3122",
"title": "ll-doctor advisor-reachability check",
"priority": "P3",
"status": "Open",
"raw_status": "open",
"confidence": "50",
"outcome": "58",
"path": ".issues/features/P3-FEAT-3122-advisor-ll-doctor-reachability-check.md"
}
Note the two status fields: status is display-cased for rendering, raw_status is the
frontmatter value. Match on raw_status in any automation.
[
{
"content": "**/.handoff*",
"kind": "file",
"ref": "**/.handoff*",
"anchor": "Glob",
"ts": "2026-06-04T06:48:07Z",
"score": -9.998166573997038
}
]
score is FTS5 BM25: more negative is a better match. An empty result is normal on a
young project — .ll/history.db only fills as sessions accumulate.
{
"has_issues": true,
"broken_refs": [],
"missing_backlinks": [["ENH-2997", "ENH-2991"]],
"cycles": [],
"stale_completed_refs": [["FEAT-2102", "FEAT-1932"]],
"broken_depends_on_refs": [],
"broken_relates_to_refs": []
}
has_issues means "this project has issue files", not "problems were found" — read the
individual lists for that.
{
"host": "claude-code",
"binary": "claude",
"version": "",
"capabilities": [
{"name": "streaming", "status": "full", "note": ""},
{"name": "claude_md_suppression", "status": "unsupported", "note": "the claude CLI has no flag to skip CLAUDE.md"}
]
}
This reports the host little-loops itself would drive for automation (per
LL_HOST_CLI / orchestration.host_cli), not the MCP client you are calling from.
[
{
"id": "a1b2c3d4e5f6",
"action": { "name": "rn-refine", "runner": "loop", "target": "FEAT-3122", "args": [], "timeout": null },
"enqueuedAt": "2026-08-20T10:00:00Z",
"priority": 0,
"status": "queued",
"result": null,
"claimedAt": null,
"ownerPid": null,
"attempt": 0,
"nextAttemptAt": null
}
]
{
"id": "a1b2c3d4e5f6",
"action": { "name": "rn-refine", "runner": "loop", "target": "FEAT-3122", "args": [], "timeout": null },
"enqueuedAt": "2026-08-20T10:00:00Z",
"priority": 0,
"status": "queued",
"result": null,
"claimedAt": null,
"ownerPid": null,
"attempt": 0,
"nextAttemptAt": null
}
[
{
"name": "rn-refine",
"path": "/abs/path/loops/rn-refine.yaml",
"category": "refine",
"labels": ["autonomous"],
"visibility": "public",
"description": "Refine an issue until implementation-ready."
}
]
[
{"name": "manage-issue", "kind": "skill", "description": "Implement an issue end-to-end.", "args": "ISSUE_ID"},
{"name": "commit", "kind": "command", "description": "Create a git commit."}
]
queue_list/queue_get mirror ll-queue list/ll-queue status; loop_list mirrors
ll-loop list — each returns the same JSON shape as its --json CLI form. skills_list
(ENH-3444) has no CLI equivalent — it anchors at the plugin root the engine itself
resolves rather than --project-root, so every returned name classifies identically
under queue_add; args is omitted when a skill/command has no frontmatter hint.
Resources and Prompts in Practice¶
Both surfaces are enumerated once at startup, but the enumeration self-heals within the
same session (ENH-3172): every resources/list, resources/read, prompts/list, and
prompts/get call cheaply checks whether the watched directories (issue category dirs,
.ll/ll-goals.md, docs/, the skills root) changed since the last build, and rebuilds
the index first if so. A newly created issue becomes readable as ll://issues/<ID> on
the next call after it's written — no restart needed — and a deleted or renamed one stops
being advertised the same way. The same applies to a newly added SKILL.md and the
prompts list.
Two caveats follow from how that check works:
- The check is a directory mtime, not a recursive walk — a change directly inside a
watched directory is detected; a change two or more levels down (a new
SKILL.mdnested under an existing skill subdirectory, a new file in an existingdocs/subdirectory) is not. A subsequent unrelated top-level change will still pick it up. - The rebuild happens lazily, on the next call to one of the four methods above — not on
a background timer. A client that only ever calls
issues_query/issue_getand never touches the resource/prompt surface won't trigger a rebuild, but it also isn't observing the stale data either. - Clients that respect the server's 5-minute
publicCacheHinton these methods (and don't listen for change events) may still serve their own cached copy of a pre-rebuild response for up to 5 minutes — a client-side cache the server can't reach into. A client that opens asubscriptions/listenstream (resources_list_changed=true/prompts_list_changed=true) gets anotifications/resources/list_changed/notifications/prompts/list_changedevent the moment a rebuild happens and should evict its cache and re-fetch, rather than waiting out the window.
Two more practical notes:
- The resource list is bounded and paginated (ENH-3174). On a mature project it is
still one entry per issue plus one per file under
docs/— this repository enumerates over 3,000 — butresources/listnow caps each response atmcp.resources.page_sizeentries (default 500) and returnsnextCursorwhen more remain; pass that value back ascursoron the next call to page through the rest. This is unconditional — it applies whether or not the config below is set. An operator can also narrow what gets enumerated at all via.ll/ll-config.json:{ "mcp": { "resources": { "issue_statuses": ["open", "in_progress"], "docs_globs": ["guides/*.md", "reference/*.md"], "page_size": 200 } } }issue_statuses/docs_globsdefault tonull(unset), which enumerates every issue status and everydocs/**/*.mdfile — identical to pre-ENH-3174 behavior, so an existing client sees no change unless this block is added. Either way, preferissues_queryto find an issue andresources/read(orissue_get) to fetch the one you want — the resource list is for browsing/discovery, not the fast path. - Prompts serve the full skill catalog on every install source (BUG-3177), not only a
plugin checkout. The skills root is resolved in order:
$LL_MCP_SKILLS_ROOTif set and valid, then$CLAUDE_PLUGIN_ROOT/skillsif set and valid, then the copy shipped inside the installedlittle_loopspackage (present on bothpypiandlocal-editablewheel/sdist builds), then the checkout-relative fallback (editable installs run straight from source). If none of those resolve, the server logs anERROR:line on stderr naming every path it tried and serves an empty prompt list rather than failing silently — check the host's server log for that line ifprompts/listcomes back empty unexpectedly. SetLL_MCP_SKILLS_ROOTin the server'senvblock to point at an arbitrary skills directory (e.g. a different checkout) if you need to override the default resolution.
Resource and prompt listings carry ttlMs/cacheScope cache hints (5 minutes, public),
so a well-behaved client will not re-enumerate on every request.
The ui:// interactive-resource scheme (ENH-3306)¶
Alongside the ll:// data resources, resources/list always advertises one
MCP Apps–compliant interactive resource: ui://issues/view, with
mimeType: "text/html;profile=mcp-app". issue_get links to it via
_meta.ui.resourceUri on its tool definition, so a client that negotiated the
io.modelcontextprotocol/ui extension at initialize can resources/read the view and
render it inline instead of raw JSON. Clients that never negotiated that capability just
never resolve it — it costs an unconditional resources/list entry, nothing more.
Unlike every other resource kind, ui://issues/view:
- Is static package data, not a project file — it's read via
importlib.resourcesfrom inside the installed wheel, not fromconfig.project_root, and is therefore never in_watched_paths()/ never goes stale from a project-side edit. - Carries no resource-level
_meta.ui(no CSP or permissions declaration) — the template is fully self-contained (no external stylesheets, fonts, images, orfetch/XHR), so the host's default sandbox applies. - Speaks a minimal view-side handshake only: it renders
ui/notifications/tool-result(readingstructuredContent, falling back toJSON.parse(content[0].text)), tears down cleanly onui/resource-teardown, and emits onlyui/message— level 1 ("notify") on the Artifact Control Levels taxonomy. It never issuestools/callback into the FSM; level-2/3 interactions are out of this scope.
Adding a Tool¶
Never let a handler print. On the stdio transport, stdout is the JSON-RPC frame —
anything a handler writes to stdout (directly, or by calling a cmd_* CLI function that
prints its result) corrupts the protocol. The client-visible symptom is a JSON parse error
that points nowhere near the offending tool, so this is the single most likely defect when
adding a tool.
Two mitigations cover every case in this codebase today:
- Prefer extracting a non-printing library function.
_tool_issue_set_statusand_tool_issue_link(mcp_server/tools.py:317-420) never callcmd_set_status/cmd_link— they callapply_status_transition/apply_link, the non-printing functions FEAT-3149 extracted from thosecmd_*implementations for exactly this reason. When the CLI function you're wrapping still prints, extract its logic first rather than wrapping the printing function. redirect_stdout/redirect_stderrwhen extraction isn't practical._tool_loop_start(mcp_server/tools.py:699-705) wrapsrun_background()— which prints on both its success and pre-flight-failure paths — incontextlib.redirect_stdout/redirect_stderr, reading the captured stderr back to build an error message on non-zero return. Reach for this only when the wrapped call is otherwise unsafe to extract.
Registration checklist for a new tool:
- Add the handler function and register it in
_TOOL_HANDLERS(mcp_server/tools.py). - Add its
types.Tooldefinition to_TOOLS, in the same source order as_TOOL_HANDLERS. No test asserts_TOOL_HANDLERS/_TOOLSparity — a tool registered in one but missed in the other is caught only if some test happens to call it, not by a dedicated gate, so double-check both by hand. - If the tool writes, add its name to
policy.MUTATING_TOOLS— neverTASK_STARTING_TOOLS, which is reserved forloop_start's "start a run" semantics (no coherent dry-run, gated byallow_tasksinstead ofallow_mutations). A mutating tool gets Guard 1'sapplydry-run wrapper automatically once it's inMUTATING_TOOLS; a tool omitted from both registries is unguarded — reachable and able to write with no dry-run and no per-transport policy check.
The Mutation Surface and Its Guards¶
Four tools write: issue_capture, issue_set_status, issue_link, and
issue_append_log. Each wraps the same library function the equivalent ll-issues
subcommand calls, so a tool call and a CLI invocation produce the same file state.
ll-auto, ll-parallel, and ll-action invoke are still off the surface entirely. ll-loop
is the one exception: loop_start (below, alongside tasks/*) starts a detached run.
Everything else about the boundary is unchanged — tasks/cancel is a control operation over
a run that is already going, signalling an existing PID, never spawning one.
Two guards sit in front of the four.
Guard 1 — dry-run by default¶
Every mutating tool takes an apply parameter that defaults to false. Called without
it, the tool returns the change it would make and writes nothing:
// tools/call issue_set_status {"issue_id": "FEAT-3149", "status": "deferred"}
{
"applied": false,
"tool": "issue_set_status",
"target": { "issue_id": "FEAT-3149", "path": ".issues/features/P3-FEAT-3149-….md" },
"changes": [ { "field": "status", "from": "open", "to": "deferred" } ]
}
Re-call with "apply": true to perform it. The default is a refusal to mutate, not an
opt-out flag: a host that omits the parameter entirely does not write, and the check is
fail-closed — only the literal boolean true opts in. "true", 1, and null are all
dry-runs.
One shape differs. A dry-run issue_capture returns no issue ID, not even a predicted
one — it reports the type, priority, slug, target directory, and rendered body instead.
The ID is allocated inside create_issue's lock hold at write time, so any ID named
before apply is a guess that is wrong precisely when it matters: when something else
allocated concurrently. The apply response carries the real one.
Guard 2 — per-transport policy¶
Whether the mutating tools may run at all is a deployment choice, set per transport in
.ll/ll-config.json:
{
"mcp": {
"transport_policy": {
"http": { "allow_mutations": false },
"stdio": { "allow_mutations": true }
}
}
}
Those are the defaults. HTTP denies mutations because that transport ships without authentication, so the posture for a transport a remote host can reach is read-only until someone opts in; stdio is a same-machine, same-user channel and defaults open. One server build serves both.
A denied call is refused at the transport layer, before the JSON-RPC body is parsed —
ASGI middleware reads the SEP-2243 Mcp-Method / Mcp-Name routing headers off the raw
request and answers with a JSON-RPC error and HTTP 403. Reads on the same server are
unaffected:
$ # with http.allow_mutations = false
$ mcp-call ll-mcp tools/call issue_set_status '{"issue_id":"FEAT-1","status":"done"}'
{"jsonrpc":"2.0","id":null,"error":{"code":-32001,"message":"policy denied tools/call/issue_set_status: …"}}
$ mcp-call ll-mcp tools/call issues_query '{}'
[ … works fine … ]
This is sound against a spoofed header even though the middleware never sees the body: the
SDK independently rejects any request whose Mcp-Method/Mcp-Name disagree with its body
(HEADER_MISMATCH, -32020), and both headers are mandatory for tools/call. A request
cannot reach a mutating handler while hiding its identity from the guard.
Distinguishing the two groups from a client¶
The seven mutating tools carry a readOnlyHint: false annotation in tools/list; the nine
read-only tools carry no annotations at all. A host can key presentation — a confirmation
prompt, a different icon — off that.
Starting, Polling, and Stopping a Run¶
loop_start starts a detached ll-loop run; tasks/get and tasks/cancel poll or stop
it (or a run started by other means, e.g. ll-loop run on the workstation). All three
share one grant (allow_tasks, below) and one identifier space (instance_id), so a host
can start a run from a phone-side session and poll/stop it from a workstation session
without SSH-ing anywhere. ll-queue is out of scope: only ll-loop runs are reachable
this way.
Starting a run: loop_start¶
$ mcp-call ll-mcp tools/call loop_start '{"loop": "rn-refine", "context": ["ISSUE_ID=FEAT-3151"]}'
{"instance_id": "rn-refine-20260814T160000-a1b2", "loop": "rn-refine"}
loop_start always performs the identical detached spawn — the same one ll-loop run
--background does — regardless of caller. What differs is the response shape, per
SEP-2663 (the MCP "tasks" extension): a client that declared the tasks extension in its
per-request capabilities and asked for task-augmented execution on that call gets back a
task-shaped result instead:
$ # client declares the tasks extension and sets params.task on this call
{"resultType": "task", "taskId": "rn-refine-20260814T160000-a1b2", "status": "working"}
Any other client — one that never declared the extension, declared it but did not set
params.task on this call, or is on a pre-2026-07-28 protocol version — gets the ordinary
shape above, instance_id and all. Either way the run started; only the envelope changes.
The task id is the run's instance_id verbatim (minted with a short entropy suffix, not
ll-loop's own one-second-resolution id, to stay unique under agent-paced calls) and is
what tasks/get/tasks/cancel accept below.
If the run cannot be spawned — a scope conflict, an unloadable loop — the call returns an
ordinary tool error (isError: true) carrying the reason. It never returns a task id or an
instance_id for a run that does not exist.
loop_start is not one of the seven mutating tools above: a dry-run "start" has no
coherent meaning, so it takes no apply parameter and is gated by allow_tasks (below)
instead of allow_mutations.
Polling and stopping: tasks/*¶
These are not tools — they are custom JSON-RPC methods registered directly on the server,
shaped to track the (not-yet-shipped) io.modelcontextprotocol/tasks extension so a
future swap to the official mechanism is a registration change, not a client-visible one.
initialize's capabilities never advertise the extension itself — the server does not
claim a capability it only implements privately.
taskId is the ll-loop instance_id verbatim — the same string ll-loop status
already prints — not a handle minted by the server:
$ mcp-call ll-mcp tasks/get '{"taskId": "rn-refine-20260811T140000"}'
{"taskId": "rn-refine-20260811T140000", "status": "working", "runStatus": "running"}
The status field reconciles PID liveness before ever reporting "working" — a run whose
process died (OOM, kernel kill) without updating its state file is reported not-running,
not left "working" forever. When no PID is resolvable at all, an updated_at-age
fallback (6h threshold) catches permanently PID-less orphans the same way (BUG-3317).
Once the run is terminal, the result also carries the
ExecutionResult fields (final_state, iterations, terminated_by, duration_ms,
captured):
$ mcp-call ll-mcp tasks/get '{"taskId": "rn-refine-20260811T140000"}'
{"taskId": "…", "status": "completed", "runStatus": "completed", "final_state": "done", "iterations": 12, "terminated_by": "completed", "duration_ms": 483000, "captured": { … }}
An unknown taskId is a distinct JSON-RPC error, never a default "working" shape:
$ mcp-call ll-mcp tasks/get '{"taskId": "no-such-run"}'
{"jsonrpc":"2.0","id":null,"error":{"code":-32002,"message":"no run found for taskId 'no-such-run'"}}
tasks/cancel stops a running instance the same way ll-loop stop does — SIGTERM, then
SIGKILL after a 10s grace period. Neither backend has a genuinely terminal "cancelled"
status, so the result never reports "cancelled" bare: resumable and the backend's raw
status ride alongside, so a host cannot mistake a resumable stop for an irreversible one:
$ mcp-call ll-mcp tasks/cancel '{"taskId": "rn-refine-20260811T140000"}'
{"taskId": "…", "status": "cancelled", "resumable": true, "runStatus": "user_stopped"}
tasks/* and loop_start get the same deny-by-default-on-HTTP treatment as the mutating
tools (Guard 2 above), but as their own grant — allow_tasks, not allow_mutations.
Consenting to issue-file writes over HTTP does not imply consenting to starting or
stopping a running agent; starting one is the same class of authority as stopping it, so
both sit behind one grant:
{
"mcp": {
"transport_policy": {
"http": { "allow_mutations": false, "allow_tasks": false },
"stdio": { "allow_mutations": true, "allow_tasks": true }
}
}
}
Those are the defaults — both closed on HTTP, both open on stdio. A denied tasks/get
reports itself as a tasks/get denial (not a tools/call one), and a denied loop_start
reports itself as a tools/call/loop_start denial, since the underlying guard is shared
with Guard 2 but every method/tool is gated independently.
Enforcement is uniform across both transports: the tools/call and tasks/* handlers
themselves consult the policy (in addition to the HTTP transport's ASGI middleware, which
still denies before the JSON-RPC body is parsed on that transport), so a denied call over
stdio returns the same -32001 JSON-RPC error the HTTP path returns.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Client reports the server failed to start | mcp extra not installed |
pip install "little-loops[mcp]"; run ll-mcp in a terminal to see the real stderr |
Spawn error / command not found, but ll-mcp works in your shell |
GUI client does not inherit shell PATH |
Use the absolute path from which ll-mcp in the config |
| Every tool succeeds but returns empty results | Server resolved the wrong project root (spawned from $HOME, no root given) |
Call capabilities and check project_root.resolved; add --project-root /abs/path (or LL_MCP_PROJECT_ROOT) — see above |
A new issue is missing from resources/list but issue_get finds it |
Client is serving its own cached resources/list response (5-minute public CacheHint) |
Call again with cache bypassed, or listen for notifications/resources/list_changed; see Resources and Prompts in Practice — the server itself self-heals on the next call |
prompts/list is empty |
Skills root failed to resolve (rare — the wheel ships its own copy); check stderr for the ERROR: no skills directory found line |
Set LL_MCP_SKILLS_ROOT to a valid skills directory in the server's env |
history_search always returns [] |
.ll/history.db absent or empty under the resolved root |
Confirm <project-root>/.ll/history.db exists (history accrues over sessions); check capabilities for the root the server actually resolved |
mcp-call exits 127 |
.mcp.json missing from cwd, or no ll-mcp key in it |
Run mcp-call from the project root; ll-adapt --host claude-code --apply |
mcp-call exits 124 |
Server started but never answered | Check for a stale install: pip show little-loops, then re-run ll-adapt |
For anything protocol-level, run the server by hand and speak to it directly — three lines of JSON on stdin is a complete session:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"debug","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| ll-mcp
See Also¶
- CLI Reference §
ll-mcp— authoritative tool parameters, response shapes, and the resource/prompt surface contract - CLI Reference §
ll-adapt— the host adapter that emits MCP config - API Reference §
little_loops.mcp_server— module-level internals - Host Compatibility — which hosts support what
- Issue Management Guide — the write path the MCP surface deliberately omits
- Artifact Control Levels — the three-level
control taxonomy any interactive resource this server exposes must conform to
(ENH-3306 fills in the concrete
ui://resource details)