Skip to content

Latest commit

 

History

History
186 lines (146 loc) · 8.25 KB

MCP.md

File metadata and controls

186 lines (146 loc) · 8.25 KB

Using MCP servers with lean-coder

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.

Quick start

/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.

The two transports

Both are stdlib-only (no mcp SDK, no pip packages). The HTTP transport and OAuth require curl on PATH.

stdio

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 environment

HTTP (streamable)

The 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"

Authentication (HTTP only)

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.

Static bearer token

[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.

OAuth 2.1 (client credentials)

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.

Guided OAuth (auto-registration)

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:

  1. Discovery - fetches the gateway's /.well-known/oauth-protected-resource (RFC 9728) then /.well-known/oauth-authorization-server (RFC 8414) to find the token_endpoint and registration_endpoint.
  2. Registration - if a registration_endpoint exists, 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.
  3. Storage - writes those to a mode-0600 sidecar at ~/.config/leancoder/mcp_auth/<server>.json, and sets config.toml to just auth = { type = "oauth" }. The secret never touches config.toml.
  4. 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.

How MCP tools behave

  • Naming. A server gw exposing search_handbook surfaces as mcp__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 rwe tier (like a non-safe lean-tool) - available only at the rwe leash, and they confirm before running unless approval is armed (/approve session or auto).
  • 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 reconnect it once fixed). A dead server never blocks the loop.
  • Results. A tool result's structuredContent is preferred, else text content blocks are joined; an isError result is prefixed [tool error].

Config reference

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.

Troubleshooting

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.