The roslyn-language-server is a .NET tool that provides rich language features for C# through the Language Server Protocol. It powers editor integrations including the C# extension for Visual Studio Code and C# Dev Kit. It offers several operations that Claude can use to navigate C# code rather than using grep and friends.
The Roslyn LSP supports many operations defined by the language service protocol, but from what I read only these commands are supported by Claude as of v2.1.62.
goToDefinition: find where a symbol is defined.findReferences: find all references to a symbol.hover: get documentation and type info for a symbol.documentSymbol: list all symbols in a document.workspaceSymbol: search for symbols by name across the entire workspaced.goToImplementation: find implementations of an interface or abstract method.prepareCallHierarchy: get call hierarchy item at a position.incomingCalls: find all callers of a function/method.outgoingCalls: find all functions/methods called by a function.
The instructions below describe how I integrated the roslyn-language-server into Claude Code to give Claude smarter tools for working with C# in the console. My understanding is that Claude in VS Code (especially as part of Copilot) shares VS Code's LSP.
I ran these on Windows using Claude Code v2.1.62. The language server is cross platform so it should work on osx or linux. There might be ways to simplify this but this worked so I stopped fiddling with it.
- Set the env variable
ENABLE_LSP_TOOL=1in~/.claude/settings.json:
{
"env": { "ENABLE_LSP_TOOL": "1" }
}
- Run
dotnet tool install --global roslyn-language-server --prerelease. This will putroslyn-language-server.cmd(or.sh?) in your global tools directory (for me,~/.dotnet/tools/). - Create a local marketplace folder. Name/location of the directory doesn't matter, but I used
~/.claude-custom-plugins/. - Inside your marketplace folder, create a directory
.claude-plugin. - In the new directory create
marketplace.jsonwith the following content. Note that all slashes should be forward slashes. When providing an absolute path start with/(ex: /Users/{{ Your Name }}. Claude was very finicky about the structure of this document. I went through multiple iterations of this and now that it's working I'm scared to touch it.
{
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "local",
"metadata":{
"description": "Local plugins for development and testing.",
"version": "1.0.0",
"license": "MIT"
},
"owner": {
"name": "{{ Your Name }}"
},
"plugins": [
{
"name": "roslyn-ls",
"version": "1.0.0",
"source": "./plugins/roslyn-ls",
"description": "LSP using roslyn-language-service.",
"category": "development",
"author": { "name": "{{ Your Name }}" },
"tags": [ "csharp" ],
"lspServers": {
"roslyn-ls": {
"command": "{{Path to the .NET tools folder}}/roslyn-language-server.cmd",
"args": [
"--stdio",
"--autoLoadProjects",
"--logLevel",
"Information",
"--extensionLogDirectory",
"{{ any directory}}/roslyn-language-service/logs"
],
"transport": "stdio",
"extensionToLanguage": {
".cs": "csharp",
".csx": "csharp",
".cshtml": "csharp"
},
"initializationOptions": {},
"settings": {},
"startupTimeout" : 120000
}
}
}
]
}- Inside your custom plugins directory, next to the
.claude-pluginfolder, create the directoriesplugins/roslyn-ls/(this must match thesourcein themarketplace.jsonfile). - Create the JSON files
.lsp.jsonand.claude-plugin/plugin.json. Your final directory structure should look like this:
{{ custom plugins path }}/
|-- .claude-plugin/
| ʟ marketplace.json
ʟ-- plugins/
ʟ-- roslyn-ls/
|-- .lsp.json
ʟ-- .claude-plugin/
ʟ plugin.json
- The content of
.claude-plugin/plugin.jsonshould look like the JSON below. I don't know if all these are necessary, but it at least works for me. I also think these values need to match their respective properties inmarketplace.json.
{
"name": "roslyn-ls",
"description": "LSP using roslyn-language-service.",
"version": "1.0.0",
"license": "MIT",
"author": {
"name": "{{ Your name }}"
}
}- The content of
.lsp.jsonproperties under thecsharpperoperty should match the properties in theroslyn-lsobject inmarketplace.json.
{
"csharp":
{
"command": "{{Path to the .NET tools folder}}/roslyn-language-server.cmd",
"args": [
"--stdio",
"--autoLoadProjects",
"--logLevel",
"Information",
"--extensionLogDirectory",
"{{ any directory}}/roslyn-language-service/logs"
],
"transport": "stdio",
"extensionToLanguage": {
".cs": "csharp",
".csx": "csharp",
".cshtml": "csharp"
},
"initializationOptions": {},
"settings": {},
"startupTimeout" : 120000
}
}- Start claude and run the command
/plugin marketplace add {{ absolute path to custom plugins folder }}. Ex. for me it was:/plugin marketplace add /Users/{{me}}/.claude-custom-plugins. - Exit claude and run
claude plugin install -s user roslyn-ls. - In claude you can verify the LSP is working with these two commands:
/plugin-> select theInstalledtab, and you should seeroslyn-ls Plugin · local · ✔ enabled.- Tell claude to
run the C# LSP command documentSymbols on {{ path to some C# file }}. You should see a line like● LSP(operation: "documentSymbol", file: "src\Vendors\IVendor.cs")
You should be all set!
@jrusbatch
Thanks for the guide — this got me further than anything else I found for C# LSP in Claude Code.
I followed your steps on WSL2 (Ubuntu) with Claude Code v2.1.71. A few things I ran into that others might hit:
The roslyn-language-server NuGet package requires .NET 10 SDK. On .NET 9, dotnet tool install --global roslyn-language-server --prerelease fails with "Settings file 'DotnetToolSettings.xml' was not found in the package." Installing .NET 10 SDK alongside .NET 9 fixed this. On Ubuntu/WSL2, note that if DOTNET_ROOT points to a user-level .dotnet install, apt install dotnet-sdk-10.0 installs to /usr/lib/dotnet/ but dotnet --list-sdks won't see it — you need to update DOTNET_ROOT to /usr/lib/dotnet.
documentSymbol works, but semantic operations hang. After setup, documentSymbol returns results fine (found 17 symbols in my file). However hover, goToDefinition, findReferences, and workspaceSymbol all hang indefinitely or return empty. This appears to be the same issue as #16360 — Claude Code's LSP client doesn't implement workspace/configuration, client/registerCapability, or window/workDoneProgress/create, which Roslyn needs for full solution analysis. Structural queries work because they don't require the full project graph; semantic queries do and get stuck waiting for responses that never come.
Did you get hover, goToDefinition, or findReferences returning actual results on your setup? Curious if native Windows behaves differently here.