Add stdio transport for desktop/IDE MCP clients #15
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Add stdio transport — the standard MCP transport for desktop and IDE clients.
Goal
lmcp is HTTP-only today. Most desktop MCP clients (Claude Desktop's
claude_desktop_config.json, JetBrains/VS Code MCP integrations, themcpCLI) expect stdio: launch the server as a subprocess, write JSON-RPC frames to its stdin, read responses from stdout. Without stdio, lmcp can't be a first-class citizen of those clients.Adds reach without adding much code — the dispatch logic in
lmcp:handle_requestalready does all the work; stdio is just a different read/write loop around it.Spec
API for lmcp
Or, equivalently, a separate runner:
Implementation sketch
Single dispatch path; no changes to
handle_request. Around 20 lines.Concerns / spec details
io.stdin:lines()blocks until a complete line; matches the spec.2025-06-18removed JSON-RPC batching, so one-message-per-line is correct.handle_requestreturning nil; the runner just doesn't write a line.id.Capabilities advertisement
Unchanged. Transport is a deployment detail, not a capability.
Scope (v1)
lmcp:run_stdio()method.server.luarunner option (e.g.LMCP_TRANSPORT=stdioenv, or a CLI arg).Out of scope
Priority
High. Unlocks Claude Desktop + many IDE clients in a single afternoon. Currently no MCP client speaks lmcp without curl-shaped HTTP. Half a day.
Implemented.
lmcp:run_stdio()in lmcp.lua + transport switch in server.lua and example_server.lua.LMCP_TRANSPORT=stdioenters line-delimited JSON-RPC mode (stdin requests, stdout responses, stderr diagnostics). Does NOT require luasocket — handle_request is transport-agnostic. Auth bypassed (parent process is trust boundary). Buffering:setvbuf("no")+ per-writeflush()belt-and-braces. EOF closes cleanly. Parse errors recoverable.LMCP_PORTwarning printed if set under stdio mode.Phase 5 reviewer caught a cross-cutting bug: the catch-all dispatch was emitting
-32601withid:nullfor any unknownnotifications/*(cancelled, roots/list_changed, etc.). Fix: top-level guardif id == nil then return nil endin handle_request — subsumes the per-methodnotifications/initializedbranch and improves the HTTP path too. JSON-RPC 2.0 spec-correct.Verified 7/7 cases live: init+notif+list+call, parse-error recovery, blank-line tolerance, EOF→exit 0, unknown-notification suppression, LMCP_PORT warning, HTTP backwards compat.
Memory: project_handle_request_is_shared.md captures the lesson — shared dispatch is transport-blind; HTTP masks bugs that stdio surfaces; vet notifications per transport.