From f40c84516cfa05dd6635fd3d9fb81c04378f4a3f Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Mon, 16 Mar 2026 13:42:07 -0400 Subject: [PATCH] feat(tools): add document_save MCP tool --- .../RpcClient.cs | 1 + .../Tools/DocumentTools.cs | 9 +++++++ .../RpcContracts.cs | 1 + .../Services/IVisualStudioService.cs | 1 + .../Services/RpcServer.cs | 1 + .../Services/VisualStudioService.cs | 24 +++++++++++++++++++ 6 files changed, 37 insertions(+) diff --git a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs index 37cfcd9..12ab718 100644 --- a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs +++ b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs @@ -109,6 +109,7 @@ public Task ShutdownAsync() public Task GetActiveDocumentAsync() => Proxy.GetActiveDocumentAsync(); public Task OpenDocumentAsync(string path) => Proxy.OpenDocumentAsync(path); public Task CloseDocumentAsync(string path, bool save) => Proxy.CloseDocumentAsync(path, save); + public Task SaveDocumentAsync(string path) => Proxy.SaveDocumentAsync(path); public Task ReadDocumentAsync(string path) => Proxy.ReadDocumentAsync(path); public Task WriteDocumentAsync(string path, string content) => Proxy.WriteDocumentAsync(path, content); public Task GetSelectionAsync() => Proxy.GetSelectionAsync(); diff --git a/src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs b/src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs index 487c6af..63f37ba 100644 --- a/src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs +++ b/src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs @@ -63,6 +63,15 @@ public async Task CloseDocumentAsync( return success ? $"Closed: {path}" : $"Document not found or failed to close: {path}"; } + [McpServerTool(Name = "document_save", Destructive = false, Idempotent = true)] + [Description("Save an open document in Visual Studio to disk.")] + public async Task SaveDocumentAsync( + [Description("The full absolute path to the document. Must be open in VS. Get the path from document_list. Supports forward slashes (/) or backslashes (\\).")] string path) + { + var success = await _rpcClient.SaveDocumentAsync(path); + return success ? $"Saved: {path}" : $"Document not found or failed to save: {path}"; + } + [McpServerTool(Name = "document_read", ReadOnly = true)] [Description("Read the contents of a document. If the document is open in VS, reads the current editor buffer (including unsaved changes); otherwise reads from disk.")] public async Task ReadDocumentAsync( diff --git a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs index 53843fc..e7f7eb4 100644 --- a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs +++ b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs @@ -19,6 +19,7 @@ public interface IVisualStudioRpc Task GetActiveDocumentAsync(); Task OpenDocumentAsync(string path); Task CloseDocumentAsync(string path, bool save); + Task SaveDocumentAsync(string path); Task ReadDocumentAsync(string path); Task WriteDocumentAsync(string path, string content); Task GetSelectionAsync(); diff --git a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs index 0c355cd..29a9f5d 100644 --- a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs @@ -15,6 +15,7 @@ public interface IVisualStudioService Task GetActiveDocumentAsync(); Task OpenDocumentAsync(string path); Task CloseDocumentAsync(string path, bool save = true); + Task SaveDocumentAsync(string path); Task ReadDocumentAsync(string path); Task WriteDocumentAsync(string path, string content); Task GetSelectionAsync(); diff --git a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs index 0561110..c9bf80a 100644 --- a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs +++ b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs @@ -169,6 +169,7 @@ public async Task RequestShutdownAsync() public Task GetActiveDocumentAsync() => _vsService.GetActiveDocumentAsync(); public Task OpenDocumentAsync(string path) => _vsService.OpenDocumentAsync(path); public Task CloseDocumentAsync(string path, bool save) => _vsService.CloseDocumentAsync(path, save); + public Task SaveDocumentAsync(string path) => _vsService.SaveDocumentAsync(path); public Task ReadDocumentAsync(string path) => _vsService.ReadDocumentAsync(path); public Task WriteDocumentAsync(string path, string content) => _vsService.WriteDocumentAsync(path, content); public Task GetSelectionAsync() => _vsService.GetSelectionAsync(); diff --git a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs index 188e2cf..f966b54 100644 --- a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs @@ -205,6 +205,30 @@ public async Task CloseDocumentAsync(string path, bool save = true) return false; } + public async Task SaveDocumentAsync(string path) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + + foreach (Document doc in dte.Documents) + { + try + { + if (PathsEqual(doc.FullName, path)) + { + doc.Save(); + return true; + } + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + } + } + + return false; + } + public async Task ReadDocumentAsync(string path) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();