Skip to main content
Tech Tutorials & Programming

Sep 7, 2026

What Is JSON-RPC, and How Does the Language Server Protocol (LSP) Work?

Learn how JSON-RPC requests, responses, and notifications become the Language Server Protocol behind autocomplete, diagnostics, hover, and go-to-definition.

What Is JSON-RPC, and How Does the Language Server Protocol (LSP) Work?

Quick Answer

JSON-RPC 2.0 is a lightweight protocol for representing method calls as JSON. It defines requests, responses, notifications, errors, and IDs, but it does not define what the methods mean or which transport must carry them.

The Language Server Protocol (LSP) builds a developer-tool contract on top of JSON-RPC. LSP gives methods such as initialize, textDocument/completion, textDocument/hover, and textDocument/definition precise meanings. An editor acts as the language client, while a separate language server analyzes code and returns structured results.

Together, JSON-RPC supplies the message grammar and LSP supplies the vocabulary. That separation lets one language server support several editors, while one editor can connect to servers for several programming languages.

Key Takeaways

  • JSON-RPC defines how to encode a remote method call; LSP defines the code-intelligence methods being called.
  • A JSON-RPC request contains an id and expects a response. A notification omits the id and must not receive a response.
  • LSP uses JSON-RPC 2.0 for lifecycle, document synchronization, language features, workspace features, progress, and cancellation.
  • The editor owns the interface and live text buffer. The language server owns parsing, indexing, type analysis, and language-specific answers.
  • LSP replaces an M-editors-by-N-languages integration problem with reusable clients and servers, although real products still need capability-specific adaptation.
  • A typical LSP session starts with initialize, continues with an initialized notification, and ends with shutdown followed by exit.
  • Stream-based LSP messages use an ASCII Content-Length header and a UTF-8 JSON body. The length is measured in bytes, not characters.
  • Document versions and position encodings matter. Stale versions, wrong ranges, and UTF-16 mistakes cause many apparent “random” language-server bugs.
  • LSP 3.18 is the latest version listed by the official site as of September 4, 2026; its specification says 3.18.x remains under development.
  • Tree-sitter, the Debug Adapter Protocol, the Build Server Protocol, MCP, ACP, and OpenRPC solve adjacent problems; none is a drop-in replacement for LSP.

Last updated: September 4, 2026. Verified against JSON-RPC 2.0 and the current, under-development Language Server Protocol 3.18.x specification.

Autocomplete looks instantaneous, but an editor may cross a process boundary before it can show one suggestion. The editor tells a language server which document changed, where the cursor is, and which capabilities it supports. The server parses the current code, consults symbols and types, then returns a list the editor can render.

A completion request on the wire can be as small as this:

json

That object is both JSON-RPC and LSP. JSON-RPC explains why it has jsonrpc, id, method, and params. LSP explains what textDocument/completion means, how positions are encoded, and which result shapes are valid.

This guide follows that message from the generic RPC layer through the language-server lifecycle. It also explains framing, synchronization, capability negotiation, failure modes, debugging, and the protocols that now surround LSP in modern developer tools.

How Do JSON-RPC and LSP Fit Together?

JSON-RPC and LSP fit together as two layers: JSON-RPC defines message mechanics, while LSP defines standardized code-intelligence operations.

The distinction is similar to grammar and vocabulary. JSON-RPC says a client can invoke a named method with structured parameters and correlate the result through an ID. LSP supplies names such as textDocument/hover and defines the parameters, result, lifecycle, and capability rules for each one.

LayerMain questionWhat it definesWhat it does not define
JSONHow is data represented?Objects, arrays, strings, numbers, booleans, and nullCalls, methods, document semantics, or transports
JSON-RPC 2.0How is a method call represented?Requests, responses, notifications, IDs, params, results, errors, and batchesCode intelligence or a mandatory network transport
LSP 3.18How do editors request language features?Lifecycle, capabilities, documents, positions, diagnostics, completion, hover, navigation, refactoring, and moreParsing algorithms, UI design, or one required implementation language
Transport and framingHow do message boundaries cross a channel?Byte framing or channel-specific deliveryThe meaning of LSP methods

The official JSON-RPC 2.0 specification describes itself as transport agnostic. The official LSP overview says LSP defines the messages exchanged between a development tool and a language server using JSON-RPC.

This layered design has an important consequence: supporting JSON-RPC does not make a program an LSP implementation. A wallet API, build service, or AI-tool protocol can also use JSON-RPC while defining completely different methods. An LSP client must understand LSP method names, data types, ordering constraints, and capabilities.

bash

In short: JSON-RPC makes a message recognizable as a call, result, notification, or error. LSP makes that message useful to an editor by defining language-aware methods, types, capabilities, and lifecycle rules.

What Is JSON-RPC 2.0?

JSON-RPC 2.0 is a stateless remote procedure call protocol that represents method invocations and their results with JSON objects.

The JSON-RPC Working Group dated the current 2.0 specification March 26, 2010 and updated it January 4, 2013. The small core has remained useful because it separates message structure from transport. The same data model can travel through standard input and output, a socket, an in-process channel, HTTP, or another message system.

JSON-RPC 2.0 defines three main message shapes:

  1. A request asks a peer to run a method and includes an ID.
  2. A response returns either a result or an error for that ID.
  3. A notification invokes a method without an ID and receives no response.

The protocol does not require a particular programming language or generated client. A sender only needs to serialize valid JSON, and a receiver needs to validate the envelope before dispatching the method.

What does a JSON-RPC request contain?

A JSON-RPC request contains a version marker, a method name, optional structured parameters, and an ID when the caller expects a response.

json
MemberRequirementMeaning
`jsonrpc`RequiredMust be the string `"2.0"`
`method`RequiredNames the operation to invoke
`params`OptionalAn object for named parameters or an array for positional parameters
`id`Required for a requestA string, number, or null used to correlate the response

The specification discourages a null request ID because null also appears when a server cannot determine an ID for an invalid request. Fractional numeric IDs are also discouraged because number representations can differ between runtimes. Strings or integers are the safest choices.

LSP narrows the generic rule further: an LSP request ID is a string or integer, not null. A response ID can be null when the original request ID is unknown, such as certain parse or validation failures.

Method names beginning with rpc. are reserved for RPC-internal methods and extensions. LSP defines its own method namespaces, including textDocument/, workspace/, window/, and $/.

What does a JSON-RPC response contain?

A JSON-RPC response repeats the request ID and contains either a result or an error, never both.

json

The repeated ID is what makes concurrent calls practical. A client can send several requests without waiting after each one. The server may finish them in a different order, and the client can still match each result to the correct pending operation.

JSON-RPC does not define an “empty success” by omitting both members. A successful call whose logical return value is empty normally uses "result": null. An unsuccessful call uses an error object.

What makes a JSON-RPC notification different?

A JSON-RPC notification omits the ID, signals that no response is wanted, and must not receive a response from the server.

json

Notifications suit events such as “this document changed” or “the configuration changed.” Sending a response would be a protocol violation because the receiver has no request ID to echo.

This fire-and-forget model has a tradeoff. The sender cannot learn from a corresponding response whether the notification failed. Implementations therefore need logs, validation, ordering discipline, and a recovery strategy for synchronized state.

How do JSON-RPC errors work?

JSON-RPC errors use a numeric code, a short message, and optional structured data that can carry implementation-specific details.

json
CodeStandard meaning
`-32700`Parse error
`-32600`Invalid request
`-32601`Method not found
`-32602`Invalid params
`-32603`Internal error
`-32099` to `-32000`Reserved for implementation-defined server errors

LSP adds protocol-specific error codes to the JSON-RPC model. Examples include -32002 for a server that has not been initialized and -32800 for a cancelled request. The outer error shape remains JSON-RPC even when the code has LSP-specific meaning.

Does JSON-RPC support batch calls?

JSON-RPC 2.0 supports a batch as a non-empty array of requests and notifications, but LSP 3.18 explicitly prohibits JSON-RPC batch messages.

json

A generic JSON-RPC server may process batch members concurrently and return responses in any order. It omits a response for each notification. A batch containing only notifications produces no response.

An implementation cannot assume that every JSON-RPC feature appears in every higher-level protocol. The LSP 3.18 base protocol is explicit: “The protocol currently does not support JSON-RPC batch messages.” It then prohibits clients and servers from sending them. Implementers should treat this as MUST NOT: every LSP request, response, or notification travels as an individual message rather than inside a batch array.

In short: JSON-RPC 2.0 provides a compact envelope for method calls. IDs correlate requests and responses, notifications omit IDs, errors use standard objects, and JSON-RPC permits batching while LSP 3.18 prohibits it.

What Is the Language Server Protocol (LSP)?

The Language Server Protocol is a JSON-RPC-based standard for exchanging code-intelligence messages between development tools and language servers.

An LSP client normally lives inside an editor or IDE. It translates editor events into protocol messages and turns server results into interface elements. A language server is a separate program or service that understands a language, project, or toolchain.

The server might wrap a compiler, reuse a parser, call a linter, maintain a symbol index, or combine several analysis engines. LSP standardizes the boundary, not the implementation behind it.

ComponentUsually ownsExamples
Editor or IDEText display, cursor, selections, menus, panels, user commandsVisual Studio Code, Neovim, Emacs, Eclipse, Zed
LSP clientProcess launch, transport, synchronization, capability negotiation, request routing, result-to-UI conversionAn editor extension or built-in language client
Language serverParsing, symbol resolution, type analysis, diagnostics, completion candidates, refactoring logicrust-analyzer, gopls, clangd, Pyright
Compiler or analysis libraryLanguage grammar, types, build rules, semantic modelA tool reused by the server

Microsoft's protocol history explains that VS Code had integrated different language tools with different protocols. A common protocol let host integration code be reused, while capability negotiation let servers expose different feature sets.

The public LSP site lists version 3.18 as the latest specification. The 3.18 document calls the current 3.18.x line “under development,” so implementers should check the exact revision used by their library. LSP versions group compatible feature additions; capability flags allow clients and servers to adopt features without requiring every implementation to update at once.

In short: LSP is the reusable contract between a code editor and a language analysis process. It standardizes messages and data types while leaving parsing algorithms, server architecture, and interface design to each implementation.

Why Was LSP Created?

LSP was created to reduce repeated integration work between language tooling and editors while isolating expensive analysis from the editor process.

Without a shared protocol, every editor needs a custom adapter for every language tool. If M editors each integrate independently with N language engines, the ecosystem can approach M × N integrations. A shared protocol moves the design toward M clients plus N servers.

bash

The model delivers three practical benefits:

  1. Reuse across editors. A server can serve any client that correctly implements the methods and capabilities it needs.
  2. Language freedom. A language server can be written in Rust, Go, Java, TypeScript, Python, C#, or another language.
  3. Process isolation. Parsing and indexing can run outside the editor's interface process, reducing coupling and containing failures.

The M × N explanation is a design model, not a literal promise that integration work disappears. Editors expose different user experiences. Servers support different subsets and extensions. Installers, process supervision, path conversion, workspace trust, configuration, and testing remain product-specific.

The VS Code Language Server Extension Guide makes the same tradeoff concrete. It identifies language/runtime differences, CPU and memory cost, and repeated editor integration as the problems LSP addresses.

In short: LSP turns language intelligence into a reusable service boundary. It reduces duplicated adapters and protects editor responsiveness, but clients still need careful handling for capabilities, lifecycle, configuration, and user experience.

Why Does LSP Use JSON-RPC?

LSP uses JSON-RPC because it provides small, inspectable, bidirectional call semantics without forcing one programming language or transport.

Plain JSON alone does not say whether an object is a request, response, error, or event. JSON-RPC adds those semantics with a few members. The client can correlate asynchronous results through IDs, and either peer can send notifications.

JSON-RPC also suited the constraints of developer tooling:

  • JSON is supported by nearly every editor and language runtime.
  • Messages can be inspected in logs without a binary decoder.
  • Named parameters evolve more safely than positional arguments.
  • IDs allow multiple requests to be in flight at once.
  • Notifications naturally represent document and workspace events.
  • The protocol does not require HTTP, which is useful for local processes and IPC.
  • Higher-level specifications can define their own methods and types.

The choice carries costs. JSON is verbose compared with a compact binary format. Runtime validation is essential because JSON has no built-in schema enforcement. Method compatibility also depends on capability negotiation and disciplined evolution.

LSP addresses those limitations with TypeScript-style interface definitions, version annotations, capability flags, a generated meta model, and implementation libraries. Those mechanisms sit above JSON-RPC; they are not supplied by JSON-RPC itself.

In short: JSON-RPC gives LSP the minimum mechanics a bidirectional tool protocol needs. It is portable and debuggable, while LSP adds the schemas and negotiation required for reliable code intelligence.

How Does an LSP Session Start and Stop?

An LSP session starts with an initialize request, becomes active after initialized, and closes through shutdown followed by exit.

Lifecycle order is not cosmetic. The initialize exchange lets both peers understand which features and data shapes they can safely use. Sending ordinary feature requests too early can produce ServerNotInitialized errors or undefined behavior.

Step 1: How does the client initialize the server?

The client sends exactly one initialize request with information about itself, the workspace, supported position encodings, and feature capabilities.

json

rootUri is deprecated in favor of workspaceFolders. It remains in this compatibility example because the current InitializeParams type still declares it as DocumentUri | null, and older servers may rely on it. New clients should treat workspaceFolders as the primary workspace model.

The exact capability tree can be much larger. A production client should advertise only behavior it actually implements. Claiming support for snippets, dynamic registration, resource operations, or edit annotations without honoring them creates failures later.

Step 2: How does the server answer initialization?

The server returns its capabilities and can include identifying information in the initialize result.

json

change: 2 means incremental text synchronization. The client sends edits rather than the entire file on every change. resolveProvider: true means completion entries can be returned quickly and enriched later through completionItem/resolve.

Step 3: What does the initialized notification mean?

The initialized notification tells the server that the client accepted the initialize result and normal protocol operation can begin.

json

The server can now dynamically register supported features, request configuration, and process document or workspace traffic. Because initialized is a notification, the server sends no response.

Step 4: How does an LSP session shut down?

The client first sends a shutdown request and waits for a null result, then sends an exit notification.

json

shutdown asks the server to prepare to stop but does not itself terminate the process. exit carries no ID and therefore receives no response. The two-message sequence lets the client distinguish an orderly close from a crash or forced termination.

PhaseClient messageExpected server behavior
Start`initialize` requestValidate client data and return server capabilities
Ready`initialized` notificationBegin normal operation; optionally register capabilities
WorkDocument, language, and workspace messagesSynchronize state and answer supported methods
Prepare to stop`shutdown` requestReturn `null` and stop accepting normal work
Stop`exit` notificationTerminate the server process

In short: Initialization establishes the contract for one LSP connection. Normal requests should follow the initialize response and initialized notification. An orderly close uses shutdown and exit as two distinct messages.

How Does Autocomplete Travel Through LSP?

LSP autocomplete combines synchronized document state with a textDocument/completion request and a correlated JSON-RPC response.

The completion request is only one moment in a longer flow. The server must already know which document is open and which text version the cursor position refers to.

Editor LSP client Language server
open app.ts
--------------------------->textDocument/didOpen
--------------------------->
type "."
--------------------------->textDocument/didChange
--------------------------->
ask for suggestionsid 41: completion request
--------------------------->
completion result
<---------------------------
render list
<---------------------------

What does a completion request contain?

A completion request identifies the document, cursor position, and optional trigger context for a specific text version known through synchronization.

json

The request does not normally include the whole document. The server reconstructs current content from prior didOpen and didChange notifications. That design saves bandwidth, but it makes synchronization correctness essential.

What does a completion response contain?

A completion response contains an array of completion items or a CompletionList with items and metadata such as whether the list is incomplete.

json

The client uses its advertised capabilities to interpret optional fields. For example, insertTextFormat: 2 denotes a snippet. A client that advertises snippet support must understand tab stops such as ${1:callback}.

Servers can defer expensive detail. The initial item can carry opaque data, which the client sends back in completionItem/resolve when the user focuses that entry. This lowers work on suggestions the user never inspects.

What happens when the user keeps typing?

The client can cancel a stale completion request with $/cancelRequest and issue a new request for the later cursor state.

json

Cancellation is best effort. The server may already have completed the request. Clients must tolerate a late response and decide whether it is still relevant to the current document and cursor.

Request IDs solve correlation, not freshness. A result with the correct ID can still be stale from the user's perspective. Responsive clients track both pending request identity and editor state before presenting an answer.

In short: Completion depends on a synchronized document, a version-relevant cursor position, and an ID-correlated result. Cancellation reduces wasted work, while client-side freshness checks prevent old answers from appearing after new edits.

How Are LSP Messages Framed and Transported?

Stream-based LSP messages use a Content-Length header followed by a UTF-8 JSON body, so receivers can separate adjacent messages safely.

JSON text has no universal boundary when multiple objects arrive on one byte stream. A single read can contain half a message, exactly one message, or several messages. LSP's base protocol solves that issue with header framing comparable to HTTP.

bash

The wire form contains real carriage-return and line-feed bytes, not the four visible characters “\r\n.” The required Content-Length value is the number of bytes in the content part. The header is ASCII, while the content is UTF-8.

Framing ruleCorrect behaviorCommon failure
Header encodingASCIIWriting localized or binary header data
Header line ending`\r\n`Using only `\n` in a strict implementation
Header/body separatorAn empty line, or `\r\n\r\n`Omitting the second line ending
Length unitUTF-8 bytesCounting JavaScript characters or Unicode code points
BodyOne JSON-RPC messageWriting logs or banners into the protocol stream

Why must Content-Length count bytes?

Content-Length must count encoded UTF-8 bytes because non-ASCII characters can occupy more than one byte.

javascript

In JavaScript, body.length counts UTF-16 code units. It is not a safe substitute for UTF-8 byte length. A body containing “é,” “中,” or an emoji can make the two numbers differ and desynchronize the entire stream.

Does LSP require standard input and output?

LSP does not require only stdio, although stdio is common for an editor-launched local server.

Implementations also use operating-system pipes, sockets, Node.js IPC, web workers, and custom remote channels. The VS Code web-extension guide notes that browser language clients and servers can communicate through Web Worker postMessage.

Channel-specific transports may preserve message boundaries without the Content-Length header. The essential rule is that both peers agree on the same transport and framing. JSON-RPC describes message objects; it does not by itself frame a stream.

Why must logs stay off stdout?

Logs must stay off a stdio protocol's stdout because one unframed line can corrupt the language client's message parser.

Use stderr, a rotating file, or the client's trace/log facility. A server that prints “Listening…” to stdout before its first header may appear to hang even though its analysis code is correct.

In short: JSON serialization is not message framing. LSP stream framing uses byte-accurate Content-Length headers. Correct line endings, incremental reads, partial writes, and a clean protocol stream are basic interoperability requirements.

How Does LSP Keep Documents Synchronized?

LSP keeps documents synchronized through open, change, save, and close messages tied to document URIs and monotonically increasing versions.

The editor buffer is authoritative for an open document. It may contain unsaved changes that do not exist on disk. A server that rereads the file for every feature request can analyze the wrong content.

What happens when a document opens?

The textDocument/didOpen notification sends the full initial text, language identifier, URI, and version.

json

The URI is the stable identity used by later messages. Clients and servers need consistent URI normalization, case handling, percent encoding, and file-path conversion across operating systems.

What happens when text changes?

The textDocument/didChange notification sends a new document version and either the full content or one or more incremental edits.

json

The server advertises its synchronization mode during initialization:

`TextDocumentSyncKind`ValueClient behavior
None`0`Do not synchronize document content
Full`1`Send the complete text after a change
Incremental`2`Send range-based edits

Incremental sync saves traffic and can align with incremental parsers. It also makes range application more sensitive to ordering. A client must apply content changes in the defined order, and a server must associate analysis with the correct document version.

Why do position encodings cause bugs?

Position encodings cause bugs because an LSP character value is an encoded offset, not a visual column or a Unicode character count.

LSP lines and characters are zero-based. For compatibility, UTF-16 remains the default when the peers do not negotiate another supported encoding. LSP 3.17 added explicit position-encoding negotiation, and LSP 3.18 retains it.

Consider this text:

bash

The emoji occupies:

  • 1 Unicode code point
  • 2 UTF-16 code units
  • 4 UTF-8 bytes

The position of b is therefore character 3 under UTF-16 but byte offset 5 under UTF-8. A server that indexes Unicode scalar values while the client assumes UTF-16 can highlight the wrong token or reject an edit.

How do versions prevent stale analysis?

Document versions let a server and client identify which buffer state produced a diagnostic, edit, or other result.

Versions increase as the document changes, including undo and redo. They do not make every method automatically safe. Servers should attach versions where the protocol supports them, and clients should reject edits that no longer apply cleanly.

Typical synchronization failures include:

  1. Applying incremental edits against the wrong prior text.
  2. Treating a URI as a native path without decoding it correctly.
  3. Mixing UTF-8, UTF-16, and code-point offsets.
  4. Publishing diagnostics for an old version after new edits.
  5. Reading the saved file while the open buffer contains unsaved text.
  6. Reordering notifications across an asynchronous internal pipeline.

In short: An LSP server analyzes the editor's live buffer, not merely the file system. Correct URIs, ordered changes, version tracking, and a negotiated position encoding keep both sides talking about the same text.

How Do LSP Capabilities Prevent Compatibility Problems?

LSP capabilities let clients and servers announce supported behavior so optional features can evolve without breaking older implementations.

LSP has many optional fields and methods. A server cannot return every possible result shape and assume the client will understand it. A client also cannot send every newer request and assume the server implements it.

Capability negotiation happens in both directions:

  • Client capabilities arrive inside initialize.params.capabilities.
  • Server capabilities return inside initialize.result.capabilities.
  • Dynamic registration can add or remove certain registrations after startup.
Capability exampleAdvertised byWhat it changes
`snippetSupport`ClientServer may return snippet-formatted completion text
`positionEncodings`ClientServer can select a mutually supported offset encoding
`completionProvider`ServerClient may send completion requests
`semanticTokensProvider`ServerClient may request semantic token data
`workspace.configuration`ClientServer may request scoped settings
`workspaceFolders`BothPeers coordinate multi-root workspace behavior

Dynamic registration uses client/registerCapability. For example, a server can register file watchers or a formatter after reading workspace configuration. The server should use dynamic registration only when the client advertised support for it.

Capabilities are not preferences alone. They are behavioral contracts. If a client claims it supports resource operations in WorkspaceEdit, the server may return file creation, rename, or deletion operations. If the client ignores them, a refactor can become incomplete or dangerous.

Does the LSP version number negotiate the connection?

LSP feature compatibility relies mainly on capabilities and “since” annotations rather than a standalone version-negotiation handshake.

An implementation might advertise its own product version in clientInfo or serverInfo, but that is not the same as selecting “LSP 3.18” on the wire. Libraries typically implement a known specification version and conditionally use features based on capability flags.

The LSP 3.18 specification marks feature additions by version and publishes a protocol meta model. The meta model can support generated types or validators, but runtime negotiation still depends on capabilities.

In short: LSP grows through optional features and explicit capabilities. Reliable implementations advertise only what they support, inspect the peer's flags, and treat every advertised capability as a promise.

Which Developer Features Does LSP Standardize?

LSP standardizes a broad set of document, language, workspace, and window operations while allowing clients and servers to implement only negotiated subsets.

The methods are organized by direction and purpose. Requests expect responses. Notifications describe state changes or one-way events. Some features support partial results, work-done progress, lazy resolution, or dynamic registration.

User-facing featureCommon LSP methodDirectionMessage kind
Autocomplete`textDocument/completion`Client → serverRequest
Completion details`completionItem/resolve`Client → serverRequest
Hover documentation`textDocument/hover`Client → serverRequest
Go to definition`textDocument/definition`Client → serverRequest
Find references`textDocument/references`Client → serverRequest
Signature help`textDocument/signatureHelp`Client → serverRequest
Rename`textDocument/rename`Client → serverRequest
Prepare rename`textDocument/prepareRename`Client → serverRequest
Code actions`textDocument/codeAction`Client → serverRequest
Formatting`textDocument/formatting`Client → serverRequest
Document symbols`textDocument/documentSymbol`Client → serverRequest
Workspace symbols`workspace/symbol`Client → serverRequest
Semantic highlighting`textDocument/semanticTokens/full`Client → serverRequest
Inlay hints`textDocument/inlayHint`Client → serverRequest
Push diagnostics`textDocument/publishDiagnostics`Server → clientNotification
Pull diagnostics`textDocument/diagnostic`Client → serverRequest
Apply an edit`workspace/applyEdit`Server → clientRequest
Request configuration`workspace/configuration`Server → clientRequest
Show progress`$/progress`Either directionNotification
Cancel work`$/cancelRequest`Either directionNotification

LSP is bidirectional. The editor is called the client because it starts and coordinates the connection, not because it is the only side allowed to send requests. A server can request configuration, ask the client to apply edits, refresh certain views, or show messages.

Are diagnostics always pushed by the server?

Diagnostics can be pushed with textDocument/publishDiagnostics or pulled through document and workspace diagnostic requests when both peers support the newer model.

Push diagnostics are familiar: the server analyzes a document and publishes a set of warnings or errors. Pull diagnostics give the client more control over timing and can use result IDs to report unchanged results efficiently.

Neither model guarantees that every diagnostic represents the newest buffer unless the implementation handles versions and cancellation correctly. The UI should avoid displaying old results over current code.

How do partial results and progress differ?

Partial results deliver pieces of an operation's actual result, while work-done progress reports status to the user.

A large reference search might stream batches of locations through a partial-result token. The same operation can report “Indexing 72%” through a work-done token. Both use $/progress, but their payload meanings and tokens are different.

In short: LSP covers far more than autocomplete. It standardizes navigation, edits, diagnostics, semantic data, workspace operations, cancellation, and progress while keeping each feature conditional on advertised support.

What Happens Inside a Language Server?

A language server converts synchronized source text and project context into structured answers such as locations, edits, diagnostics, and completion items.

LSP does not mandate one internal design. A small server may parse only the current file. A mature server may maintain an incremental syntax tree, project graph, module resolver, type database, symbol index, build configuration, and persistent cache.

A typical request pipeline looks like this:

  1. Receive and validate. The server parses the framed JSON-RPC message and validates method parameters.
  2. Find the document snapshot. It maps the URI and version to the current in-memory text.
  3. Update syntax. An incremental parser reuses unaffected tree regions when possible.
  4. Resolve project context. The server loads configuration, dependencies, imports, and workspace metadata.
  5. Run semantic analysis. It resolves names, types, references, control flow, or linter rules.
  6. Build an LSP result. Internal objects become protocol types such as Location, Hover, TextEdit, or Diagnostic.
  7. Check cancellation and freshness. The server stops obsolete work where practical.
  8. Return or publish. It sends a JSON-RPC response or an LSP notification.
Internal concernWhy it matters
Error-tolerant parsingCode is frequently incomplete while the user types
Incremental computationRebuilding a whole project after every keystroke is too slow
Snapshot isolationConcurrent requests may refer to different document states
Dependency discoveryTypes and symbols often live outside the open file
CachingIndexes and parse trees are expensive to recreate
CancellationCompletion becomes irrelevant as soon as the user keeps typing
Result conversionInternal byte spans must become negotiated LSP positions

The VS Code language-server guide explicitly recommends an error-tolerant parser. Normal source code is often temporarily invalid between keystrokes, yet developers still expect completion and navigation to work.

This explains why a language server is not simply a compiler running on every request. Compilers usually optimize for complete builds and definitive errors. Interactive tooling optimizes for partial code, low latency, incremental updates, cancellation, and useful approximate answers.

In short: LSP standardizes the boundary, while the server's value comes from the analysis behind it. Good servers combine error tolerance, incremental state, project awareness, cancellation, and careful conversion into protocol types.

Is LSP Stateless?

LSP is not stateless at the session level because the server normally tracks capabilities, open documents, versions, workspace state, and analysis caches.

JSON-RPC's specification describes the RPC protocol as stateless. That statement means each JSON-RPC envelope does not define an application session model. It does not prevent a protocol built on JSON-RPC from maintaining state across messages.

LSP depends heavily on ordered context:

  • initialize establishes capabilities for later messages.
  • didOpen creates an in-memory document snapshot.
  • didChange mutates that snapshot.
  • didClose releases or changes ownership of the open document.
  • Workspace-folder and configuration notifications update project context.
  • Dynamic registrations change which requests and notifications are active.

A completion request does not carry the full document and entire workspace. It refers to state built through earlier messages. If a server restarts, the client usually needs to initialize it and reopen relevant documents.

LayerStateless or stateful?Explanation
JSON-RPC envelopeStateless modelDefines independent message shapes and ID correlation
LSP connectionStatefulNegotiated capabilities and lifecycle affect valid behavior
Open documentStatefulCurrent content is reconstructed from open/change notifications
Analysis engineUsually statefulIndexes, syntax trees, dependency graphs, and caches persist

This is a useful correction when comparing LSP with REST or remote JSON-RPC APIs. The serialization format does not determine the state model of the application protocol.

In short: JSON-RPC supplies stateless envelopes, but LSP builds a stateful conversation from them. Document synchronization and capability negotiation are central to how language servers work.

How Does LSP Compare With Tree-sitter, DAP, BSP, MCP, ACP, and OpenRPC?

LSP handles editor-to-language intelligence, while adjacent standards and libraries handle parsing, debugging, builds, AI tools, agents, or RPC descriptions.

These technologies often appear in the same developer product. They are complementary more often than competitive.

TechnologyPrimary jobTypical participantsRelationship to JSON-RPC or LSP
[Tree-sitter](https://tree-sitter.github.io/tree-sitter/)Incremental concrete syntax parsingAn application and a parser libraryCan power syntax or an LSP server; it is not an editor/server protocol
[Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/overview.html)Standardize debugger UI and debug adaptersDevelopment tool and debug adapterUses LSP-style `Content-Length` framing, but its JSON envelope is not JSON-RPC
[Build Server Protocol](https://build-server-protocol.github.io/docs/specification)Standardize build-tool communicationIDE and build serverComplements LSP with build targets, compilation, tests, and dependency data
[Model Context Protocol](https://modelcontextprotocol.io/)Connect AI applications to tools, resources, and workflowsAI host, MCP client, and MCP serverAlso uses JSON-RPC, but defines agent-facing capabilities rather than code-intelligence methods
[Agent Client Protocol](https://agentclientprotocol.com/)Connect code editors and coding agentsEditor-side client and coding agentJSON-RPC-based agent interaction; adjacent to LSP in AI-enabled editors
[OpenRPC](https://spec.open-rpc.org/)Describe JSON-RPC APIs in a machine-readable documentAPI producers, generators, and clientsComparable in purpose to OpenAPI, but for JSON-RPC
LSPProvide interactive language intelligenceEditor/IDE client and language serverJSON-RPC application protocol for source-code features

Is Tree-sitter a replacement for LSP?

Tree-sitter is not a replacement for LSP because it parses syntax locally, while LSP defines a cross-process contract for semantic language features.

Tree-sitter can quickly identify nodes, scopes, and structural ranges as text changes. It is excellent for syntax highlighting, selection, folding, and editor-aware parsing. It does not automatically know project-wide types, build configuration, external symbols, or how an editor should request a rename.

A language server can use Tree-sitter internally. An editor can also use Tree-sitter for immediate syntax features while an LSP server supplies deeper semantic answers.

How is the Debug Adapter Protocol different?

The Debug Adapter Protocol standardizes debugging operations such as launching, attaching, setting breakpoints, stepping, inspecting variables, and reading stack traces.

DAP reuses LSP-style Content-Length framing, but it does not use JSON-RPC. A DAP request has fields such as seq, type, command, and arguments; responses and events use DAP's own envelope shapes.

LSP answers questions about source code. DAP controls and observes a running program. A full IDE commonly implements both through separate clients and adapters.

How are LSP and MCP different?

LSP connects an editor to language intelligence, while MCP connects an AI application to external tools, context, and workflows.

Both use JSON-RPC and capability-oriented client-server ideas, so their wire messages can look similar. Their method vocabularies and trust models are different. LSP might request the definition of a symbol. MCP might list tools or ask an approved service to perform an action.

For a deeper treatment, read What Is MCP and How Does Model Context Protocol Work?.

Where does OpenRPC fit?

OpenRPC describes the methods, parameters, results, errors, and examples of a JSON-RPC API in a machine-readable format.

It can support documentation, validation, and code generation for generic JSON-RPC services. LSP already publishes detailed protocol types and a meta model, so OpenRPC is an adjacent ecosystem concept rather than the canonical LSP description format.

In short: LSP occupies one layer of a larger tool stack. Parsers understand syntax, build servers understand builds, debug adapters control executions, AI protocols expose tools, and description formats document RPC APIs.

How Does JSON-RPC Compare With REST and gRPC?

JSON-RPC models named method calls, REST models resources through HTTP semantics, and gRPC models typed services with Protocol Buffers.

No option is universally superior. The right choice depends on interoperability, transport, streaming, browser compatibility, schema requirements, performance, and operational tooling.

DimensionJSON-RPC 2.0REST-style HTTP APIgRPC
Core abstractionMethod invocationResources and representationsTyped service methods
Common payloadJSONUsually JSON, but not requiredProtocol Buffers
TransportTransport agnosticHTTPHTTP/2 in the standard stack
SchemaExternal or protocol-specificOpenAPI or other schema, optional`.proto` definition
Human inspectionEasyEasy for JSON APIsRequires decoding/tooling
StreamingTransport/protocol dependentHTTP-dependentBuilt-in unary and streaming modes
Browser usePossible with a suitable transportNative web modelUsually needs gRPC-Web or a gateway
Code generationOptionalOptionalCentral to normal use
Error modelJSON-RPC error objectHTTP status plus application bodygRPC status and typed details
LSP fitChosen base message formatNot the LSP wire modelCould work technically, but would be a different protocol

Why did LSP not simply use REST?

LSP needs bidirectional calls and notifications over long-lived local channels where HTTP resource semantics add limited value.

An editor asks operations such as “resolve this completion item” or “find references at this position.” These map naturally to method calls. The server also needs to request work from the client and publish events. JSON-RPC provides the same envelope in both directions.

Would gRPC be faster?

gRPC can reduce payload size and provide generated typed stubs, but changing LSP to gRPC would trade away simple JSON interoperability and the existing ecosystem.

Performance is not determined by serialization alone. Parsing a project, resolving types, indexing dependencies, and computing refactors usually cost more than decoding a small request envelope. Binary encoding may matter in some deployments, but it would not eliminate analysis latency or synchronization complexity.

The gRPC overview describes its Protocol Buffers-based service model and cross-language code generation. Those strengths suit many internal services. LSP chose a different balance for heterogeneous editor tooling.

In short: JSON-RPC is a strong fit for inspectable, bidirectional tool calls. REST is strongest when HTTP resource semantics matter. gRPC is strongest when typed contracts, generated stubs, and efficient service communication justify a heavier stack.

What Are the Most Common JSON-RPC and LSP Bugs?

The most common LSP failures come from broken framing, stale document state, incorrect positions, capability mismatches, and lifecycle violations.

Many bugs look semantic at first. Completion may stop after typing an emoji, diagnostics may drift by one line, or a server may “randomly” disconnect. The actual defect can sit below the language engine.

SymptomLikely causeFirst check
Client hangs on startupInvalid header, stdout logging, or missing initialize responseCapture raw bytes and verify framing
Works for ASCII but fails for emojiByte/code-unit position mismatchConfirm negotiated position encoding
Diagnostics appear on old textStale version or slow analysis resultLog URI and version with every analysis
Rename applies partial editsUnsupported `WorkspaceEdit` capabilityCompare returned edit shape with client flags
Server reports unknown documentURI normalization mismatch or missed `didOpen`Log canonical URIs on both peers
Responses appear attached to wrong UI actionReused or mishandled request IDsTrack one pending entry per ID
CPU stays high while typingCancellation ignored or whole project reanalyzedProfile request lifetimes and invalidation
JSON parser fails after a log messageServer wrote logs to stdoutSend logs to stderr or a file
Feature never activatesServer capability absent or registration failedInspect initialize and register messages
Edit ranges drift after several changesIncremental edits applied out of orderReplay the exact change stream

Which JSON-RPC mistakes break LSP?

Five JSON-RPC mistakes account for a large share of basic interoperability failures:

  1. Sending a response to a notification.
  2. Returning both result and error.
  3. Changing the request ID's type between request and response.
  4. Treating every incoming method as client-to-server.
  5. Assuming responses arrive in request order.

Which lifecycle mistakes break LSP?

Common lifecycle mistakes include sending feature requests before initialization, advertising unsupported capabilities, processing work after shutdown, and exiting without the proper sequence.

Servers also need to handle unexpected termination. If the child process crashes, the client may restart it. A restart creates a new session: initialize again, restore configuration, and reopen documents rather than assuming prior in-memory state survived.

Which text-model mistakes break LSP?

Text-model mistakes include mixing saved files with live buffers, applying ranges in the wrong coordinate system, and losing document versions across asynchronous work.

Use immutable snapshots internally when possible. A request should analyze one coherent text state even if later didChange notifications arrive while the request is running.

In short: Debug the protocol boundary before blaming the parser. Byte framing, IDs, lifecycle order, URIs, versions, positions, and capabilities are the foundation every language feature depends on.

How Should Developers Debug an LSP Connection?

Developers should debug LSP by capturing messages at the client/server boundary, preserving direction and timing, then validating framing, IDs, versions, and capabilities.

A structured trace is more useful than isolated server logs. Record enough context to reconstruct the conversation without leaking source code, credentials, or private paths.

What should an LSP trace record?

An LSP trace should record timestamp, direction, method, request ID, URI, document version, duration, result status, and selected capability decisions.

bash

Avoid logging full document text by default. Source code may contain secrets, customer data, unpublished intellectual property, or local paths. Use opt-in payload logging and redact known secret fields.

What is a reliable debugging sequence?

Use this order to narrow the failure:

  1. Verify process health. Did the server start, remain alive, and write errors to stderr?
  2. Inspect raw framing. Are headers correct, byte lengths exact, and message boundaries intact?
  3. Check initialization. Did both peers exchange the expected capabilities?
  4. Follow one URI. Do open and change messages reconstruct the editor buffer exactly?
  5. Follow one request ID. Does one request receive exactly one matching response?
  6. Check position encoding. Do all spans use the negotiated unit?
  7. Check freshness. Was the result computed for the active version and cursor state?
  8. Profile analysis. Only after protocol correctness, investigate parsing and semantic performance.

Which testing layers should a language server use?

A mature language server should use protocol-unit tests, transcript tests, editor integration tests, and fuzz or property tests for framing and incremental edits.

Test layerWhat it catches
Handler unit testIncorrect method logic and result conversion
Golden transcript testLifecycle, capabilities, IDs, and exact result shapes
Incremental-sync replayRange application and version drift
Framing testPartial reads, combined messages, Unicode byte lengths
End-to-end editor testReal activation, process control, UI integration
Cancellation stress testLeaked work and stale responses
Cross-client testAssumptions tied to one editor implementation

The VS Code guide describes both unit and end-to-end approaches. Cross-client tests are especially valuable because a server that works only with the library used to build it may depend on behavior the specification does not guarantee.

In short: Treat LSP as a distributed system in miniature. Trace the wire, correlate IDs, reproduce document snapshots, test cancellation, and separate protocol bugs from language-analysis bugs.

When Should You Build a Language Server?

Build a language server when semantic tooling must work across multiple editors, needs project-wide analysis, or benefits from an isolated long-running process.

LSP is a strong fit for a programming language, configuration language, query language, template system, domain-specific language, or framework whose users need completion, navigation, diagnostics, and refactoring.

Use an LSP server when several of these are true:

  • The same intelligence should work in more than one editor.
  • Analysis requires an existing compiler, parser, or language runtime.
  • Project-wide indexing is too expensive to repeat in each plugin.
  • The tooling needs a persistent cache or dependency graph.
  • The editor process should be isolated from CPU-heavy work.
  • The feature set matches standardized LSP methods.
  • A compatible server already exists and needs only a client adapter.

When is a direct editor extension simpler?

A direct editor extension is often simpler for a small, editor-specific feature that does not need project semantics or cross-editor reuse.

Examples include snippets, a grammar for syntax highlighting, bracket rules, a simple code generator, or one lightweight completion provider. The VS Code programmatic language-features documentation shows how an extension can implement providers without a language server.

Should you build from scratch or use an SDK?

Use a mature LSP library unless the implementation has unusual transport, runtime, or dependency constraints.

Common libraries include:

An SDK can handle framing, typed messages, request correlation, cancellation plumbing, and evolving protocol types. It cannot decide the server's language semantics, cache design, security policy, or latency budget.

What should a first implementation support?

A first server should implement a narrow, correct vertical slice before attempting the entire specification.

  1. Initialize and shutdown correctly.
  2. Support open, change, and close synchronization.
  3. Add one high-value feature such as diagnostics or completion.
  4. Track versions and position encoding.
  5. Add trace logging and transcript tests.
  6. Test with at least two clients when cross-editor support is a goal.
  7. Add capabilities only as their behavior becomes complete.

In short: LSP pays off when language intelligence is reusable, stateful, or computationally heavy. For one small editor feature, a native extension can be cheaper. Start narrow and make the protocol boundary correct first.

How Do LSP, MCP, and Proxy Infrastructure Fit Into Modern Developer Workflows?

LSP, MCP, and proxy infrastructure solve separate layers: code intelligence, AI-tool integration, and controlled outbound web access.

The acronyms increasingly appear together because modern editors can host both language clients and AI agents. The boundaries are easier to understand when following one workflow.

bash

LSP helps the editor understand source code. MCP lets an AI host discover and call approved external tools. A forward proxy carries selected application traffic to a destination through an intermediary IP. None of these layers substitutes for another.

The open-source Proxidize MCP server is a practical example of another JSON-RPC application protocol running alongside LSP in the same AI-enabled editor. It lets compatible AI clients work with approved Proxidize account capabilities such as proxy management, rotation, access points, usage, and analytics. That is an MCP use case, not an LSP language feature.

Proxidize's managed residential and mobile proxies can sit farther downstream when a legitimate developer or AI workflow needs reliable, location-aware access to public web data. Residential proxies fit broad global data collection. Mobile proxies fit workflows that specifically need real US mobile carrier IPs.

Do not route local editor-to-language-server traffic through a forward proxy merely because both use client-server terminology. A proxy does not improve LSP semantics, document synchronization, or completion latency. It belongs on an outbound HTTP or SOCKS connection whose destination access requires that network layer.

Any scraping, crawling, or automated access must comply with applicable law, website terms, privacy obligations, and the data owner's rights. Proxies improve network routing and IP diversity; they do not grant permission to access restricted or private data.

In short: LSP powers code understanding, MCP powers agent-tool connections, and proxies power selected network access. A secure architecture keeps those responsibilities explicit and grants each component only the permissions it needs.

What Are LSP's Limits?

LSP standardizes communication, but it does not guarantee identical features, performance, security, or user experience across editors and servers.

The protocol removes one category of integration duplication. It does not make all language engines equal or erase platform differences.

LSP does not define:

  • How a programming language is parsed or type-checked.
  • How quickly completion or diagnostics must return.
  • How an editor presents a hover, code action, or progress report.
  • How a server is installed, upgraded, sandboxed, or restarted.
  • How a remote server authenticates users or encrypts traffic.
  • How build systems expose every project-specific detail.
  • How proprietary features become portable before standardization.
  • How clients resolve conflicting edits from multiple tools.
  • How source code and telemetry are governed inside an organization.

Feature standardization can also lag experimentation. Editors and servers may use custom methods under namespaced extensions, but each extension reduces portability. A successful experimental feature still needs documented capability negotiation and graceful fallback.

Remote language servers add another layer of limits. Source code, paths, diagnostics, and dependency metadata may cross a network boundary. Teams need authentication, encryption, tenant isolation, data retention rules, latency budgets, and explicit user consent. LSP itself is not a complete security protocol.

Finally, process separation does not guarantee responsiveness. A language server can still monopolize CPU, consume excessive memory, ignore cancellation, or flood the client with updates. Supervision, resource limits, caching, observability, and performance testing remain implementation responsibilities.

In short: LSP creates interoperability, not uniformity. The protocol is the shared contract; product quality still depends on the language engine, client integration, operational controls, and security model.

In Summary

  • JSON-RPC 2.0 defines requests, responses, notifications, IDs, results, and errors in JSON.
  • LSP uses that generic RPC grammar to define code-intelligence operations for editors and language servers.
  • Requests have IDs and receive one correlated response. Notifications omit IDs and receive no response.
  • An LSP session negotiates capabilities through initialize, synchronizes open documents, processes feature calls, then closes with shutdown and exit.
  • Stream-based LSP uses Content-Length framing. The length is the UTF-8 body size in bytes.
  • Correct document versions, URI handling, and position encodings are essential for accurate edits and diagnostics.
  • Capability flags let newer LSP features coexist with older or smaller implementations.
  • A language server is normally stateful even though JSON-RPC describes stateless message envelopes.
  • Tree-sitter, DAP, BSP, MCP, ACP, and OpenRPC solve related but different developer-tool problems.
  • LSP standardizes the interface, while the server's parser, type system, index, cache, and latency determine the quality of the experience.

Frequently asked questions

No. JSON-RPC is the generic request, response, notification, and error format. LSP is a higher-level protocol that defines language-tool methods and types using JSON-RPC 2.0.

LSP does not require HTTP. Many local language servers use stdio or IPC. A custom remote implementation can use HTTP or another authenticated transport, but both peers must agree on framing and security.

A language server is any process that provides language intelligence to a tool. An LSP server is a language server that exposes those capabilities through the Language Server Protocol.

Both sides can send requests. Clients send most language-feature requests. Servers can request configuration, ask a client to apply workspace edits, or initiate other supported operations.

Content-Length gives stream receivers an exact message boundary. JSON objects can be split or combined across reads, so parsing “one read equals one message” is unsafe.

LSP inherited UTF-16-based offsets from common editor and JavaScript string models. Modern clients can advertise other position encodings, but UTF-16 remains the compatibility default if no alternative is negotiated.

No. Tree-sitter is an incremental parsing system. It can run inside an editor or language server, but it does not implement LSP by itself.

No. MCP connects AI applications to tools and context. LSP connects editors to language intelligence. AI-enabled developer tools can use both at the same time.

Ready to launch?

Proxies built for real operations.

For teams that depend on stability, not luck.