roslyn-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/LspClient.js +131 -0
- package/dist/index.js +94 -0
- package/dist/test.js +25 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Roslyn MCP Server
|
|
2
|
+
|
|
3
|
+
A robust, independent Model Context Protocol (MCP) server integration for Microsoft's **Roslyn Language Server (C#)**.
|
|
4
|
+
|
|
5
|
+
## Why use this project?
|
|
6
|
+
The original Go-based MCP proxies typically encounter intense difficulties when operating with Microsoft's `StreamJsonRpc`. Common crashes (like `InvalidOperationException` due to null payloads, or `Document is null` on hovering) render the native MCP connections useless on huge codebases.
|
|
7
|
+
|
|
8
|
+
This Node.js / TypeScript wrapper introduces a foolproof communication bridge that forcefully conforms to the rigorous LSP standards:
|
|
9
|
+
* Features **`didOpen` buffer loading**, ensuring large C# solutions will natively understand dynamic diagnostic requests without crashing.
|
|
10
|
+
* Includes rigorous fallback configurations tailored to seamlessly route tools like hover, definition, and reference symbol queries to your underlying MCP client (like Claude/Cursor).
|
|
11
|
+
|
|
12
|
+
## Global Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g roslyn-mcp-server
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Pre-requisites
|
|
19
|
+
You must have the official `.NET 8 (or above) SDK` installed along with the `roslyn-language-server` global tool.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
dotnet tool install -g Microsoft.CodeAnalysis.LanguageServer
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## How to use as an MCP Server
|
|
26
|
+
|
|
27
|
+
Once installed globally, you can effortlessly configure it within your favorite Agent configurations (e.g., `mcp_config.json`, `claude_desktop_config.json`, Cursor etc.):
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"csharp-lsp": {
|
|
33
|
+
"command": "roslyn-mcp-server",
|
|
34
|
+
"args": ["<OPTIONAL_ABSOLUTE_PATH_TO_CSHARP_PROJECT>"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
> **Note:** If you don't supply `<OPTIONAL_ABSOLUTE_PATH_TO_CSHARP_PROJECT>`, the server will gracefully fallback to the Current Working Directory (CWD). This makes it infinitely portable!
|
|
41
|
+
|
|
42
|
+
## Exposed MCP Tools
|
|
43
|
+
|
|
44
|
+
* **`csharp_hover`**: Fetches the semantic type or variable definitions in markdown formats precisely at line/column boundaries.
|
|
45
|
+
* **`csharp_definition`**: Retrieves the concrete origination URI.
|
|
46
|
+
* **`csharp_references`**: Locates all occurrences or assignments of methods/properties across the entire initialized app constraints.
|
|
47
|
+
* **`csharp_diagnostics`**: Summarizes the syntax errors and build warnings on a component.
|
|
48
|
+
* **`csharp_workspace_symbol`**: Queries fuzzy name references broadly resolving the file locations across large infrastructures.
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
1. `npm install`
|
|
53
|
+
2. `npm run build`
|
|
54
|
+
3. Execute tests via: `node dist/test.js` or `npm start`
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import * as rpc from 'vscode-jsonrpc/node.js';
|
|
3
|
+
import * as lsp from 'vscode-languageserver-protocol';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
export class LspClient {
|
|
7
|
+
workspacePath;
|
|
8
|
+
command;
|
|
9
|
+
args;
|
|
10
|
+
process;
|
|
11
|
+
connection;
|
|
12
|
+
diagnostics = new Map();
|
|
13
|
+
openedDocuments = new Set();
|
|
14
|
+
constructor(workspacePath, command = 'C:\\Users\\PC2\\.dotnet\\tools\\roslyn-language-server.cmd', args = ['--stdio', '--logLevel=Information']) {
|
|
15
|
+
this.workspacePath = workspacePath;
|
|
16
|
+
this.command = command;
|
|
17
|
+
this.args = args;
|
|
18
|
+
// Start the LSP process
|
|
19
|
+
this.process = spawn(this.command, this.args, { shell: true });
|
|
20
|
+
if (!this.process.stdin || !this.process.stdout) {
|
|
21
|
+
throw new Error("LSP process stdio could not be started.");
|
|
22
|
+
}
|
|
23
|
+
// Log stderr output from LSP for debugging
|
|
24
|
+
this.process.stderr?.on('data', (data) => {
|
|
25
|
+
console.error(`[LSP STDERR]: ${data.toString()}`);
|
|
26
|
+
});
|
|
27
|
+
// Create the JSON-RPC connection
|
|
28
|
+
this.connection = rpc.createMessageConnection(new rpc.StreamMessageReader(this.process.stdout), new rpc.StreamMessageWriter(this.process.stdin));
|
|
29
|
+
// Listener (Push Event) to catch and store diagnostic reports
|
|
30
|
+
this.connection.onNotification(lsp.PublishDiagnosticsNotification.method, (params) => {
|
|
31
|
+
this.diagnostics.set(params.uri, params.diagnostics);
|
|
32
|
+
});
|
|
33
|
+
this.connection.listen();
|
|
34
|
+
}
|
|
35
|
+
ensureDocumentOpen(filePath) {
|
|
36
|
+
const uri = this.pathToUri(filePath);
|
|
37
|
+
if (!this.openedDocuments.has(uri)) {
|
|
38
|
+
try {
|
|
39
|
+
const content = readFileSync(filePath, 'utf8');
|
|
40
|
+
this.connection.sendNotification(lsp.DidOpenTextDocumentNotification.method, {
|
|
41
|
+
textDocument: {
|
|
42
|
+
uri: uri,
|
|
43
|
+
languageId: 'csharp',
|
|
44
|
+
version: 1,
|
|
45
|
+
text: content
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
this.openedDocuments.add(uri);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
console.error(`ERROR: Failed to open document ${filePath} natively:`, e);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async initialize() {
|
|
56
|
+
const rootUri = pathToFileURL(this.workspacePath).href;
|
|
57
|
+
const initParams = {
|
|
58
|
+
processId: process.pid,
|
|
59
|
+
rootUri: rootUri,
|
|
60
|
+
capabilities: {
|
|
61
|
+
workspace: {
|
|
62
|
+
symbol: {
|
|
63
|
+
dynamicRegistration: true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
textDocument: {
|
|
67
|
+
hover: { dynamicRegistration: true },
|
|
68
|
+
definition: { dynamicRegistration: true },
|
|
69
|
+
references: { dynamicRegistration: true },
|
|
70
|
+
publishDiagnostics: { relatedInformation: true },
|
|
71
|
+
synchronization: {
|
|
72
|
+
dynamicRegistration: true,
|
|
73
|
+
willSave: true,
|
|
74
|
+
willSaveWaitUntil: true,
|
|
75
|
+
didSave: true
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
workspaceFolders: [
|
|
80
|
+
{
|
|
81
|
+
uri: rootUri,
|
|
82
|
+
name: 'workspace'
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
// Send initialize request
|
|
87
|
+
await this.connection.sendRequest(lsp.InitializeRequest.method, initParams);
|
|
88
|
+
// Send initialized notification to signal readiness
|
|
89
|
+
await this.connection.sendNotification(lsp.InitializedNotification.method, {});
|
|
90
|
+
}
|
|
91
|
+
async hover(filePath, line, column) {
|
|
92
|
+
this.ensureDocumentOpen(filePath);
|
|
93
|
+
return this.connection.sendRequest(lsp.HoverRequest.method, {
|
|
94
|
+
textDocument: { uri: this.pathToUri(filePath) },
|
|
95
|
+
// LSP positions are 0-based.
|
|
96
|
+
position: { line, character: column }
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async definition(filePath, line, column) {
|
|
100
|
+
this.ensureDocumentOpen(filePath);
|
|
101
|
+
return this.connection.sendRequest(lsp.DefinitionRequest.method, {
|
|
102
|
+
textDocument: { uri: this.pathToUri(filePath) },
|
|
103
|
+
position: { line, character: column }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async references(filePath, line, column) {
|
|
107
|
+
this.ensureDocumentOpen(filePath);
|
|
108
|
+
// CRITICAL FIX: The 'context' object must not be omitted to prevent Roslyn crashes
|
|
109
|
+
return this.connection.sendRequest(lsp.ReferencesRequest.method, {
|
|
110
|
+
textDocument: { uri: this.pathToUri(filePath) },
|
|
111
|
+
position: { line, character: column },
|
|
112
|
+
context: { includeDeclaration: true }
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async workspaceSymbol(query) {
|
|
116
|
+
return this.connection.sendRequest(lsp.WorkspaceSymbolRequest.method, {
|
|
117
|
+
query
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
getDiagnostics(filePath) {
|
|
121
|
+
this.ensureDocumentOpen(filePath);
|
|
122
|
+
return this.diagnostics.get(this.pathToUri(filePath)) || [];
|
|
123
|
+
}
|
|
124
|
+
pathToUri(filePath) {
|
|
125
|
+
return pathToFileURL(filePath).href;
|
|
126
|
+
}
|
|
127
|
+
shutdown() {
|
|
128
|
+
this.connection.end();
|
|
129
|
+
this.process.kill();
|
|
130
|
+
}
|
|
131
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { LspClient } from "./LspClient.js";
|
|
6
|
+
import * as lsp from "vscode-languageserver-protocol";
|
|
7
|
+
// The target workspace folder is taken as an argument, defaults to the current execution directory
|
|
8
|
+
const workspacePath = process.argv[2] ?? process.cwd();
|
|
9
|
+
if (!workspacePath) {
|
|
10
|
+
console.error("ERROR: Could not resolve the workspace path.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
// Start the official server via LspClient
|
|
14
|
+
const lspClient = new LspClient(workspacePath);
|
|
15
|
+
// Define the MCP Bridge Server
|
|
16
|
+
const mcpServer = new McpServer({
|
|
17
|
+
name: "roslyn-mcp-server",
|
|
18
|
+
version: "1.0.0"
|
|
19
|
+
});
|
|
20
|
+
// TOOL 1: Hover (Go To Definition hover info etc.)
|
|
21
|
+
mcpServer.tool("csharp_hover", "Returns the type and documentation for the variable/method at the specified line and column", {
|
|
22
|
+
filePath: z.string().describe("Absolute path to the C# file"),
|
|
23
|
+
line: z.number().describe("Line number (1-based)"),
|
|
24
|
+
column: z.number().describe("Column number (1-based)")
|
|
25
|
+
}, async ({ filePath, line, column }) => {
|
|
26
|
+
// AI usually sends 1-based lines, LSP works 0-based.
|
|
27
|
+
const lspLine = Math.max(0, line - 1);
|
|
28
|
+
const lspCol = Math.max(0, column - 1);
|
|
29
|
+
const hover = await lspClient.hover(filePath, lspLine, lspCol);
|
|
30
|
+
if (!hover || !hover.contents) {
|
|
31
|
+
return { content: [{ type: "text", text: "Hover info not found." }] };
|
|
32
|
+
}
|
|
33
|
+
let hoverText = "";
|
|
34
|
+
if (lsp.MarkupContent.is(hover.contents)) {
|
|
35
|
+
hoverText = hover.contents.value;
|
|
36
|
+
}
|
|
37
|
+
else if (typeof hover.contents === 'string') {
|
|
38
|
+
hoverText = hover.contents;
|
|
39
|
+
}
|
|
40
|
+
else if (Array.isArray(hover.contents)) {
|
|
41
|
+
hoverText = hover.contents.map(c => typeof c === 'string' ? c : c.value).join('\n');
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
hoverText = hover.contents.value;
|
|
45
|
+
}
|
|
46
|
+
return { content: [{ type: "text", text: hoverText }] };
|
|
47
|
+
});
|
|
48
|
+
// TOOL 2: C# Definition (Go to definition - Where was it defined?)
|
|
49
|
+
mcpServer.tool("csharp_definition", "Returns the source location of where the specified method, class, or variable is defined", {
|
|
50
|
+
filePath: z.string().describe("Absolute path to the C# file"),
|
|
51
|
+
line: z.number().describe("Line number (1-based)"),
|
|
52
|
+
column: z.number().describe("Column number (1-based)")
|
|
53
|
+
}, async ({ filePath, line, column }) => {
|
|
54
|
+
const definition = await lspClient.definition(filePath, Math.max(0, line - 1), Math.max(0, column - 1));
|
|
55
|
+
return { content: [{ type: "text", text: JSON.stringify(definition, null, 2) }] };
|
|
56
|
+
});
|
|
57
|
+
// TOOL 3: C# References (Where is it used?)
|
|
58
|
+
mcpServer.tool("csharp_references", "Finds all usages of the specified variable, class, or method across the entire project", {
|
|
59
|
+
filePath: z.string().describe("Absolute path to the C# file"),
|
|
60
|
+
line: z.number().describe("Line number (1-based)"),
|
|
61
|
+
column: z.number().describe("Column number (1-based)")
|
|
62
|
+
}, async ({ filePath, line, column }) => {
|
|
63
|
+
const references = await lspClient.references(filePath, Math.max(0, line - 1), Math.max(0, column - 1));
|
|
64
|
+
return { content: [{ type: "text", text: JSON.stringify(references, null, 2) }] };
|
|
65
|
+
});
|
|
66
|
+
// TOOL 4: C# Diagnostics (Build/Syntax errors)
|
|
67
|
+
mcpServer.tool("csharp_diagnostics", "Retrieves the current syntax errors, build issues, and warnings for the requested file", {
|
|
68
|
+
filePath: z.string().describe("Absolute path to the C# file")
|
|
69
|
+
}, async ({ filePath }) => {
|
|
70
|
+
const diagnostics = lspClient.getDiagnostics(filePath);
|
|
71
|
+
return { content: [{ type: "text", text: JSON.stringify(diagnostics, null, 2) }] };
|
|
72
|
+
});
|
|
73
|
+
// TOOL 5: Workspace Symbol (Project-wide Search)
|
|
74
|
+
mcpServer.tool("csharp_workspace_symbol", "Searches for methods, classes, or interfaces by name or partial name across the entire C# project", {
|
|
75
|
+
query: z.string().describe("The search query (e.g. method or class name)")
|
|
76
|
+
}, async ({ query }) => {
|
|
77
|
+
const symbols = await lspClient.workspaceSymbol(query);
|
|
78
|
+
return { content: [{ type: "text", text: JSON.stringify(symbols, null, 2) }] };
|
|
79
|
+
});
|
|
80
|
+
async function main() {
|
|
81
|
+
try {
|
|
82
|
+
console.error("Sending LSP Initialization request...");
|
|
83
|
+
await lspClient.initialize();
|
|
84
|
+
console.error("LSP Server successfully initialized and handshaked.");
|
|
85
|
+
const transport = new StdioServerTransport();
|
|
86
|
+
await mcpServer.connect(transport);
|
|
87
|
+
console.error("MCP Server is now listening over Stdio. Ready for communication.");
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
console.error("ERROR:", e);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
main();
|
package/dist/test.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LspClient } from "./LspClient.js";
|
|
2
|
+
async function run() {
|
|
3
|
+
console.log("Starting LspClient on TestCsharpApp...");
|
|
4
|
+
const lsp = new LspClient("C:\\Users\\PC2\\source\\repos\\RoslynMcpServer\\TestCsharpApp");
|
|
5
|
+
try {
|
|
6
|
+
await lsp.initialize();
|
|
7
|
+
console.log("LSP Initialized successfully.");
|
|
8
|
+
console.log("Waiting 5 seconds for background Roslyn analysis to push diagnostics...");
|
|
9
|
+
setTimeout(() => {
|
|
10
|
+
const diags = lsp.getDiagnostics("C:\\Users\\PC2\\source\\repos\\RoslynMcpServer\\TestCsharpApp\\Program.cs");
|
|
11
|
+
console.log("\n==================================");
|
|
12
|
+
console.log("Diagnostics Found:");
|
|
13
|
+
console.log(JSON.stringify(diags, null, 2));
|
|
14
|
+
console.log("==================================\n");
|
|
15
|
+
console.log("Shutting down...");
|
|
16
|
+
lsp.shutdown();
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}, 5000);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
console.error("Test Error:", e);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
run();
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "roslyn-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"README.md"
|
|
9
|
+
],
|
|
10
|
+
"bin": {
|
|
11
|
+
"roslyn-mcp-server": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"model context protocol",
|
|
21
|
+
"csharp",
|
|
22
|
+
"roslyn",
|
|
23
|
+
"lsp",
|
|
24
|
+
"language server",
|
|
25
|
+
"ai",
|
|
26
|
+
"claude"
|
|
27
|
+
],
|
|
28
|
+
"author": "Antigravity",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"description": "An unconditionally robust MCP server for Microsoft Roslyn Language Server (C#)",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
|
+
"vscode-jsonrpc": "^8.2.1",
|
|
34
|
+
"vscode-languageserver-protocol": "^3.17.5",
|
|
35
|
+
"zod": "^4.3.6"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^25.5.2",
|
|
39
|
+
"typescript": "^6.0.2"
|
|
40
|
+
}
|
|
41
|
+
}
|