Skip to content

Commit 47b841e

Browse files
authored
feat(tools): add document_save MCP tool (#38)
1 parent d1887ea commit 47b841e

6 files changed

Lines changed: 37 additions & 0 deletions

File tree

src/CodingWithCalvin.MCPServer.Server/RpcClient.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ public Task ShutdownAsync()
109109
public Task<DocumentInfo?> GetActiveDocumentAsync() => Proxy.GetActiveDocumentAsync();
110110
public Task<bool> OpenDocumentAsync(string path) => Proxy.OpenDocumentAsync(path);
111111
public Task<bool> CloseDocumentAsync(string path, bool save) => Proxy.CloseDocumentAsync(path, save);
112+
public Task<bool> SaveDocumentAsync(string path) => Proxy.SaveDocumentAsync(path);
112113
public Task<string?> ReadDocumentAsync(string path) => Proxy.ReadDocumentAsync(path);
113114
public Task<bool> WriteDocumentAsync(string path, string content) => Proxy.WriteDocumentAsync(path, content);
114115
public Task<SelectionInfo?> GetSelectionAsync() => Proxy.GetSelectionAsync();

src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ public async Task<string> CloseDocumentAsync(
6363
return success ? $"Closed: {path}" : $"Document not found or failed to close: {path}";
6464
}
6565

66+
[McpServerTool(Name = "document_save", Destructive = false, Idempotent = true)]
67+
[Description("Save an open document in Visual Studio to disk.")]
68+
public async Task<string> SaveDocumentAsync(
69+
[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)
70+
{
71+
var success = await _rpcClient.SaveDocumentAsync(path);
72+
return success ? $"Saved: {path}" : $"Document not found or failed to save: {path}";
73+
}
74+
6675
[McpServerTool(Name = "document_read", ReadOnly = true)]
6776
[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.")]
6877
public async Task<string> ReadDocumentAsync(

src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public interface IVisualStudioRpc
1919
Task<DocumentInfo?> GetActiveDocumentAsync();
2020
Task<bool> OpenDocumentAsync(string path);
2121
Task<bool> CloseDocumentAsync(string path, bool save);
22+
Task<bool> SaveDocumentAsync(string path);
2223
Task<string?> ReadDocumentAsync(string path);
2324
Task<bool> WriteDocumentAsync(string path, string content);
2425
Task<SelectionInfo?> GetSelectionAsync();

src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public interface IVisualStudioService
1515
Task<DocumentInfo?> GetActiveDocumentAsync();
1616
Task<bool> OpenDocumentAsync(string path);
1717
Task<bool> CloseDocumentAsync(string path, bool save = true);
18+
Task<bool> SaveDocumentAsync(string path);
1819
Task<string?> ReadDocumentAsync(string path);
1920
Task<bool> WriteDocumentAsync(string path, string content);
2021
Task<SelectionInfo?> GetSelectionAsync();

src/CodingWithCalvin.MCPServer/Services/RpcServer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ public async Task RequestShutdownAsync()
169169
public Task<DocumentInfo?> GetActiveDocumentAsync() => _vsService.GetActiveDocumentAsync();
170170
public Task<bool> OpenDocumentAsync(string path) => _vsService.OpenDocumentAsync(path);
171171
public Task<bool> CloseDocumentAsync(string path, bool save) => _vsService.CloseDocumentAsync(path, save);
172+
public Task<bool> SaveDocumentAsync(string path) => _vsService.SaveDocumentAsync(path);
172173
public Task<string?> ReadDocumentAsync(string path) => _vsService.ReadDocumentAsync(path);
173174
public Task<bool> WriteDocumentAsync(string path, string content) => _vsService.WriteDocumentAsync(path, content);
174175
public Task<SelectionInfo?> GetSelectionAsync() => _vsService.GetSelectionAsync();

src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,30 @@ public async Task<bool> CloseDocumentAsync(string path, bool save = true)
205205
return false;
206206
}
207207

208+
public async Task<bool> SaveDocumentAsync(string path)
209+
{
210+
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
211+
var dte = await GetDteAsync();
212+
213+
foreach (Document doc in dte.Documents)
214+
{
215+
try
216+
{
217+
if (PathsEqual(doc.FullName, path))
218+
{
219+
doc.Save();
220+
return true;
221+
}
222+
}
223+
catch (Exception ex)
224+
{
225+
VsixTelemetry.TrackException(ex);
226+
}
227+
}
228+
229+
return false;
230+
}
231+
208232
public async Task<string?> ReadDocumentAsync(string path)
209233
{
210234
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

0 commit comments

Comments
 (0)