• Terms of Service

Learn Internet Grow

Tech Skills

  • Learn
  • Books
  • Search
  • Create
  • Measure
  • Secure
  • Share
  • Terms of Service

LSP vs DAP: Two Protocols With the Same Shape and Opposite Versioning Philosophies

Filed Under: Search

Two specification pages, published by the same organisation, describe their transport in nearly identical words.

The Language Server Protocol: “The base protocol consists of a header and a content part (comparable to HTTP).” The Debug Adapter Protocol: “The base protocol exchanges messages that consist of a header and a content part (comparable to HTTP).”

Most comparisons of LSP vs DAP stop right there and conclude these are one protocol wearing two hats. One paragraph further down each page, they are not. LSP says the content part “uses JSON-RPC 2.0 to describe requests, responses and notifications.” DAP says it “uses JSON to describe requests, responses, and events.” One term shortened, one term swapped — and that is a different wire format, a different message taxonomy, and, downstream of both, a diametrically opposite policy on version numbers.

Two identical parallel conduits of light carrying visibly different streams, illustrating LSP vs DAP as protocols with the same shape and different contents

The two sentences that are almost the same

Put the two base-protocol definitions side by side and the family resemblance is not something you have to argue for. It is a design copied deliberately, by one team from itself.

LSP (3.18 spec)DAP (overview page)
FramingHeader + content part, “comparable to HTTP”Header + content part, “comparable to HTTP”
Separator\r\n\r\n
Required headerContent-LengthContent-Length
Other headersContent-Type, optional, defaults to application/vscode-jsonrpc; charset=utf-8“Currently only a single header field is supported and required”
Header encodingASCIIASCII
Body encodingUTF-8UTF-8
Body formatJSON-RPC 2.0JSON — explicitly not JSON-RPC
Message kindsrequests, responses, notificationsrequests, responses, events
TransportNot specified — the spec defines the message layer onlyNot specified, but the overview names two patterns: stdio (“single session mode”) and a listening port (“multi session mode”)

Neither protocol says anything about how the bytes get from one process to the other. That silence is load-bearing in both designs: it lets the same message format run over a stdio pipe, a TCP socket, or a Node IPC channel without the specification acquiring an opinion about any of them.

Two mirrored woven banners with identical patterns that diverge sharply near one end, lit by a single shaft of light at the divergence point

Everything above is shared. Below the body-format row, nothing is.

Where the wire formats actually part company

A JSON-RPC 2.0 message carries jsonrpc, method, params, id, and result or error. Here is the example straight out of the LSP 3.18 specification:

Content-Length: ...\r\n\r\n{	"jsonrpc": "2.0",	"id": 1,	"method": "textDocument/completion",	"params": {		...	}}

And here is the example straight out of the DAP overview:

Content-Length: 119\r\n\r\n{	"seq": 153,	"type": "request",	"command": "next",	"arguments": { "threadId": 3 }}

Same header. Same encoding. Completely different envelope. DAP’s base type is ProtocolMessage, and per the protocol’s own JSON schema it requires exactly two fields: seq, an integer that starts at 1 and increments per sender, and type, an enum of request, response, or event. There is no jsonrpc version marker, no method, no params, and no id — the seq does the correlation work an id would do, and the schema says so: “seq can be used to order requests, responses, and events, and to associate requests with their corresponding responses.”

The reason Microsoft gives is not the reason you would expect

It would be tidy to say DAP diverged because debugging’s traffic is different. That is a defensible reading, and it is our reading rather than a sourced one.

What Microsoft actually wrote, in André Weinand’s VS Code engineering post of 7 August 2018, is this:

“For historical reasons, DAP uses a JSON-based wire-format inspired by the (now obsolete) V8 Debugging Protocol. Please note that this format is similar to but not compatible with the JSON-RPC used in the LSP.”

For historical reasons. Not “for architectural fit.” The team inherited a shape from V8’s debugging protocol, kept it, and documented the incompatibility rather than papering over it. The same post frames the broader split as intentional — “We always envisioned decoupling the ‘frontend’ UI from the language specific ‘backend’ implementation as much as possible” — but the choice of envelope is attributed to history, and no primary source found on 2026-09-04 says otherwise.

The domain-fit argument survives as an observation about the result. A debug session is dominated by state changes nobody asked for: a breakpoint is hit, a thread exits, output lands on stdout. DAP’s schema defines 17 event kinds — stopped, continued, exited, terminated, output, breakpoint, module, loadedSource, process, capabilities, invalidated, memory, three progress events, thread, and initialized. Making event a first-class member of the base message type, rather than a notification that happens to travel in the same direction, fits that traffic well. Whether it was chosen for that reason is a separate question, and the record says it was not.

Two containers with identical outer shells cut open to reveal completely different internal structures, one machined and compartmented, one simpler with sparks escaping

One more concrete divergence, easy to miss and expensive to discover at runtime: LSP 3.18 states that “the protocol currently does not support JSON-RPC batch messages” — it uses JSON-RPC, minus one of JSON-RPC’s own features.

Counting both protocols against their own machine-readable specs

Both projects publish a machine-readable model of themselves, which makes the size comparison arithmetic rather than impression. LSP publishes metaModel.json (self-declaring "version": "3.18.0"); DAP publishes debugAdapterProtocol.json, a draft-04 JSON Schema. Counts below are from those two files as fetched on 2026-09-04.

LSP 3.18DAP
Requests6945 (2 of them reverse requests)
Notifications / events26 notifications17 events
Type definitions387 structures192 definitions
Server / adapter capability objectServerCapabilities — 36 propertiesCapabilities — 42 properties, 35 named supports*
Client capability objectClientCapabilities — 6 top-level properties (TextDocumentClientCapabilities alone has 32)InitializeRequestArguments — 17 properties, 10 named supports*

The row that matters is the capability object. DAP’s adapter-side list is larger than LSP’s server-side one — 42 properties against 36 — despite DAP being the smaller protocol on every other axis. That is not an accident of scope. It is the mechanical consequence of a versioning policy, and the point where the two protocols stop being the same thing.

LSP prints a version number. DAP refuses to.

LSP runs a dated release train. Its own changelog lists 19 entries from 3.0 through 3.18.0, and the recent ones are dated to the day: 3.6.0 on 22 February 2018, 3.16.0 on 14 December 2020, 3.17.0 on 10 May 2022, and 3.18.0 on 4 June 2026. Individual fields are tagged in place — the 3.18 document states that its new features carry “a corresponding since version 3.18 text or in JSDoc using @since 3.18.0 annotation” — so you can date any given property to the release that introduced it.

⚠️ 3.18 is the current version, not 3.17. As of 2026-09-04 the 3.17 document describes itself as “the previous 3.17.x version” and the 3.18 document as “the current 3.18.x version” — while also stating, in the same sentence, that it “is under development.” A great deal of secondary writing still calls 3.17 current. It has not been since the 3.18 changelog entry dated 4 June 2026.

DAP took the other road and wrote the reasoning into its overview page:

“The Debug Adapter Protocol defines many features and this number is still growing, albeit slowly. However, the protocol is still at its first version because it was an explicit design goal to support new feature[s] in a completely backward compatible way. Making this possible without version numbers requires that every new feature gets a corresponding flag that lets a development tool know whether a debug adapter supports the feature or not. The absence of the flag always means that the feature is not supported.”

There is a changelog — 71 entries on 2026-09-04, running from 1.0.1 to 1.71.x. The major number has never been anything but 1, and the listing carries no dates. No primary source states whether that minor number tracks the wire protocol or the published schema artifact; the overview’s prose is unambiguous only that runtime compatibility never depends on it.

A segmented viaduct carrying linked carriages toward the horizon beside a single unbroken monolithic arch spanning the same distance

The compatibility mechanism is identical. Only the label differs.

This is where the neat story about “opposite philosophies” has to be qualified, and the qualification is more interesting than the story.

LSP 3.18’s own spec says, in the paragraph immediately before the base protocol section:

“The version of the specification is used to group features into a new specification release and to refer to their first appearance. Features in the spec are kept compatible using so called capability flags which are exchanged between the client and the server during initialization.”

Read that against DAP’s “the protocol is still at its first version” paragraph and the two describe the same runtime mechanism. Neither gates behaviour on a version number. Both negotiate a capability object at initialize and treat absence as “not supported” — LSP: “A missing property should be interpreted as an absence of the capability.” DAP: “It is not necessary to return an explicit false for unsupported capabilities.” Both promise forward tolerance of fields they have never heard of; LSP’s wording is “a ClientCapabilities object literal can have more properties set than currently defined. Servers receiving a ClientCapabilities object literal with unknown properties should ignore these properties.”

So LSP’s “3.18” is a documentation artifact, not a compatibility gate. The two protocols do not disagree about how compatibility works. They disagree about whether to also publish a human-readable label on top of it.

Two doors with very different outward faces, one carrying a blank brass plate, cut away to reveal precisely identical internal latch mechanisms

What the label buys

A version number is a coordination noun. “Pull diagnostics, as of LSP 3.17” is a sentence a maintainer can put in a release note, an issue tracker can search, and a client author can test against. @since 3.17.0 on a field is archaeology you can do in a text editor: it dates a property, which bounds the earliest client that could understand it. Dated releases also make the protocol’s own history legible — multi-root workspace folders arrived in 3.6 in February 2018, more than a year after launch, which reads as an admission about the original design.

What the label costs

It implies a coordination point that does not exist at runtime. “We support LSP 3.17” sounds like a contract and is not one: a server can claim 3.17 and implement four of its features, because the only thing the wire checks is the capability object. The number invites clients to reason about it anyway, and reasoning about it is a bug. It also produces the state the 3.18 document is in on 2026-09-04 — simultaneously “the current 3.18.x version” and “under development.” Both are true. Neither is actionable.

What permanent version 1 buys, and what it costs

DAP’s refusal removes the ambiguity. There is exactly one question a debug adapter can be asked — what flags did you set? — and one honest answer. Nobody can claim a version, or imply a feature set they did not implement. A 2016-era adapter and a 2026-era client negotiate on identical terms to any other pair, because the terms have never changed.

The bill arrives as vocabulary. There is no way to say “this adapter targets the 2023 protocol,” because the protocol has no such noun; debugging an integration means comparing flag sets. And because every feature must be its own flag forever, the flag set grows monotonically and can never be consolidated — 35 supports* booleans on the adapter side, 10 more on the client side, and no mechanism to retire one. LSP can deprecate a field and date the deprecation. DAP’s flags are load-bearing forever by construction.

Two facing ranks of plain pennant flags across an open channel, some raised and lit, others furled, with one rank noticeably denser than the other

Both protocols also allow capabilities to change mid-session, and the asymmetry there is telling. LSP does it with client/registerCapability and client/unregisterCapability — actual requests, with responses, that can fail. DAP does it with the capabilities event, and the schema attaches a disclaimer: “this event has a hint characteristic: a client can only be expected to make a ‘best effort’ in honoring individual capabilities but there are no guarantees.” A protocol with no version number cannot afford a renegotiation that fails, so it does not have one.

The coordinate system is the tell

If you want a single test for which philosophy aged better, do not look at the changelogs. Look at what each protocol treated as negotiable on day one.

DAP’s initialize request lets the client declare its own coordinate conventions. From the schema: linesStartAt1 — “If true all line numbers are 1-based (default)” — columnsStartAt1, worded the same for columns, and pathFormat, an enum of path or uri that “Determines in what format paths are specified.” The adapter is expected to speak whichever convention the tool on the other end already uses. The overview lists these among what the tool tells the adapter: “the format of file paths, native or uri, whether line and column values are 0 or 1 based, the locale used by the development tool.”

LSP froze its coordinates into a type. Position has exactly two properties in the 3.18 metamodel: line, documented as “Line position in a document (zero-based)”, and character, “Character offset on a line in a document (zero-based).” Zero-based is not negotiable and never has been. The unit of character — a UTF-16 code unit, inherited from the JavaScript strings of the protocol’s first client — took six years and a new negotiated capability in 3.17 to make configurable at all, and remains the mandatory default in 3.18; that saga has its own article and is not this one’s.

Two surveyor's grids over the same terrain, one welded rigidly with a fused origin corner and one strung on adjustable cord with a shifted origin

The pattern is consistent with each protocol’s versioning stance, and it is the strongest evidence available for either. DAP treated its own conventions as data and put them behind flags. LSP treated its conventions as structure and put them in a type. A release train is what you need when your primitives are frozen: shipping “3.17” is how you introduce a negotiated encoding into a type that was never designed to have one. Permanent version 1 is affordable precisely because DAP left itself fewer things it could ever need to break.

Which one aged better

Neither, cleanly — and the useful answer is not a winner.

DAP’s discipline held. A protocol that never printed a version has never had to defend one, has no migration guide, and has no deprecated-since table. Its 71 changelog entries are almost entirely clarifications and additive flags.

LSP’s release train did work the flags could not. It gave a large ecosystem — clients and servers written by people with no relationship to each other, let alone to Microsoft — a shared vocabulary for a moving target across a decade. Coordination among strangers has a cost, and a version number is a cheap way to pay it.

The transferable rule is narrower than either protocol’s marketing:

  • Capability flags are the compatibility mechanism. Both protocols agree on this completely. If your interface gates behaviour on a version number, you have built a lookup table your peers must keep in sync, and you will regret it.
  • A version number is a communication artifact, and it is worth something. Publishing one is not a design failure. Letting anything at runtime read it is.
  • The decision that actually binds you is which primitives you refuse to negotiate. DAP made line-base, column-base and path format into flags and never needed a major version. LSP made character offsets into a type and needed six years and a spec release to loosen one field. That choice is made before your first client ships, and it is the one you cannot take back.

❓ Frequently Asked Questions

Is DAP built on JSON-RPC?

No. This is the most common error in write-ups comparing the two. DAP uses a JSON body with a Content-Length header — the same framing as LSP — but its message envelope is a ProtocolMessage with seq and type fields, not JSON-RPC’s method/params/id/result. Microsoft states the incompatibility directly: the format is “similar to but not compatible with the JSON-RPC used in the LSP.” A JSON-RPC library will not parse DAP traffic.

What is the current version of LSP?

3.18. As of 2026-09-04 the 3.18 specification describes itself as “the current 3.18.x version” and 3.17 describes itself as “the previous 3.17.x version.” The 3.18.0 changelog entry is dated 4 June 2026. Many secondary sources still name 3.17, which was current from 10 May 2022 until 3.18 landed.

What version of DAP is current?

DAP does not have a current version in the sense LSP does. Its overview states the protocol “is still at its first version because it was an explicit design goal to support new feature[s] in a completely backward compatible way.” The changelog carries an incrementing number — 1.71.x as of 2026-09-04, across 71 entries beginning at 1.0.1 — but no primary source states that a client should gate behaviour on it, and the overview is explicit that compatibility is carried by capability flags instead.

Can one process implement both protocols?

Yes, and several do — but not with one message parser. Because the envelopes differ, a tool that speaks both needs two codecs behind one transport layer: the same Content-Length framing on both connections, a JSON-RPC decoder on one, a seq/type decoder on the other.

Why does DAP have more capability flags than LSP has server capabilities?

Because it has no other place to put a feature. LSP can group additions into a dated release and describe them in prose; DAP’s design goal requires that “every new feature gets a corresponding flag.” The result on 2026-09-04 is 42 properties on DAP’s Capabilities object — 35 of them supports* booleans — against 36 on LSP’s ServerCapabilities, even though DAP defines 45 requests to LSP’s 69.

Which protocol should I model a new one on?

Model the parts they agree on: an out-of-process peer, a length-prefixed header over an unspecified transport, and a capability object exchanged once at initialize where a missing key means “not supported.” That much is settled by two independent protocols reaching the same answer. The version-number question is genuinely open, and the deciding factor is not protocol design at all — it is whether your ecosystem needs a noun to coordinate around. Then spend real time on which primitives you are freezing, because that is the decision neither approach can rescue you from.


📚 Recommended Resources

Disclosure: some links below are Amazon affiliate links. If you buy through them, this site earns a small commission at no extra cost to you.

For this subject the free primary material genuinely beats the books, and saying so is more useful than padding a list. Both protocols are small enough to read end to end in an afternoon, and both publish a machine-readable model you can query instead of trusting anyone’s summary — including this one.

Start with the specifications themselves. The LSP 3.18 specification is a single page; its base-protocol and initialize sections are the two worth reading closely. The DAP overview is shorter still, and its “Initialization” section contains the design-goal statement that explains everything about the protocol’s versioning.

Then read the models, not the prose. LSP’s metaModel.json and DAP’s debugAdapterProtocol.json are the authoritative artifacts. Every count in this article came from loading those two files and counting, which took less time than finding a blog post that claimed to know. If you are integrating with either protocol, a twenty-line script against these files answers questions no article can.

For the version-policy argument itself, the DAP changelog is the most instructive page of all: 71 entries that never once needed a major-version bump. Read it beside LSP’s own dated change log and the trade-off makes itself.

Free, and worth more than any book if you only have an hour: open a language server and a debug adapter with tracing enabled and read the actual traffic. Every serious LSP client can dump the JSON-RPC stream, and every serious DAP client can dump the seq/type stream. Ten minutes of real messages teaches the difference between the two envelopes better than any description of them.

Two books that apply, though neither is about these protocols

Both are about the problem underneath them — designing an interface between systems that ship independently and cannot be upgraded in lockstep.

  • Designing Data-Intensive Applications — Martin Kleppmann, O’Reilly Media. Chapter 4, “Encoding and Evolution,” is the best treatment in print of the exact question LSP and DAP answer differently: how do you change a message format when the two ends ship on their own schedules? Kleppmann’s forward- and backward-compatibility framing is the vocabulary this article is written in.
  • Building Microservices, 2nd edition — Sam Newman, O’Reilly Media. Its chapters on versioning and on avoiding lockstep releases make the same argument DAP’s design goal makes, from the service side rather than the protocol side. Read it for the failure modes of the alternative: the coordinated upgrade you can never quite schedule.

Newsletter

Tags

Agentic AI Agentic Development AI AI Agents AI Coding Agents Artificial intelligence Backlinks Blockchain Bounded Context Browser ChatGPT Plugins Claude Code CLPR Context Engineering Cross-Chain Cryptocurrency Developer Tools Distributed Systems DLT Domain-Driven Design Email Google Search HBAR Hedera Hedera Hashgraph Internet LLM Machine Learning MCP Microservices Model Context Protocol News NLP Orchestration Photoshop Productivity Saga Pattern SEO Smart Contracts Software Architecture software engineering Twitter Typescript Web3 Writing

Recent Articles

  • QUIC and HTTP/3: The Protocol That Won Is Being Routed Around October 11, 2026
  • The Mark I Perceptron: The Machine That Learned to See October 11, 2026
  • GraphRAG vs Vector RAG: Knowledge Graphs for AI, Explained October 11, 2026
  • Cardano Tokenomics: How ADA’s Supply Actually Works October 10, 2026
  • Transformer Encoder vs Decoder: How to Read the Diagram October 10, 2026
  • Nobody Checks the Verified Badge: The Real State of Git Commit Signing October 10, 2026

Copyright © 2026 · News Pro Theme On Genesis Framework · WordPress · Log in

Content on this site is produced with the assistance of Large Language Models and edited by a human. It is intended for educational purposes — facts may occasionally be incorrect. If you spot an error, please email Larionovdmitri@learninternetgrow.com.

Affiliate Disclosure: This site contains affiliate links to Amazon and other partners. If you purchase through these links, LearnInternetGrow.com may earn a commission at no additional cost to you. We only recommend products we genuinely believe in.

Privacy Policy · Terms of Service