lean-coder has a generic MCP client in the core.
Add a server and its tools join the model's tool list as mcp__<server>__<tool>. No
servers are configured by default, so MCP adds no tools or context until you add one.
A lean-tool is a .py file you write; an MCP server is an external
process or endpoint you connect to.
/mcp add fs npx -y @modelcontextprotocol/server-filesystem /some/dir # a stdio server
/mcp add gw https://mcp-gateway.example.com/mcp/handbook/mcp # an HTTP server
/mcp # enable/disable menu (per server, live)
/mcp list # configured servers + connection state
/mcp reconnect [name] # (re)connect all enabled, or just one
/mcp remove <name> # forget a server
/mcp clean [name] # drop stale entries (enabled but never defined)
/mcp add <name> <spec> infers the transport from the spec: a http(s):// URL ->
HTTP; anything else -> a stdio command line. The server is saved to your config
and enabled immediately; its tools appear on the next turn.
Guided auth: if an HTTP server you add or reconnect answers 401/403, lean-coder
offers an inline walkthrough: pick OAuth 2.1 or static bearer, answer a few prompts,
and it configures auth and reconnects. With a gateway that supports dynamic client
registration no manual client setup is needed; see
Guided OAuth (auto-registration) below. No TTY (piped /
headless)? It prints the exact config block to add instead.
Every subcommand also answers /mcp ? with its full help.
Both are stdlib-only (no mcp SDK, no pip packages). The HTTP transport and OAuth
require curl on PATH.
A subprocess speaking newline-delimited JSON-RPC 2.0 over stdin/stdout. Configured with command + args + env:
[mcp_servers.fs]
transport = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/some/dir"]
# env = { SOME_TOKEN = "..." } # merged over the current environmentThe streamable-HTTP MCP flow: initialize -> capture the Mcp-Session-Id header ->
notifications/initialized -> tools/list / tools/call. Responses may be SSE
(event: message / data: {…}) or plain JSON - both are handled. Session-less servers
(no Mcp-Session-Id) work too.
[mcp_servers.gw]
transport = "http"
url = "https://mcp-gateway.example.com/mcp/handbook/mcp"Auth reduces to a single Authorization: Bearer <token> header. Two ways to produce it:
Prefer OAuth 2.1 where the gateway offers it. A static bearer token is a long-lived secret: if it leaks it works until someone rotates it by hand. OAuth 2.1 client credentials issue a short-lived JWT (typically ~1h) that lean-coder fetches, caches, and refetches automatically, so a leaked token expires on its own and the client secret never rides on the wire per request. Use a static bearer only when the server has no OAuth endpoint.
[mcp_servers.gw]
transport = "http"
url = "https://…/mcp/handbook/mcp"
auth = { type = "bearer", token_env = "GW_KEY" } # or: token = "literal-token"Prefer token_env (read from the environment at call time) over a literal token in
the file, so no secret is written to config.toml.
For a gateway that issues short-lived JWTs. lean-coder fetches a token from token_url,
caches it, and refetches automatically on a 401:
[mcp_servers.gw]
transport = "http"
url = "https://…/mcp/handbook/mcp"
auth = { type = "oauth", token_url = "https://…/oauth/token", client_id = "my-client", client_secret_env = "GW_SECRET", scope = "mcp:access" }client_secret_env (env lookup) is preferred; client_secret (literal) is accepted.
scope is optional. Both bearer-static and oauth end as the same one Bearer header,
so a gateway that accepts either works with both.
The oauth block above can be set up by the guided walkthrough on a 401/403,
including dynamic client registration (DCR, RFC 7591). Choosing OAuth in the prompt does:
- Discovery - fetches the gateway's
/.well-known/oauth-protected-resource(RFC 9728) then/.well-known/oauth-authorization-server(RFC 8414) to find thetoken_endpointandregistration_endpoint. - Registration - if a
registration_endpointexists, it POSTs a client-credentials registration (passing a registration key as a Bearer header if the endpoint is guarded- it asks you for one, blank if the endpoint is open) and gets back a
client_id+client_secret.
- it asks you for one, blank if the endpoint is open) and gets back a
- Storage - writes those to a mode-0600 sidecar at
~/.config/leancoder/mcp_auth/<server>.json, and setsconfig.tomlto justauth = { type = "oauth" }. The secret never touchesconfig.toml. - Connect - fetches the first token, caches it (with its expiry) back into the sidecar, and reconnects.
If the gateway has no DCR endpoint, the walkthrough falls back to asking for the
token_url / client_id / secret manually; you can put the secret in an env var
(client_secret_env) or let lean-coder store it in the same 0600 sidecar.
Where secrets live: Anything you supply (a static bearer token, a
pre-existing OAuth client secret) belongs in an environment variable referenced by
token_env / client_secret_env - never written to disk by lean-coder. Anything
lean-coder mints itself (a DCR client secret, cached access tokens) goes in the
0600 sidecar under the config dir, the same convention providers use for their
login files. Either way, config.toml stays free of secrets.
The sidecar (~/.config/leancoder/mcp_auth/<server>.json) holds
{client_id, client_secret, token_endpoint, registration_endpoint, scope} plus the
cached {access_token, expires_at}; delete the file to force a fresh registration.
- Naming. A server
gwexposingsearch_handbooksurfaces asmcp__gw__search_handbook. The namespace keeps MCP tools from colliding with core tools, lean-tools, or each other. - Driver-only. MCP tools always run on the driver (the machine running
lean-coder), never on a
/connect-ed remote executor. The subprocess/endpoint lives where lean-coder does. - Leash tier. MCP tools may have side effects, so they ride the
rwetier (like a non-safelean-tool) - available only at therweleash, and they confirm before running unless approval is armed (/approve sessionorauto). - Startup. Enabled servers connect at launch. A server that fails to connect just
contributes no tools; its error is shown by
/mcp list(and you can/mcp reconnectit once fixed). A dead server never blocks the loop. - Results. A tool result's
structuredContentis preferred, else text content blocks are joined; anisErrorresult is prefixed[tool error].
Two keys in config.toml, both empty by default (config saves automatically):
# Servers you've added (name -> spec). Written by /mcp add; edit by hand for auth/env.
[mcp_servers.<name>]
transport = "stdio" | "http"
# stdio:
command = "…"
args = ["…"]
env = { KEY = "val" }
# http:
url = "https://…"
auth = { type = "bearer", token_env = "…" } # or the oauth form above
# Which servers are live on the tool surface (per-server gate).
mcp_enabled = ["gw", "fs"]/mcp (menu), /mcp add, /mcp remove, and the enable/disable menu all persist these
for you - you only edit by hand to set auth/env, which the add shortcut doesn't
prompt for.
| Symptom | Likely cause |
|---|---|
/mcp list shows error: … |
Server unreachable / bad command / auth rejected. Fix, then /mcp reconnect <name>. |
0 tools but connected |
Some proxy servers list nothing until after initialize - lean-coder always handshakes first, so this usually means the upstream itself is empty or its own auth failed. |
HTTP server needs curl |
The HTTP transport shells out to curl; install it, or use a stdio server. |
| Tool never offered to the model | Raise the leash to rwe (/leash rwe) - MCP tools ride that tier. |
Secret ended up in config.toml |
Use token_env / client_secret_env instead of the literal forms. |