# csharp-language-server-protocol: the C# protocol stack behind OmniSharp

> OmniSharp.Extensions.LanguageProtocol is a set of MIT licensed .NET packages that implement the Language Server Protocol, the Debug Adapter Protocol and JSON-RPC in C#, shipped as the building blocks of OmniSharp. It suits teams writing a C# language server or editor client, and its 1.0 migration is the thing to read before you depend on it.

**OmniSharp/csharp-language-server-protocol** — Language Server Protocol in C#

- Repository: https://github.com/OmniSharp/csharp-language-server-protocol
- Stars: 641 · Forks: 110
- Language: C#
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/omnisharp-csharp-language-server-protocol

## A protocol library, not a language server

This repository is an implementation of the Language Server Protocol written entirely in C# for .NET. That single sentence sets the scope, and it is important to be clear about it, because the name and the OmniSharp organisation can suggest something bigger.

What is here is the protocol layer: the request and response types, the notification types, the JSON-RPC transport, the request dispatch machinery, and the Debug Adapter Protocol equivalents. The C# analysis engine that makes a language server useful is not in this repository. That lives in OmniSharp and is the consumer of this code.

The audience is therefore narrower than a language server user. It is a developer writing a language server in .NET, writing a client that speaks LSP to a server, or building an editor integration that needs the protocol types generated in C# rather than hand-written.

The project states its ambition for the language server side as 100% conformance with the protocol itself, and it acknowledges the difficulty of that goal directly, blaming the nature of the protocol's TypeScript upbringing. That is an accurate and useful admission. The LSP specification was written around a TypeScript type system, and the shapes it produces, unions of optional properties with discriminating fields, translate into C# in ways that are workable but not always elegant.

The organisation matters too. The project is supported by the .NET Foundation, and the copyright is held by the .NET Foundation and contributors, which is a meaningful signal about long-term stewardship for a library that other tool authors build on.

## JSON-RPC, dispatch, and the absence of a mediator package

The request dispatch section is the most architecturally interesting part of the documentation, and it describes a deliberate simplification.

Internally the library revolves around its own request, response, handler and pipeline contracts, living in the OmniSharp.Extensions.JsonRpc namespace. Incoming JSON-RPC messages are dispatched to registered handlers without requiring an external mediator package.

That last clause is the design decision. A common pattern for message handling in .NET is to route everything through a mediator, and that pattern has known costs, a dependency on a specific mediator library, and a debugging story where the call stack tells you nothing about which handler ran. This library keeps the dispatch inside itself, so there is one fewer package to take, one fewer version to track, and a stack trace that points at your own code.

The JSON-RPC implementation is described as designed to implement the specification as correctly as possible, and it is available on its own. The documentation points at docs/jsonrpc.md for using the JsonRpcServer by itself, which tells you the layering is deliberate. You can take the transport without the LSP types, and you can take the LSP types without the transport if you are implementing the protocol over a different channel.

The same pattern repeats for the language protocol and the debug adapter protocol. docs/lsp.md covers using the LanguageClient and LanguageServer on their own, and docs/dap.md covers the DebugAdapterClient and DebugAdapterServer. Three protocols, each usable as a layer, each documented as a standalone.

## The package split is the API you code against

The NuGet table in the README is the map of the codebase, and it shows a deliberate decomposition into many small packages rather than one large one.

The language server side is OmniSharp.Extensions.LanguageProtocol, which holds the protocol types, with OmniSharp.Extensions.LanguageProtocol.Proposals for the optional and experimental parts of the specification, and OmniSharp.Extensions.LanguageProtocol.Testing for the test helpers. The server implementation is OmniSharp.Extensions.LanguageServer, with a separate OmniSharp.Extensions.LanguageServer.Shared package. The client side is OmniSharp.Extensions.LanguageClient.

The debug adapter side mirrors it: OmniSharp.Extensions.DebugAdapter, .DebugAdapter.Client, .DebugAdapter.Server, .DebugAdapter.Proposals, .DebugAdapter.Shared and .DebugAdapter.Testing.

The JSON-RPC layer has its own set, OmniSharp.Extensions.JsonRpc, plus OmniSharp.Extensions.JsonRpc.Generators and OmniSharp.Extensions.JsonRpc.Testing.

The Proposals packages deserve a note, because the name is doing real work. The LSP specification has a large surface of features that servers are not expected to implement, and the convention in the community has been to keep those out of the main type surface. Splitting them into a separate package lets a server reference only the core types, and the proposals package pulls in the rest.

This is a lot of packages, and it is a real cost of adoption. Each one is a version to track, and a mismatch between the LanguageProtocol and LanguageServer package versions is the kind of thing that produces a confusing restore error rather than a clear message. For a consumer, picking the matching set from the same release is the discipline this layout demands.

## Reusing Microsoft.Extensions rather than inventing its own

The library does not roll its own configuration, dependency injection or logging. It re-uses some of the common packages used by .NET Core, and the README names them: Microsoft.Extensions.Configuration for common configuration, Microsoft.Extensions.DependencyInjection for common Dependency Injection abstractions, and Microsoft.Extensions.Logging for logging.

That is a deliberate alignment with the platform, and it has a concrete consequence for how the library is hosted. Because the abstractions are the standard ones, a language server built on this library is configured and wired the same way as any other modern .NET service. You use the familiar configuration builder, the familiar service collection, and the familiar logging factory, and the logging output goes wherever your host application already sends it.

For a library that other tool authors embed in their own processes, this is the difference between being easy to adopt and being a second-class citizen in someone else's startup sequence. A language server that used its own configuration abstraction and its own logger would need the host to bridge two worlds. This one disappears into the host's.

The DependencyInjection entry deserves a slight qualification, since the wording is about abstractions rather than a container. The library defines and consumes the standard interfaces, and the host application is the one that registers the concrete container. That is the correct boundary for a library, since choosing a container is a decision for the application.

The practical takeaway is that wiring up a server on this stack should look like the setup of any other .NET application, which is precisely the point of reusing those packages.

## Building the repository with the Nuke build

The getting started section is deliberately terse, and the whole of it is four lines:

```bash
1. git clone
2. run `build.ps1` / `build.sh`
3. ...
4. Profit
```

That is a joke about how little else there is to it, but the build is real and it is driven by Nuke, whose bootstrap scripts are exactly the build.ps1 and build.sh the README tells you to run.

The repository layout shows what the build actually is. There is an LSP.sln solution, a .build/ directory, a .nuke/ directory, build.cmd, build.ps1 and build.sh, plus a Directory.Build.props, a Directory.Build.targets and a Directory.Packages.props for shared MSBuild properties and central package version management. The build is driven by Nuke, which is a .NET build automation tool whose bootstrap scripts are exactly the build.ps1 and build.sh the README tells you to run. The nuget.config, global.json, GitVersion.yml and GitReleaseManager.yaml files handle package sources, SDK pinning and version and release automation.

The Node side of the build is small and has a specific job. The package.json is marked private, with husky for git hooks, lint-staged and prettier plus the prettier XML plugin. The plugin is the giveaway, since the LSP specification is distributed as a set of TypeScript definitions and a meta model, and the protocol types in this library are generated from them. There are two files that record what the generator consumed, language-server-protocol.sha.txt and debug-adapter-protocol.sha.txt, which are hashes of the upstream specification files. Those hashes are the mechanism for tracking which revision of the LSP spec the types were generated from, and they are the answer to the question of how a generated library stays current with a moving specification.

The sample directory contains SampleServer, and there are also atom-testextension and vscode-testextension directories, which suggests the protocol implementations are exercised from real editor extensions rather than only from unit tests.

## Testing, build pipelines and the shape of the project

The status section shows a project that runs its builds in more than one place. There is an Azure Pipelines status badge and a GitHub Actions status badge, each with a history badge, plus a codecov badge for code coverage. The build files in the repository match that, with an .azure-pipelines.yml, an azure-pipelines.nuke.yml and a .travis.yml, the last being a leftover from an earlier era. The .appveyor.yml is there too, along with a .github/ directory for the current workflow.

Multiple pipelines is usually a sign of a project that serves more than one community, and for a .NET Foundation project that makes sense. The GitHub Actions badge points at a workflow named ci.yml. The .husky/ directory and the lint-stagedrc.js file show that pre-commit hooks are part of the workflow, and the .prettierrc and .editorconfig files point at a shared formatting setup.

The benchmarks/ directory is worth noting separately from the tests. A protocol library is not usually where you would expect benchmarks, but the request dispatch and the JSON-RPC serialisation are on the hot path of every keystroke-triggered request, and the JSON-RPC generators package suggests source generation is used to keep that path fast. Whether the benchmarks cover the parts you care about is something to check in the repository.

The documentation is unusually well organised for a project of this size, with separate documents per protocol layer plus a separate architecture.md at the repository root, and the migration guide for the 1.0 change.

## The 1.0 migration is the thing to read first

The README states that applications upgrading from 0.19.x should follow the 1.0 migration guide at docs/migration-v1.md. Given that the current release line in the tags is v0.19.11, that sentence is the most important operational detail in the whole document.

The shape of the change is visible in the package names, and the NuGet table in the README shows the current layout. The server side is OmniSharp.Extensions.LanguageServer, the client side is OmniSharp.Extensions.LanguageClient, the shared protocol types are OmniSharp.Extensions.LanguageProtocol, and the proposals and testing helpers are separate packages. The Debug Adapter packages follow the same split, with .DebugAdapter, .DebugAdapter.Client, .DebugAdapter.Server, .DebugAdapter.Proposals, .DebugAdapter.Shared and .DebugAdapter.Testing.

A package and namespace rename is the kind of change a search and replace handles for the type names but not for the assembly references in a project file, and not for the extension method namespaces that consumers import, which is why the project keeps a written guide for it.

A second thing to weigh is the version history itself. The release tags are v0.19.11 from 2026-10-07, v0.19.10 from 2026-06-30 and v0.19.9 from 2023-09-21. The last push was on 2026-10-14, and the repository is not archived, so work is continuing. A 1.0 release line exists and the 0.19.x line is the one you are most likely to find documented in older blog posts and tutorials.

The licence is MIT, with the copyright held by the .NET Foundation and contributors. The project also operates under a Contributor Covenant code of conduct, and contributions are covered by the .NET Foundation contribution licence agreement.

## Conclusion

csharp-language-server-protocol fits a team that is writing a C# language server, a C# language client, or a custom editor integration and wants the protocol types, the JSON-RPC transport and the request dispatch already done in a maintained .NET library. It is the wrong choice for a project that only needs to talk to an existing C# language server, since the OmniSharp server itself already exposes that over LSP, and for anyone unwilling to follow a 1.0 migration. Before adopting it, read docs/migration-v1.md if you are coming from 0.19.x, decide whether you want the language protocol alone or the debug adapter packages as well, and confirm your target framework matches the packages you pick, since the split into OmniSharp.Extensions.LanguageProtocol, LanguageServer, LanguageClient and their Proposals and Testing siblings is the API you will be coding against.

## FAQ

### What does OmniSharp csharp-language-server-protocol contain?

It is an implementation of the Language Server Protocol written entirely in C# for .NET, together with a Debug Adapter Protocol implementation and a JSON-RPC layer. It provides the protocol types, transport and dispatch, not the C# analysis engine, which lives in OmniSharp itself.

### Which NuGet packages does csharp-language-server-protocol publish?

The language server packages OmniSharp.Extensions.LanguageServer, .LanguageServer.Shared, .LanguageClient and .LanguageProtocol with separate .Proposals and .Testing packages, matching Debug Adapter packages, plus OmniSharp.Extensions.JsonRpc with .JsonRpc.Generators and .JsonRpc.Testing.

### How do I migrate from 0.19.x to 1.0?

The README directs upgrading applications to the 1.0 migration guide at docs/migration-v1.md. The change is a package and namespace rename, with the server, client and shared protocol types split into separate assemblies with Proposals and Testing helpers moved out.

### Does csharp-language-server-protocol need a mediator package for request dispatch?

No. The library uses its own request, response, handler and pipeline contracts in OmniSharp.Extensions.JsonRpc and dispatches incoming JSON-RPC messages to registered handlers without requiring an external mediator package.

### What licence is csharp-language-server-protocol released under?

It is provided as-is under the MIT license, with copyright held by the .NET Foundation and contributors, and the project is supported by the .NET Foundation. The last push was on 2026-10-14 and the repository is not archived.

## Sources

- [Issues](https://github.com/OmniSharp/csharp-language-server-protocol/issues)
- [License: MIT](https://github.com/OmniSharp/csharp-language-server-protocol/blob/master/LICENSE)
- [OmniSharp/csharp-language-server-protocol on GitHub](https://github.com/OmniSharp/csharp-language-server-protocol)
- [README](https://github.com/OmniSharp/csharp-language-server-protocol/blob/master/README.md)
- [Releases](https://github.com/OmniSharp/csharp-language-server-protocol/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/omnisharp-csharp-language-server-protocol
