Roslynk 3.0.0
dotnet tool install --global Roslynk --version 3.0.0
dotnet new tool-manifest
dotnet tool install --local Roslynk --version 3.0.0
#tool dotnet:?package=Roslynk&version=3.0.0
nuke :add-package Roslynk --version 3.0.0
Roslynk
Roslyn + link — the link between AI test harnesses and Roslyn.
Add the MCP to your AI harness, then type "Open (solution file name)" - that's it!
The biggest time saver you will see is checking for compiler errors and warnings; with Roslynk it is practically instant, no need to wait minutes for the solution to build.
Roslynk gives an MCP client (e.g. Claude) semantic intelligence over C# code via Roslyn:
- diagnostics
- symbol navigation
- find-references
- semantic rename
- code actions
- dead-code detection
- and more
All operating directly on the projects compiled in a loaded solution!
- Transport: HTTP only, bound to loopback (
127.0.0.1/::1). - Host: foreground console on Linux/WSL/macOS; on Windows, also installable as a headless service. No UI.
- Observability: OpenTelemetry, exported via OTLP to a backend you configure.
Connect an MCP client (all platforms)
Requires the .NET 10 SDK on PATH. The stdio verb is a
self-launching bridge: the MCP client spawns it, it starts the shared HTTP daemon on
localhost:6502 if one isn't already running, and pipes the session through. Nothing to launch or
babysit by hand.
First install the global .NET tool from NuGet (no clone, no build needed):
dotnet tool install roslynk -g
Claude Desktop
claude mcp add roslynk -- dnx Roslynk --yes -- stdio
Antigravity (agy)
Using the CLI:
agy mcp add roslynk roslynk stdio
(Or with dnx: agy mcp add roslynk dnx Roslynk --yes -- stdio)
Or manually in ~/.gemini/config/mcp_config.json:
{
"mcpServers": {
"roslynk": {
"command": "roslynk",
"args": ["stdio"]
}
}
}
To attach to an already-running daemon instead of spawning one:
agy mcp add roslynk http://localhost:6502
Manual mcp.json (VS Code, Cursor, Windsurf, and others)
Add the following entry to your MCP configuration file (e.g. .vscode/mcp.json,
~/.cursor/mcp.json, or your AI tool's equivalent):
{
"mcpServers": {
"roslynk": {
"type": "stdio",
"command": "dnx",
"args": ["Roslynk", "--yes", "--", "stdio"]
}
}
}
Important: the
stdioargument at the end is required. Without it the process starts as an HTTP daemon and never responds to the MCPinitializehandshake, causing the client to hang.
Tagged releases (e.g. 1.0.0-beta.1, no v prefix) are packed and pushed to nuget.org by CI.
From a source checkout:
claude mcp add roslynk -- dotnet run --project /path/to/Roslynk/Source/App/Morris.Roslynk.Mcp -- stdio
Or in mcp.json from a source checkout:
{
"mcpServers": {
"roslynk": {
"type": "stdio",
"command": "dotnet",
"args": ["run", "--project", "/path/to/Roslynk/Source/App/Morris.Roslynk.Mcp", "--", "stdio"]
}
}
}
(With a published build, use Morris.Roslynk.Mcp stdio as the command instead — it starts faster.)
DeepSeek Harness (dsh)
DeepSeek Harness bridges MCP servers through @deepseek-ai/dsh-mcp-client — one plugin row per
server. Add Roslynk to your user patch layer at $DSH_HOME/cordis.patch.yml (defaults to
~/.dsh/cordis.patch.yml or %USERPROFILE\.dsh\cordis.patch.yml), which is applied to every profile:
- insert:
- id: mcp-roslynk
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: roslynk
transport: stdio
command: dnx
args: ['Roslynk', '--yes', '--', 'stdio']
Important: a new plugin has to go inside an
insert:list. A bare top-level- id: mcp-roslynkrow is an id-targeted config override of a row that already exists in the composed tree, sodshprintspatch: entry "mcp-roslynk" not foundto stderr and boots with no bridge and no tools.
To attach to an already-running daemon instead of spawning one, use the HTTP transport:
- insert:
- id: mcp-roslynk
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: roslynk
transport: streamable-http
url: http://localhost:6502
The tools reach the model as mcp__roslynk__*. Check the composition without booting a session:
dsh --profile web --dump-config
The row should be listed under a # == <path>/cordis.patch.yml comment, with no not found warning
above it. dsh watches the patch file, so edits reconnect the server without restarting the process.
The daemon outlives individual sessions so Roslyn workspaces stay warm across clients. Its console
output goes to Roslynk/daemon.log under the local application-data folder (~/.local/share on
Linux, ~/Library/Application Support on macOS, %LOCALAPPDATA% on Windows).
🤖 Configure your agent
Connecting the MCP server is only half the job. Left to its own habits an agent will still grep
for call sites, read whole files to find a member, and run dotnet build to check for errors — all
slower and less correct than the equivalent Roslynk call. Teach it the tools once and it stops.
Claude Code: install the skill
This repo ships a ready-made skill in skills/roslynk. Copy it into your skills
folder and Claude will load it automatically whenever a task touches C#:
# user-wide (all projects)
cp -r skills/roslynk ~/.claude/skills/
# or per-project
cp -r skills/roslynk .claude/skills/
Antigravity: install the skill
Antigravity discovers skills in ~/.gemini/config/skills for user-wide skills, and
<projectRoot>/.agents/skills for project ones:
# user-wide (all projects)
cp -r skills/roslynk ~/.gemini/config/skills/
# or per-project
cp -r skills/roslynk .agents/skills/
DeepSeek Harness: install the skill
dsh scans $DSH_HOME/skills (defaults to ~/.dsh/skills) for user-wide skills, and
<projectRoot>/.dsh/skills for project ones — the project root being the nearest ancestor
containing .git:
# user-wide (all profiles, all projects)
cp -r skills/roslynk ~/.dsh/skills/
# or per-project
cp -r skills/roslynk .dsh/skills/
The skill directory is watched, so a fresh copy is picked up without restarting dsh.
Everything else: example system prompt for LLMs
⚡ Pro Tip: Add this system prompt to your AGENTS.md / CLAUDE.md, Cursor Rules, Windsurf
rules, or any AI agent configuration file. It dramatically improves how often and how well your
LLM reaches for Roslynk instead of grepping, reading files, or rebuilding the solution.
You have access to
roslynk, an MCP server holding a live Roslyn compilation of a C# solution. Prefer it over grep, file reads, hand-written edits, anddotnet buildfor anything semantic: it answers from the compiler's symbol model, so it sees partial classes, generated code, inactive#ifbranches and Razor, and it never matches names inside comments or strings.Getting started: call
open_solutionwith the absolute path to the.sln/.slnx; thesolutionIdit returns is the handle every other tool needs. It waits for the load by default (noWait=truepre-warms in the background); every solution tool waits server-side up to the configured timeout, so you normally get the result rather than a retry prompt. Only if the timeout elapses do calls returnerror=Indexing(withprojects=loaded/?); retry shortly. Never fall back to reading or editing files directly. Never callreload_solutionon your own initiative — a file watcher picks up edits automatically (unlessget_solution_statuslists awatchDiagnostic=for that solution); suggest a reload instead.Lifecycle:
open_solution: Load a solution. Parameters:solutionPath(absolute),noWait(return before the load finishes). Idempotent, cheap to repeat.get_solution_status: List every loaded solution and its progress, with the messages behindloadDiagnostics(skipped analyzers, failed project loads) indented under each Ready solution. A missing generator DLL names the project to build; once built, the next call picks it up without a reload. Optional parameters:solutionId(the idopen_solutionechoed, or an absolute path in either slash form, to report only that loaded solution; an id that names no loaded solution returnserror=NotFoundwithcandidate=lines) andallLoadDiagnostics(every message instead of the first 20 per solution). Any solution whose file watcher failed in a way a reload would not fix (such as the Linux inotify watch limit) also listswatchDiagnostic=lines, capped the same way: until they clear, edits on disk may not reach it.reload_solution: Force a from-disk re-evaluation. Parameter:solutionId. Only when the user explicitly asks.Navigation:
find_definition: Go to definition from a cursor position. Parameters:solutionId,filePath,line,column(1-based).get_expression_info: Compiler facts about the expression at a position: type and converted type, bound symbol and selected overload, nullability, constant value, implicit conversion, origin and doc summary; on a declared name, the declared symbol and its type. Parameters:solutionId,filePath(.cs/.razor/.cshtml),line,column(1-based).get_symbol: Identify a symbol and get its declaration. Parameters:solutionId,symbolName(fully-qualified).get_symbol_body: Read a symbol's source text, body included. Parameters:solutionId,symbolName(fully-qualified),includeLeadingTrivia,startItem(page a long declaration; the response reportstruncated=Y/count/nextStartItem),maxSize(page size in characters; default the response budget),part(fetch one declaration of a partial type; oversized parts are listed withomitted=Yand line counts),countOnly(returncount=returned-text lines only, no text).get_members: List a type's members with their locations. Parameters:solutionId,symbolName,includeInherited,nameFilter,includeMethods/includeFields/includeProperties/includeEvents/includeNestedTypes,countOnly. Property/event accessors are not listed as methods: each property/event appears once, as its own leaf, soincludeMethodscovers ordinary methods, constructors and local functions.search_symbols: Find symbols by symbol name — a bare name is a case-insensitive substring, or a dotted container-qualified name (Type.Member, matched as a suffix of the containing type/namespace chain). It matches the symbol's own name, never its members, so useget_membersto list a type's members. Parameters:solutionId,symbolNameFilter,kinds(optional JSON array of kind names such as["Method"]; a JSON-array string is also accepted),maxSize(page size; paging reaches every match),countOnly.multi_query: Run several of the read-only queries above in one call, against a single snapshot of the solution. Parameters:solutionId,operations(each names a tool and carries that tool's own parameters), optionalexpectSnapshot. The whole response stays within the server's response budget (80,000 characters by default): every operation runs, whole results are delivered smallest-first until the budget is filled (in request order), and the rest are named once in the header asretry=<index>:<size>(size = the result's cost as a ratio of the budget, rounded up to 0.1). Sizes ≥ 1.0 must be re-sent alone (they page); sizes summing to ≤ 1.0 fit one continuation batch together. See the skill's multi_query section. For impact questions ("what uses X?", "what breaks if I change X?"), batchfind_references,get_callers,get_callees,find_implementationsandget_type_hierarchyin one call.Relationships:
find_references: Every usage of a symbol. Parameters:solutionId,symbolName,startItem/maxSize(an oversized result pages; nothing is dropped),countOnly.find_reads/find_writes: Where a field, property or parameter is read or written, each location taggedread,assign,compound,increment,ref,outorinit;compound,incrementandrefappear in both. Parameters:solutionId,symbolName(a parameter isNamespace.Type.Method:parameter),startItem/maxSize,countOnly.get_callers: Who calls a member (method, constructor, operator, property, indexer or event), overloads resolved, including calls the compiler inserts (foreach/awaitplumbing,usingdisposal, operators, conversions). Parameters:solutionId,symbolName,countOnly.get_callees: What a member calls - the inverse ofget_callers; methods, constructors, property/event accessors and operators, overload-exact, with referenced-assembly callees grouped per assembly. Parameters:solutionId,symbolName,excludeExternal,countOnly.find_implementations: Implementors/overrides of an interface, abstract or virtual member. Parameters:solutionId,symbolName,countOnly.get_type_hierarchy: Base types, interfaces and derived types. Parameters:solutionId,symbolName,countOnly.Diagnostics:
get_diagnostics: Compile check — this replacesdotnet build. Parameters:solutionId,includeErrors,includeWarnings,includeInfo,includeHidden(all default false),includeAnalyzers, and optional filtersprojectName,filePath,ids(array of strings),summaryOnly,countOnly. The header always reportserrors=/warnings=/infos=/hidden=counts — for the filtered set, with each active filter echoed as afilter=header — so an empty body withfilter=lines means the filter matched nothing, not a clean solution; opt into detail only when they are non-zero.maxSizecaps the listed entries withcount=/truncated=Y;summaryOnly=truereturns one line per id (count, one example location and message) instead. Filters apply after compilation, so a filtered call reuses the cached result. Costly per call (it compiles and analyzes everything the edits affect), so run it once after a task's edits, not after each one. Razor compiler (RZ*) errors are included, reported against the.razor/.cshtmlfile.Code actions:
get_code_actions: List fixes and refactorings at a position. Parameters:solutionId,filePath,line,column,endLine,endColumn.apply_code_action: Apply one by its opaqueactionId(pass it back verbatim). Parameters:solutionId,actionId,checkOnly.apply_code_fix: Fix the diagnostic (compiler or analyzer, e.g.CS0219orIDE0005) at a positionget_diagnosticsreported, no round-trip. Parameters:solutionId,filePath,diagnosticId,line,column(required),checkOnly. When the diagnostic has several fixes it writes nothing and returnserror=Conflictwith acandidateactionId per fix; apply the chosen one withapply_code_action.Editing:
apply_patch: Edit text files with a git unified diff. Parameters:solutionId,patch,baseVersions,checkOnly. Hunks are content-anchored, not line-number-anchored — include enough context that each matches exactly one place. Paths are relative to the solution folder (a unique suffix also resolves); a missing file isNotFound, an ambiguous suffixAmbiguous.rename_parameter: Rename one parameter of a method, constructor or indexer overload with Roslyn: declaration, body, named arguments and<paramref>docs, cascading across the override/interface family. Parameters:solutionId,symbolName(the declaring member),parameterName,newName,checkOnly.rename_symbol: Compiler-correct rename across partial classes, every#ifbranch, every target framework, and.razor/.cshtml. Parameters:solutionId,symbolName,newName,checkOnly.change_signature: Append one optional parameter to an ordinary method and thread an argument into every call site. Parameters:solutionId,symbolName(the method),parameterType,parameterName,defaultValue,callSiteArgument,checkOnly.extract_method: Extract a selection of statements or an expression into a new method or local function with Roslyn's own Extract Method refactoring; writes nothing when Roslyn cannot extract it safely or the result would not compile. Parameters:solutionId,filePath,line,column,endLine,endColumn,methodName,asLocalFunction,checkOnly.remove_unused_usings: Strip unusedusingdirectives (CS8019), preserving surrounding trivia. Parameters:solutionId,filePath(omit for the whole solution),checkOnly.Dead code:
find_dead_code: Unreferenced members with a confidence and a reason — candidates, never verdicts; it deletes nothing. Parameters:solutionId,scope(FQN prefix; use it on large solutions),includePublic,maxSize(scan cap),countOnly.find_dead_conditionals:#ifbranches never compiled under any loaded configuration. Parameters:solutionId,countOnly.Conventions: most tools take a fully-qualified
Namespace.Type.Membername (noglobal::); a generic type may be written without its type parameters (Namespace.Box) when only one arity exists — several arities areerror=Ambiguouswith one candidate per arity, and spellings likeNamespace.Box`1orNamespace.Box<int>resolve too; a local function is named as a member of the method declaring it (Namespace.Type.Method.local, nesting further for one inside another);find_definition,get_expression_infoandget_code_actionsare position-based instead. Every tool that takes afilePath, and every write tool, works on.razorand.cshtmlfiles as well as.cs: positions are given in the Razor file, and edits are written back to it in its own indentation. Responses are a compactkey=valueheader block, a blank line, then a tab-indented outline; booleans areY/N. Errors are header-only:error=Indexing(retry),Ambiguous/NotFound(pick from thecandidate=lines),Stale(re-read and recompute),Conflict(re-run the discovery step). Watch fortruncated=Yand continue withstartItem(or raisemaxSize). PasscountOnly=truewhen you only need totals: the response keeps the headers pluscount=and onecount.<kind>=line per kind present, with no body, notruncated=and nonextStartItem=;startItem/maxSizepage limits are ignored (get_diagnostics' row cap andfind_dead_code's scan cap still apply). Leave defaulted parameters alone unless you need the non-default behaviour, and re-query rather than reusing an earlier response — the solution is edited live.Writing safely: every write tool accepts
checkOnly=trueto preview the changed-file list without writing — use it before a broad rename. Writes are atomic and stale-guarded: if a file changed on disk since Roslynk read it the whole batch is rejected witherror=Stalerather than clobbering the other edit. Successful writes advance the in-memory model immediately, soget_diagnosticsstraight afterwards reflects the change — no reload, no rebuild.The edit loop: make the task's changes →
get_diagnosticsonce at the end (bare call, read the counts) → if errors appeared, re-call withincludeErrors=true→ fix withapply_code_fixat each entry'sline/column(orapply_code_actionwith acandidateit offers) orapply_patch→ repeat.
Run the daemon manually (Linux / WSL / macOS)
For a foreground daemon with visible logs:
./installer/run.sh
Listens on http://localhost:6502. Point your MCP client at that URL (streamable HTTP). Ctrl+C stops it.
Settings
All Roslynk:* settings come from one typed object bound from the Roslynk section and read once at startup, so changing any of them requires a daemon restart. Settings are read from appsettings.json beside the tool executable. Nothing ships that file: appsettings.json.example ships instead, with every key present but commented out and a one-line explanation above it. No file at all — or an untouched copy — means the daemon runs on its code defaults:
| Key | Default | Meaning |
|---|---|---|
Roslynk:Port |
6502 | TCP port the local daemon listens on (loopback only). |
Roslynk:MaxResponseChars |
80,000 | Character cap every tool result is held under; a configured value below the 8,000 minimum is raised to it. |
Roslynk:LoadWaitSeconds |
30 | How long solution-scoped tools wait server-side for an in-flight initial load before returning error=Indexing; 0 means do not wait. |
Roslynk:IdleEvictionMinutes |
30 | Minutes a solution may sit unused before the daemon evicts it; 0 disables eviction. |
Roslynk:EvictionCheckMinutes |
5 | Minutes between idle-eviction sweeps; floored at 1. |
To change a setting, copy or rename appsettings.json.example to appsettings.json beside the executable and uncomment what you need, then restart the daemon. The loader accepts JavaScript/C#-style comments, but VS Code flags them as errors in .json files unless you associate the file with JSONC in your settings:
"files.associations": { "appsettings*.json": "jsonc" }
An appsettings.json beside the executable lives inside the tool's version directory (~/.dotnet/tools/.store/roslynk/<version>/...), so the next dotnet tool update extracts a fresh tool and silently loses your edit. For durable overrides use the equivalent environment variables, which the same configuration reads: Roslynk__Port, Roslynk__MaxResponseChars, Roslynk__LoadWaitSeconds, Roslynk__IdleEvictionMinutes, Roslynk__EvictionCheckMinutes.
A shared daemon serves every client with one set of values — set the smallest response cap your clients need.
Run (Windows)
Foreground dev:
dotnet run --project Source/App/Morris.Roslynk.Mcp
Observing Roslynk
Stop existing Roslynk daemon.
Open PowerShell:
dotnet tool install -g Aspire.Cli
dotnet tool install -g Roslynk
aspire dashboard run --allow-anonymous
- CTRL Click Dashboard link in output to open it in your browser.
- Then click the
TracesIcon in the page that opens. - Leave PowerShell window open.
- Open second PowerShell window:
Env: Otel Exporter Otlp Endpoint = "http://localhost:4317"
Env: Otel Exporter Otlp Protocol = "grpc"
roslynk
You will then see all the Roslynk traces in Aspire.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0 | 169 | 10/22/2026 |
| 2.2.0 | 333 | 10/14/2026 |
| 2.1.0 | 395 | 10/9/2026 |
| 2.0.0 | 312 | 9/25/2026 |
| 1.2.0 | 124 | 9/25/2026 |
| 1.1.0 | 282 | 8/5/2026 |
| 1.0.0 | 144 | 7/20/2026 |
| 1.0.0-beta.2 | 80 | 7/17/2026 |
| 1.0.0-beta.1 | 88 | 7/17/2026 |