Skip to content

Commit 18adb0f

Browse files
authored
feat(tools): add LSP-style navigation tools (#17)
* feat(tools): add LSP-style navigation tools Add four navigation tools that mirror LSP protocol capabilities: - symbol_document: Get all symbols in a file using FileCodeModel - symbol_workspace: Search symbols across the entire solution - goto_definition: Navigate to symbol definition using Edit.GoToDefinition - find_references: Find all usages of a symbol via text search Includes new DTOs in SymbolModels.cs and implements the three-layer RPC pattern (NavigationTools -> RpcClient -> RpcServer -> VisualStudioService). * fix(tools): register NavigationTools with MCP server
1 parent 2995ddc commit 18adb0f

8 files changed

Lines changed: 674 additions & 2 deletions

File tree

src/CodingWithCalvin.MCPServer.Server/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ static async Task RunServerAsync(string pipeName, string host, int port, string
9696
.WithHttpTransport()
9797
.WithTools<SolutionTools>()
9898
.WithTools<DocumentTools>()
99-
.WithTools<BuildTools>();
99+
.WithTools<BuildTools>()
100+
.WithTools<NavigationTools>();
100101

101102
var app = builder.Build();
102103

src/CodingWithCalvin.MCPServer.Server/RpcClient.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public Task<List<ToolInfo>> GetAvailableToolsAsync()
6565
}
6666

6767
var tools = new List<ToolInfo>();
68-
var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools) };
68+
var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools), typeof(Tools.NavigationTools) };
6969

7070
foreach (var toolType in toolTypes)
7171
{
@@ -124,4 +124,12 @@ public Task<List<FindResult>> FindAsync(string searchText, bool matchCase, bool
124124
public Task<bool> CleanSolutionAsync() => Proxy.CleanSolutionAsync();
125125
public Task<bool> CancelBuildAsync() => Proxy.CancelBuildAsync();
126126
public Task<BuildStatus> GetBuildStatusAsync() => Proxy.GetBuildStatusAsync();
127+
128+
public Task<List<SymbolInfo>> GetDocumentSymbolsAsync(string path) => Proxy.GetDocumentSymbolsAsync(path);
129+
public Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100)
130+
=> Proxy.SearchWorkspaceSymbolsAsync(query, maxResults);
131+
public Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int column)
132+
=> Proxy.GoToDefinitionAsync(path, line, column);
133+
public Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100)
134+
=> Proxy.FindReferencesAsync(path, line, column, maxResults);
127135
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System.ComponentModel;
2+
using System.Text.Json;
3+
using System.Threading.Tasks;
4+
using ModelContextProtocol.Server;
5+
6+
namespace CodingWithCalvin.MCPServer.Server.Tools;
7+
8+
[McpServerToolType]
9+
public class NavigationTools
10+
{
11+
private readonly RpcClient _rpcClient;
12+
private readonly JsonSerializerOptions _jsonOptions;
13+
14+
public NavigationTools(RpcClient rpcClient)
15+
{
16+
_rpcClient = rpcClient;
17+
_jsonOptions = new JsonSerializerOptions { WriteIndented = true };
18+
}
19+
20+
[McpServerTool(Name = "symbol_document", ReadOnly = true)]
21+
[Description("Get all symbols (classes, methods, properties, etc.) defined in a file. Returns a hierarchical list of symbols with their names, kinds, and locations. The file must be part of a project in the open solution.")]
22+
public async Task<string> GetDocumentSymbolsAsync(
23+
[Description("The full absolute path to the source file. Must be a file in a project within the open solution. Supports forward slashes (/) or backslashes (\\).")] string path)
24+
{
25+
var symbols = await _rpcClient.GetDocumentSymbolsAsync(path);
26+
if (symbols.Count == 0)
27+
{
28+
return "No symbols found. The file may not be part of the solution or may not have a code model (only works with C#/VB files in projects).";
29+
}
30+
31+
return JsonSerializer.Serialize(symbols, _jsonOptions);
32+
}
33+
34+
[McpServerTool(Name = "symbol_workspace", ReadOnly = true)]
35+
[Description("Search for symbols (classes, methods, properties, etc.) across the entire solution. Returns symbols matching the query with their locations. Useful for finding types or members by name.")]
36+
public async Task<string> SearchWorkspaceSymbolsAsync(
37+
[Description("The search query to match against symbol names. Case-insensitive. Partial matches are supported.")] string query,
38+
[Description("Maximum number of results to return. Defaults to 100. Use lower values for faster results on large solutions.")] int maxResults = 100)
39+
{
40+
var result = await _rpcClient.SearchWorkspaceSymbolsAsync(query, maxResults);
41+
if (result.Symbols.Count == 0)
42+
{
43+
return $"No symbols matching '{query}' found in the solution.";
44+
}
45+
46+
return JsonSerializer.Serialize(result, _jsonOptions);
47+
}
48+
49+
[McpServerTool(Name = "goto_definition", ReadOnly = true)]
50+
[Description("Navigate to the definition of a symbol at a specific position in a file. Opens the file containing the definition and returns its location. Uses Visual Studio's 'Go To Definition' feature.")]
51+
public async Task<string> GoToDefinitionAsync(
52+
[Description("The full absolute path to the source file containing the symbol reference. Supports forward slashes (/) or backslashes (\\).")] string path,
53+
[Description("The line number (1-based) where the symbol reference is located.")] int line,
54+
[Description("The column number (1-based) where the symbol reference is located. Position the cursor within or at the start of the symbol name.")] int column)
55+
{
56+
var result = await _rpcClient.GoToDefinitionAsync(path, line, column);
57+
if (!result.Found)
58+
{
59+
return "Definition not found. The cursor may not be on a navigable symbol, or the definition may be in external/compiled code.";
60+
}
61+
62+
return JsonSerializer.Serialize(result, _jsonOptions);
63+
}
64+
65+
[McpServerTool(Name = "find_references", ReadOnly = true)]
66+
[Description("Find all references to a symbol at a specific position in a file. Returns a list of locations where the symbol is used throughout the solution. Uses text-based search with word boundary matching.")]
67+
public async Task<string> FindReferencesAsync(
68+
[Description("The full absolute path to the source file containing the symbol. Supports forward slashes (/) or backslashes (\\).")] string path,
69+
[Description("The line number (1-based) where the symbol is located.")] int line,
70+
[Description("The column number (1-based) where the symbol is located. Position within or at the start of the symbol name.")] int column,
71+
[Description("Maximum number of references to return. Defaults to 100. Use lower values for faster results.")] int maxResults = 100)
72+
{
73+
var result = await _rpcClient.FindReferencesAsync(path, line, column, maxResults);
74+
if (!result.Found)
75+
{
76+
return "No references found. The cursor may not be on a valid identifier.";
77+
}
78+
79+
return JsonSerializer.Serialize(result, _jsonOptions);
80+
}
81+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System.Collections.Generic;
2+
3+
namespace CodingWithCalvin.MCPServer.Shared.Models;
4+
5+
public enum SymbolKind
6+
{
7+
Unknown,
8+
Namespace,
9+
Class,
10+
Struct,
11+
Interface,
12+
Enum,
13+
Delegate,
14+
Function,
15+
Property,
16+
Field,
17+
Event,
18+
Variable,
19+
Parameter,
20+
EnumMember,
21+
Constant
22+
}
23+
24+
public class SymbolInfo
25+
{
26+
public string Name { get; set; } = string.Empty;
27+
public string FullName { get; set; } = string.Empty;
28+
public SymbolKind Kind { get; set; }
29+
public string FilePath { get; set; } = string.Empty;
30+
public int StartLine { get; set; }
31+
public int StartColumn { get; set; }
32+
public int EndLine { get; set; }
33+
public int EndColumn { get; set; }
34+
public string ContainerName { get; set; } = string.Empty;
35+
public List<SymbolInfo> Children { get; set; } = new();
36+
}
37+
38+
public class LocationInfo
39+
{
40+
public string FilePath { get; set; } = string.Empty;
41+
public int Line { get; set; }
42+
public int Column { get; set; }
43+
public int EndLine { get; set; }
44+
public int EndColumn { get; set; }
45+
public string Preview { get; set; } = string.Empty;
46+
}
47+
48+
public class WorkspaceSymbolResult
49+
{
50+
public List<SymbolInfo> Symbols { get; set; } = new();
51+
public int TotalCount { get; set; }
52+
public bool Truncated { get; set; }
53+
}
54+
55+
public class DefinitionResult
56+
{
57+
public bool Found { get; set; }
58+
public List<LocationInfo> Definitions { get; set; } = new();
59+
public string SymbolName { get; set; } = string.Empty;
60+
public SymbolKind SymbolKind { get; set; }
61+
}
62+
63+
public class ReferencesResult
64+
{
65+
public bool Found { get; set; }
66+
public List<LocationInfo> References { get; set; } = new();
67+
public string SymbolName { get; set; } = string.Empty;
68+
public int TotalCount { get; set; }
69+
public bool Truncated { get; set; }
70+
}

src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public interface IVisualStudioRpc
3434
Task<bool> CleanSolutionAsync();
3535
Task<bool> CancelBuildAsync();
3636
Task<BuildStatus> GetBuildStatusAsync();
37+
38+
Task<List<SymbolInfo>> GetDocumentSymbolsAsync(string path);
39+
Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100);
40+
Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int column);
41+
Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100);
3742
}
3843

3944
/// <summary>

src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ public interface IVisualStudioService
3030
Task<bool> CleanSolutionAsync();
3131
Task<bool> CancelBuildAsync();
3232
Task<BuildStatus> GetBuildStatusAsync();
33+
34+
Task<List<SymbolInfo>> GetDocumentSymbolsAsync(string path);
35+
Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100);
36+
Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int column);
37+
Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100);
3338
}

src/CodingWithCalvin.MCPServer/Services/RpcServer.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,12 @@ public Task<List<FindResult>> FindAsync(string searchText, bool matchCase, bool
184184
public Task<bool> CleanSolutionAsync() => _vsService.CleanSolutionAsync();
185185
public Task<bool> CancelBuildAsync() => _vsService.CancelBuildAsync();
186186
public Task<BuildStatus> GetBuildStatusAsync() => _vsService.GetBuildStatusAsync();
187+
188+
public Task<List<SymbolInfo>> GetDocumentSymbolsAsync(string path) => _vsService.GetDocumentSymbolsAsync(path);
189+
public Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100)
190+
=> _vsService.SearchWorkspaceSymbolsAsync(query, maxResults);
191+
public Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int column)
192+
=> _vsService.GoToDefinitionAsync(path, line, column);
193+
public Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100)
194+
=> _vsService.FindReferencesAsync(path, line, column, maxResults);
187195
}

0 commit comments

Comments
 (0)