6.6 KiB
LSP Server Stub
The CleverAgents LSP server stub provides a minimal Language Server Protocol implementation that handles the core lifecycle handshake over JSON-RPC stdin/stdout transport with Content-Length header framing.
Usage
Launch the stub server via the CLI:
agents lsp serve
agents lsp serve --log-level debug
The server reads JSON-RPC messages from stdin and writes responses to stdout, following the LSP base protocol specification.
Options
| Flag | Default | Description |
|---|---|---|
--log-level |
info |
Logging level: debug, info, warning, error |
On startup the server prints a banner with its PID and transport details to the console (stderr), then enters the message loop.
Transport Limits
| Limit | Value | Description |
|---|---|---|
| Max Content-Length | 10 MB | Messages exceeding this are rejected (DoS guard) |
| Max header lines | 32 | Headers with more lines are dropped (DoS guard) |
Messages with a Content-Length header exceeding 10 MB
(MAX_CONTENT_LENGTH) are skipped with a warning log entry and
the server continues reading the next message. Up to
MAX_CONTENT_LENGTH bytes of body are consumed and discarded
in 64 KB chunks so that subsequent messages are not affected by
a transport desync. The discard is capped to prevent a
malicious client from blocking the server with an absurdly
large declared length.
Header sections with more than 32 lines (MAX_HEADER_LINES)
are skipped to prevent denial-of-service from adversarial clients
sending infinite non-empty header lines. Remaining header lines
are drained up to the blank terminator (with a secondary cap of
MAX_HEADER_LINES drain lines) so the stream stays aligned for
the next message.
Malformed Content-Length values (non-integer strings) are
rejected and remaining headers are drained before resuming.
Deeply nested JSON payloads that exceed Python's recursion limit are caught and return a JSON-RPC parse error response. This prevents a denial-of-service from adversarial clients sending pathologically nested structures to exhaust the call stack.
Other recoverable transport errors — negative Content-Length,
incomplete message body, and malformed JSON — are also skipped
(with appropriate warning logs or JSON-RPC parse error responses)
rather than terminating the server. If the output stream is
broken (e.g. client disconnected), the server logs a warning and
terminates gracefully. Only a true EOF (closed stdin), a broken
output pipe, or an exit notification stops the event loop.
Supported Methods
initialize
Responds with a stubbed InitializeResult containing:
- serverInfo:
{ "name": "cleveragents-lsp-stub", "version": "0.1.0" } - capabilities: All capabilities from the specification's LSP
Capability Exposure section are reported as stubs (disabled or
null). The capability set includes
textDocumentSync,completionProvider,hoverProvider,definitionProvider,referencesProvider,documentFormattingProvider,renameProvider,codeActionProvider,diagnosticProvider,signatureHelpProvider,documentSymbolProvider, andworkspaceSymbolProvider.
A second initialize request is rejected with InvalidRequest
(-32600) per the LSP specification.
shutdown
Returns a JSON-RPC success response with result: null. Marks the
server as ready for exit. After shutdown, only exit is
accepted — all other requests receive an InvalidRequest (-32600)
error, and notifications are silently ignored, per the LSP
specification (section 3.16).
Like all other requests (except initialize), shutdown is subject
to the pre-initialize guard: sending shutdown before initialize
returns a ServerNotInitialized (-32002) error.
exit
Terminates the server event loop. If shutdown was received first
the exit code is 0; otherwise it is 1.
Pre-Initialize Requests
Requests received before the initialize handshake (including
shutdown) return a ServerNotInitialized (-32002) error, per the
LSP specification:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32002,
"message": "Server not initialized: textDocument/completion"
}
}
Notifications sent before initialize are silently ignored.
Unsupported Methods
After initialize, all other LSP methods (e.g.
textDocument/didOpen, textDocument/completion,
textDocument/hover) return a JSON-RPC MethodNotFound error:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32601,
"message": "Not implemented: textDocument/completion"
}
}
Notifications (messages without an id field) for unsupported
methods are silently ignored, per the JSON-RPC specification.
A2A Facade Wiring
An A2aLocalFacade instance is stored on LspServer so that the
dispatch path can be wired through A2A when server mode lands.
In the current stub implementation the facade is a placeholder —
all methods are handled by hardcoded stub handlers in
_dispatch_stub(). The facade is not called at this time.
TODO (M7+): Wire
_dispatch_stub()throughself._facadewhen server mode is implemented.
Architecture
stdin → [Content-Length framing] → [JSON-RPC parser]
↓
[LspServer._handle_message]
↓
[_dispatch_stub (hardcoded)]
↓
[Stub handlers]
↓
stdout ← [Content-Length framing] ← [JSON-RPC response]
Note:
A2aLocalFacadeis stored but not called in the current stub. The architecture diagram above will change when server mode routes through the facade.
Future Roadmap
- M7: Wire
textDocument/didOpenandtextDocument/didClosenotifications to the A2A context pipeline. - M8: Implement
textDocument/completionvia the LspToolAdapter, delegating to real language server processes managed by LspRuntime. - M9: Add
textDocument/diagnosticspush support with server-initiated notifications. - M10: Full LSP feature parity for supported languages (Python, TypeScript, Go) with hot-reload of language server configurations from the LSP Registry.
- Server mode: Replace stub handlers with real LSP transport proxying through the A2A facade in server mode.