Skip to content

Commit 932e3ae

Browse files
committed
feat: ensuring tool success
1 parent 14bae03 commit 932e3ae

29 files changed

Lines changed: 1022 additions & 290 deletions

assets/logo.png

945 KB
Loading

resources/publish.manifest.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "http://json.schemastore.org/vsix-publish",
3+
"categories": [
4+
"coding",
5+
"other"
6+
],
7+
"identity": {
8+
"internalName": "VS-VSMCP"
9+
},
10+
"overview": "../README.md",
11+
"publisher": "CodingWithCalvin",
12+
"qna": true,
13+
"repo": "https://www.github.com/CodingWithCalvin/VS-VSMCP"
14+
}

src/CodingWithCalvin.VSMCP.Server/CodingWithCalvin.VSMCP.Server.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
<ImplicitUsings>disable</ImplicitUsings>
99
<RootNamespace>CodingWithCalvin.VSMCP.Server</RootNamespace>
1010
<AssemblyName>CodingWithCalvin.VSMCP.Server</AssemblyName>
11+
<PublishSingleFile>true</PublishSingleFile>
12+
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
13+
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
1114
</PropertyGroup>
1215

1316
<ItemGroup>

src/CodingWithCalvin.VSMCP.Server/Program.cs

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using System;
22
using System.CommandLine;
3+
using System.Threading;
34
using System.Threading.Tasks;
45
using CodingWithCalvin.VSMCP.Server;
56
using CodingWithCalvin.VSMCP.Server.Tools;
67
using Microsoft.AspNetCore.Builder;
78
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.Hosting;
10+
using Microsoft.Extensions.Logging;
811
using ModelContextProtocol.AspNetCore;
912
using ModelContextProtocol.Protocol;
1013
using ModelContextProtocol.Server;
@@ -16,6 +19,11 @@
1619
IsRequired = true
1720
};
1821

22+
var hostOption = new Option<string>(
23+
name: "--host",
24+
getDefaultValue: () => "localhost",
25+
description: "Host address to bind the HTTP server to (e.g., localhost, 0.0.0.0, *)");
26+
1927
var portOption = new Option<int>(
2028
name: "--port",
2129
getDefaultValue: () => 5050,
@@ -26,31 +34,55 @@
2634
getDefaultValue: () => "Visual Studio MCP",
2735
description: "Server name displayed to MCP clients");
2836

37+
var logLevelOption = new Option<string>(
38+
name: "--log-level",
39+
getDefaultValue: () => "Information",
40+
description: "Minimum log level (Error, Warning, Information, Debug)");
41+
2942
var rootCommand = new RootCommand("Visual Studio MCP Server")
3043
{
3144
pipeOption,
45+
hostOption,
3246
portOption,
33-
nameOption
47+
nameOption,
48+
logLevelOption
3449
};
3550

36-
rootCommand.SetHandler(async (string pipeName, int port, string serverName) =>
51+
rootCommand.SetHandler(async (string pipeName, string host, int port, string serverName, string logLevel) =>
3752
{
38-
await RunServerAsync(pipeName, port, serverName);
39-
}, pipeOption, portOption, nameOption);
53+
await RunServerAsync(pipeName, host, port, serverName, logLevel);
54+
}, pipeOption, hostOption, portOption, nameOption, logLevelOption);
4055

4156
return await rootCommand.InvokeAsync(args);
4257

43-
static async Task RunServerAsync(string pipeName, int port, string serverName)
58+
static async Task RunServerAsync(string pipeName, string host, int port, string serverName, string logLevel)
4459
{
60+
// Parse log level
61+
var msLogLevel = logLevel switch
62+
{
63+
"Error" => LogLevel.Error,
64+
"Warning" => LogLevel.Warning,
65+
"Debug" => LogLevel.Debug,
66+
_ => LogLevel.Information
67+
};
68+
69+
// Create shutdown token for graceful shutdown
70+
using var shutdownCts = new CancellationTokenSource();
71+
4572
// Connect to Visual Studio via named pipe
46-
var rpcClient = new RpcClient();
73+
var rpcClient = new RpcClient(shutdownCts);
4774
await rpcClient.ConnectAsync(pipeName);
4875

4976
Console.Error.WriteLine($"Connected to Visual Studio via pipe: {pipeName}");
5077

5178
// Build the web application
5279
var builder = WebApplication.CreateBuilder();
5380

81+
// Configure logging
82+
builder.Logging.SetMinimumLevel(msLogLevel);
83+
builder.Logging.AddFilter("Microsoft.AspNetCore", msLogLevel);
84+
builder.Logging.AddFilter("ModelContextProtocol", msLogLevel);
85+
5486
builder.Services.AddSingleton(rpcClient);
5587

5688
builder.Services.AddMcpServer(options =>
@@ -70,7 +102,16 @@ static async Task RunServerAsync(string pipeName, int port, string serverName)
70102

71103
app.MapMcp();
72104

73-
Console.Error.WriteLine($"MCP Server listening on http://localhost:{port}");
105+
var bindingUrl = $"http://{host}:{port}";
106+
app.Urls.Add(bindingUrl);
107+
108+
// Register shutdown token to stop the application
109+
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
110+
shutdownCts.Token.Register(() => lifetime.StopApplication());
111+
112+
Console.Error.WriteLine($"MCP Server listening on {bindingUrl} (LogLevel: {logLevel})");
113+
114+
await app.RunAsync();
74115

75-
await app.RunAsync($"http://localhost:{port}");
116+
Console.Error.WriteLine("Server shutdown complete");
76117
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"profiles": {
3+
"CodingWithCalvin.VSMCP.Server": {
4+
"commandName": "Project",
5+
"launchBrowser": true,
6+
"environmentVariables": {
7+
"ASPNETCORE_ENVIRONMENT": "Development"
8+
},
9+
"applicationUrl": "https://localhost:57573;http://localhost:57574"
10+
}
11+
}
12+
}

src/CodingWithCalvin.VSMCP.Server/RpcClient.cs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,34 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.ComponentModel;
34
using System.IO.Pipes;
5+
using System.Linq;
6+
using System.Reflection;
7+
using System.Threading;
48
using System.Threading.Tasks;
59
using CodingWithCalvin.VSMCP.Shared;
610
using CodingWithCalvin.VSMCP.Shared.Models;
11+
using ModelContextProtocol.Server;
712
using StreamJsonRpc;
813

914
namespace CodingWithCalvin.VSMCP.Server;
1015

11-
public class RpcClient : IVisualStudioRpc, IDisposable
16+
public class RpcClient : IVisualStudioRpc, IServerRpc, IDisposable
1217
{
18+
private readonly CancellationTokenSource _shutdownCts;
1319
private NamedPipeClientStream? _pipeClient;
1420
private JsonRpc? _jsonRpc;
1521
private IVisualStudioRpc? _proxy;
1622
private bool _disposed;
23+
private List<ToolInfo>? _cachedTools;
1724

1825
public bool IsConnected => _pipeClient?.IsConnected ?? false;
1926

27+
public RpcClient(CancellationTokenSource shutdownCts)
28+
{
29+
_shutdownCts = shutdownCts;
30+
}
31+
2032
public async Task ConnectAsync(string pipeName, int timeoutMs = 10000)
2133
{
2234
_pipeClient = new NamedPipeClientStream(
@@ -27,7 +39,7 @@ public async Task ConnectAsync(string pipeName, int timeoutMs = 10000)
2739

2840
await _pipeClient.ConnectAsync(timeoutMs);
2941

30-
_jsonRpc = JsonRpc.Attach(_pipeClient);
42+
_jsonRpc = JsonRpc.Attach(_pipeClient, this);
3143
_proxy = _jsonRpc.Attach<IVisualStudioRpc>();
3244
}
3345

@@ -45,6 +57,54 @@ public void Dispose()
4557

4658
private IVisualStudioRpc Proxy => _proxy ?? throw new InvalidOperationException("Not connected to Visual Studio");
4759

60+
#region IServerRpc Implementation
61+
62+
public Task<List<ToolInfo>> GetAvailableToolsAsync()
63+
{
64+
if (_cachedTools != null)
65+
{
66+
return Task.FromResult(_cachedTools);
67+
}
68+
69+
var tools = new List<ToolInfo>();
70+
var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools) };
71+
72+
foreach (var toolType in toolTypes)
73+
{
74+
var category = toolType.Name.Replace("Tools", "");
75+
76+
foreach (var method in toolType.GetMethods(BindingFlags.Public | BindingFlags.Instance))
77+
{
78+
var toolAttr = method.GetCustomAttribute<McpServerToolAttribute>();
79+
if (toolAttr == null)
80+
{
81+
continue;
82+
}
83+
84+
var descAttr = method.GetCustomAttribute<DescriptionAttribute>();
85+
86+
tools.Add(new ToolInfo
87+
{
88+
Name = toolAttr.Name ?? method.Name,
89+
Description = descAttr?.Description ?? string.Empty,
90+
Category = category
91+
});
92+
}
93+
}
94+
95+
_cachedTools = tools;
96+
return Task.FromResult(tools);
97+
}
98+
99+
public Task ShutdownAsync()
100+
{
101+
Console.Error.WriteLine("Shutdown requested via RPC");
102+
_shutdownCts.Cancel();
103+
return Task.CompletedTask;
104+
}
105+
106+
#endregion
107+
48108
#region IVisualStudioRpc Implementation
49109

50110
public Task<SolutionInfo?> GetSolutionInfoAsync() => Proxy.GetSolutionInfoAsync();
@@ -68,7 +128,7 @@ public Task<List<FindResult>> FindAsync(string searchText, bool matchCase, bool
68128
public Task<bool> BuildSolutionAsync() => Proxy.BuildSolutionAsync();
69129
public Task<bool> BuildProjectAsync(string projectName) => Proxy.BuildProjectAsync(projectName);
70130
public Task<bool> CleanSolutionAsync() => Proxy.CleanSolutionAsync();
71-
public Task CancelBuildAsync() => Proxy.CancelBuildAsync();
131+
public Task<bool> CancelBuildAsync() => Proxy.CancelBuildAsync();
72132
public Task<BuildStatus> GetBuildStatusAsync() => Proxy.GetBuildStatusAsync();
73133

74134
#endregion

src/CodingWithCalvin.VSMCP.Server/Tools/BuildTools.cs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,50 +9,52 @@ namespace CodingWithCalvin.VSMCP.Server.Tools;
99
public class BuildTools
1010
{
1111
private readonly RpcClient _rpcClient;
12+
private readonly JsonSerializerOptions _jsonOptions;
1213

1314
public BuildTools(RpcClient rpcClient)
1415
{
1516
_rpcClient = rpcClient;
17+
_jsonOptions = new JsonSerializerOptions { WriteIndented = true };
1618
}
1719

18-
[McpServerTool]
20+
[McpServerTool(Name = "build_solution")]
1921
[Description("Build the entire solution")]
20-
public async Task<string> build_solution()
22+
public async Task<string> BuildSolutionAsync()
2123
{
2224
var success = await _rpcClient.BuildSolutionAsync();
2325
return success ? "Build started" : "Failed to start build (is a solution open?)";
2426
}
2527

26-
[McpServerTool]
28+
[McpServerTool(Name = "build_project")]
2729
[Description("Build a specific project")]
28-
public async Task<string> build_project(
30+
public async Task<string> BuildProjectAsync(
2931
[Description("The name of the project to build")] string projectName)
3032
{
3133
var success = await _rpcClient.BuildProjectAsync(projectName);
3234
return success ? $"Build started for project: {projectName}" : $"Failed to build project: {projectName}";
3335
}
3436

35-
[McpServerTool]
37+
[McpServerTool(Name = "clean_solution")]
3638
[Description("Clean the entire solution (remove build outputs)")]
37-
public async Task<string> clean_solution()
39+
public async Task<string> CleanSolutionAsync()
3840
{
3941
var success = await _rpcClient.CleanSolutionAsync();
4042
return success ? "Clean started" : "Failed to start clean (is a solution open?)";
4143
}
4244

43-
[McpServerTool]
45+
[McpServerTool(Name = "build_cancel")]
4446
[Description("Cancel the current build operation")]
45-
public async Task<string> build_cancel()
47+
public async Task<string> CancelBuildAsync()
4648
{
47-
await _rpcClient.CancelBuildAsync();
48-
return "Build cancel requested";
49+
var cancelled = await _rpcClient.CancelBuildAsync();
50+
return cancelled ? "Build cancelled" : "No build is currently in progress";
4951
}
5052

51-
[McpServerTool]
53+
[McpServerTool(Name = "build_status")]
5254
[Description("Get the current build status")]
53-
public async Task<string> build_status()
55+
public async Task<string> GetBuildStatusAsync()
5456
{
5557
var status = await _rpcClient.GetBuildStatusAsync();
56-
return JsonSerializer.Serialize(status, new JsonSerializerOptions { WriteIndented = true });
58+
return JsonSerializer.Serialize(status, _jsonOptions);
5759
}
5860
}

0 commit comments

Comments
 (0)