Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/CodingWithCalvin.MCPServer.Server/RpcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public Task<List<ToolInfo>> GetAvailableToolsAsync()
}

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

foreach (var toolType in toolTypes)
{
Expand Down Expand Up @@ -124,4 +124,12 @@ public Task<List<FindResult>> FindAsync(string searchText, bool matchCase, bool
public Task<bool> CleanSolutionAsync() => Proxy.CleanSolutionAsync();
public Task<bool> CancelBuildAsync() => Proxy.CancelBuildAsync();
public Task<BuildStatus> GetBuildStatusAsync() => Proxy.GetBuildStatusAsync();

public Task<List<SymbolInfo>> GetDocumentSymbolsAsync(string path) => Proxy.GetDocumentSymbolsAsync(path);
public Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100)
=> Proxy.SearchWorkspaceSymbolsAsync(query, maxResults);
public Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int column)
=> Proxy.GoToDefinitionAsync(path, line, column);
public Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100)
=> Proxy.FindReferencesAsync(path, line, column, maxResults);
}
81 changes: 81 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/Tools/NavigationTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System.ComponentModel;
using System.Text.Json;
using System.Threading.Tasks;
using ModelContextProtocol.Server;

namespace CodingWithCalvin.MCPServer.Server.Tools;

[McpServerToolType]
public class NavigationTools
{
private readonly RpcClient _rpcClient;
private readonly JsonSerializerOptions _jsonOptions;

public NavigationTools(RpcClient rpcClient)
{
_rpcClient = rpcClient;
_jsonOptions = new JsonSerializerOptions { WriteIndented = true };
}

[McpServerTool(Name = "symbol_document", ReadOnly = true)]
[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.")]
public async Task<string> GetDocumentSymbolsAsync(
[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)
{
var symbols = await _rpcClient.GetDocumentSymbolsAsync(path);
if (symbols.Count == 0)
{
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).";
}

return JsonSerializer.Serialize(symbols, _jsonOptions);
}

[McpServerTool(Name = "symbol_workspace", ReadOnly = true)]
[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.")]
public async Task<string> SearchWorkspaceSymbolsAsync(
[Description("The search query to match against symbol names. Case-insensitive. Partial matches are supported.")] string query,
[Description("Maximum number of results to return. Defaults to 100. Use lower values for faster results on large solutions.")] int maxResults = 100)
{
var result = await _rpcClient.SearchWorkspaceSymbolsAsync(query, maxResults);
if (result.Symbols.Count == 0)
{
return $"No symbols matching '{query}' found in the solution.";
}

return JsonSerializer.Serialize(result, _jsonOptions);
}

[McpServerTool(Name = "goto_definition", ReadOnly = true)]
[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.")]
public async Task<string> GoToDefinitionAsync(
[Description("The full absolute path to the source file containing the symbol reference. Supports forward slashes (/) or backslashes (\\).")] string path,
[Description("The line number (1-based) where the symbol reference is located.")] int line,
[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)
{
var result = await _rpcClient.GoToDefinitionAsync(path, line, column);
if (!result.Found)
{
return "Definition not found. The cursor may not be on a navigable symbol, or the definition may be in external/compiled code.";
}

return JsonSerializer.Serialize(result, _jsonOptions);
}

[McpServerTool(Name = "find_references", ReadOnly = true)]
[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.")]
public async Task<string> FindReferencesAsync(
[Description("The full absolute path to the source file containing the symbol. Supports forward slashes (/) or backslashes (\\).")] string path,
[Description("The line number (1-based) where the symbol is located.")] int line,
[Description("The column number (1-based) where the symbol is located. Position within or at the start of the symbol name.")] int column,
[Description("Maximum number of references to return. Defaults to 100. Use lower values for faster results.")] int maxResults = 100)
{
var result = await _rpcClient.FindReferencesAsync(path, line, column, maxResults);
if (!result.Found)
{
return "No references found. The cursor may not be on a valid identifier.";
}

return JsonSerializer.Serialize(result, _jsonOptions);
}
}
70 changes: 70 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/Models/SymbolModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Collections.Generic;

namespace CodingWithCalvin.MCPServer.Shared.Models;

public enum SymbolKind
{
Unknown,
Namespace,
Class,
Struct,
Interface,
Enum,
Delegate,
Function,
Property,
Field,
Event,
Variable,
Parameter,
EnumMember,
Constant
}

public class SymbolInfo
{
public string Name { get; set; } = string.Empty;
public string FullName { get; set; } = string.Empty;
public SymbolKind Kind { get; set; }
public string FilePath { get; set; } = string.Empty;
public int StartLine { get; set; }
public int StartColumn { get; set; }
public int EndLine { get; set; }
public int EndColumn { get; set; }
public string ContainerName { get; set; } = string.Empty;
public List<SymbolInfo> Children { get; set; } = new();
}

public class LocationInfo
{
public string FilePath { get; set; } = string.Empty;
public int Line { get; set; }
public int Column { get; set; }
public int EndLine { get; set; }
public int EndColumn { get; set; }
public string Preview { get; set; } = string.Empty;
}

public class WorkspaceSymbolResult
{
public List<SymbolInfo> Symbols { get; set; } = new();
public int TotalCount { get; set; }
public bool Truncated { get; set; }
}

public class DefinitionResult
{
public bool Found { get; set; }
public List<LocationInfo> Definitions { get; set; } = new();
public string SymbolName { get; set; } = string.Empty;
public SymbolKind SymbolKind { get; set; }
}

public class ReferencesResult
{
public bool Found { get; set; }
public List<LocationInfo> References { get; set; } = new();
public string SymbolName { get; set; } = string.Empty;
public int TotalCount { get; set; }
public bool Truncated { get; set; }
}
5 changes: 5 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public interface IVisualStudioRpc
Task<bool> CleanSolutionAsync();
Task<bool> CancelBuildAsync();
Task<BuildStatus> GetBuildStatusAsync();

Task<List<SymbolInfo>> GetDocumentSymbolsAsync(string path);
Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100);
Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int column);
Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ public interface IVisualStudioService
Task<bool> CleanSolutionAsync();
Task<bool> CancelBuildAsync();
Task<BuildStatus> GetBuildStatusAsync();

Task<List<SymbolInfo>> GetDocumentSymbolsAsync(string path);
Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100);
Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int column);
Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100);
}
8 changes: 8 additions & 0 deletions src/CodingWithCalvin.MCPServer/Services/RpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,12 @@ public Task<List<FindResult>> FindAsync(string searchText, bool matchCase, bool
public Task<bool> CleanSolutionAsync() => _vsService.CleanSolutionAsync();
public Task<bool> CancelBuildAsync() => _vsService.CancelBuildAsync();
public Task<BuildStatus> GetBuildStatusAsync() => _vsService.GetBuildStatusAsync();

public Task<List<SymbolInfo>> GetDocumentSymbolsAsync(string path) => _vsService.GetDocumentSymbolsAsync(path);
public Task<WorkspaceSymbolResult> SearchWorkspaceSymbolsAsync(string query, int maxResults = 100)
=> _vsService.SearchWorkspaceSymbolsAsync(query, maxResults);
public Task<DefinitionResult> GoToDefinitionAsync(string path, int line, int column)
=> _vsService.GoToDefinitionAsync(path, line, column);
public Task<ReferencesResult> FindReferencesAsync(string path, int line, int column, int maxResults = 100)
=> _vsService.FindReferencesAsync(path, line, column, maxResults);
}
Loading