From f5a7314bdf5072098c3a1d05800d1866d98042e6 Mon Sep 17 00:00:00 2001 From: Vidoy Date: Sun, 2 Aug 2026 03:09:27 +0200 Subject: [PATCH] Added File and Folder Creation. Added the ability to create files and folders for the AI to assure better workflows. --- .../RpcClient.cs | 2 + .../Tools/DocumentTools.cs | 26 +++++++ .../Models/DocumentModels.cs | 8 ++ .../RpcContracts.cs | 3 + .../Services/IVisualStudioService.cs | 3 + .../Services/RpcServer.cs | 3 + .../Services/VisualStudioService.cs | 78 +++++++++++++++++++ 7 files changed, 123 insertions(+) diff --git a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs index d4465c2..c4219a5 100644 --- a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs +++ b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs @@ -110,6 +110,8 @@ public Task ShutdownAsync() public Task> GetOpenDocumentsAsync() => Proxy.GetOpenDocumentsAsync(); public Task GetActiveDocumentAsync() => Proxy.GetActiveDocumentAsync(); public Task OpenDocumentAsync(string path) => Proxy.OpenDocumentAsync(path); + public Task CreateFileAsync(string path, string? content = null) => Proxy.CreateFileAsync(path, content); + public Task CreateFolderAsync(string path) => Proxy.CreateFolderAsync(path); public Task CloseDocumentAsync(string path, bool save) => Proxy.CloseDocumentAsync(path, save); public Task SaveDocumentAsync(string path) => Proxy.SaveDocumentAsync(path); public Task RunCodeCleanupAsync(string path) => Proxy.RunCodeCleanupAsync(path); diff --git a/src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs b/src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs index cea3155..cdf3dab 100644 --- a/src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs +++ b/src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs @@ -201,4 +201,30 @@ public async Task FindTextAsync( return JsonSerializer.Serialize(results, _jsonOptions); } + + [McpServerTool(Name = "file_create", Destructive = false, Idempotent = true)] + [Description("Create a new file on disk and open it in Visual Studio. If the file already exists, it will be opened without modification. Optionally provides initial content for new files.")] + public async Task CreateFileAsync( + [Description("The full absolute path where to create the file (including filename). Supports forward slashes (/) or backslashes (\\).")] string path, + [Description("Optional initial content for the new file. If not provided, creates an empty file. Only applies if file doesn't exist.")] string? content = null) + { + var result = await _rpcClient.CreateFileAsync(path, content); + if (result.Success) + { + return $"Created: {path}"; + } + else + { + return $"Failed to create file: {path}. Error: {result.ErrorMessage}"; + } + } + + [McpServerTool(Name = "folder_create", Destructive = false, Idempotent = true)] + [Description("Create a new folder (directory) on disk. The parent directory must already exist.")] + public async Task CreateFolderAsync( + [Description("The full absolute path to the folder to create. Supports forward slashes (/) or backslashes (\\).")] string path) + { + var success = await _rpcClient.CreateFolderAsync(path); + return success ? $"Created folder: {path}" : $"Failed to create folder: {path}"; + } } diff --git a/src/CodingWithCalvin.MCPServer.Shared/Models/DocumentModels.cs b/src/CodingWithCalvin.MCPServer.Shared/Models/DocumentModels.cs index 4b62721..671cb89 100644 --- a/src/CodingWithCalvin.MCPServer.Shared/Models/DocumentModels.cs +++ b/src/CodingWithCalvin.MCPServer.Shared/Models/DocumentModels.cs @@ -24,3 +24,11 @@ public class FindResult public string Text { get; set; } = string.Empty; public string DocumentPath { get; set; } = string.Empty; } + +public class FileCreateResult +{ + public bool Success { get; set; } + public string Path { get; set; } = string.Empty; + public bool CreatedNew { get; set; } + public string ErrorMessage { get; set; } = string.Empty; +} diff --git a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs index fb63bd8..5da9c82 100644 --- a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs +++ b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs @@ -18,6 +18,8 @@ public interface IVisualStudioRpc Task> GetOpenDocumentsAsync(); Task GetActiveDocumentAsync(); Task OpenDocumentAsync(string path); + Task CreateFileAsync(string path, string? content = null); + Task CreateFolderAsync(string path); Task CloseDocumentAsync(string path, bool save); Task SaveDocumentAsync(string path); Task RunCodeCleanupAsync(string path); @@ -85,3 +87,4 @@ public interface IServerRpc Task> GetAvailableToolsAsync(); Task ShutdownAsync(); } + diff --git a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs index 75926ce..5e42f67 100644 --- a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs @@ -14,6 +14,8 @@ public interface IVisualStudioService Task> GetOpenDocumentsAsync(); Task GetActiveDocumentAsync(); Task OpenDocumentAsync(string path); + Task CreateFileAsync(string path, string? content = null); + Task CreateFolderAsync(string path); Task CloseDocumentAsync(string path, bool save = true); Task SaveDocumentAsync(string path); Task RunCodeCleanupAsync(string path); @@ -69,3 +71,4 @@ public interface IVisualStudioService Task ShowToolWindowAsync(string name); Task HideToolWindowAsync(string caption); } + diff --git a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs index 4ca2877..cc1d194 100644 --- a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs +++ b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs @@ -168,6 +168,8 @@ public async Task RequestShutdownAsync() public Task> GetOpenDocumentsAsync() => _vsService.GetOpenDocumentsAsync(); public Task GetActiveDocumentAsync() => _vsService.GetActiveDocumentAsync(); public Task OpenDocumentAsync(string path) => _vsService.OpenDocumentAsync(path); + public Task CreateFileAsync(string path, string? content = null) => _vsService.CreateFileAsync(path, content); + public Task CreateFolderAsync(string path) => _vsService.CreateFolderAsync(path); public Task CloseDocumentAsync(string path, bool save) => _vsService.CloseDocumentAsync(path, save); public Task SaveDocumentAsync(string path) => _vsService.SaveDocumentAsync(path); public Task RunCodeCleanupAsync(string path) => _vsService.RunCodeCleanupAsync(path); @@ -228,3 +230,4 @@ public Task WriteOutputPaneAsync(string paneIdentifier, string message, bo public Task ShowToolWindowAsync(string name) => _vsService.ShowToolWindowAsync(name); public Task HideToolWindowAsync(string caption) => _vsService.HideToolWindowAsync(caption); } + diff --git a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs index 0a642f2..60d0f06 100644 --- a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs @@ -259,6 +259,84 @@ public async Task> GetOpenDocumentsAsync() }; } + public async Task CreateFileAsync(string path, string? content = null) + { + using var activity = VsixTelemetry.Tracer.StartActivity("CreateFile"); + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + try + { + // Normalize the path + var normalizedPath = NormalizePath(path); + + // Check if file already exists + bool fileExists = File.Exists(normalizedPath); + + if (!fileExists) + { + // Create parent directories if they don't exist + var directory = Path.GetDirectoryName(normalizedPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + // Write initial content (or empty file) - using sync version for .NET Framework 4.8 compatibility + File.WriteAllText(normalizedPath, content ?? string.Empty); + } + + // Open the file in Visual Studio + var dte = await GetDteAsync(); + dte.ItemOperations.OpenFile(normalizedPath); + + return new FileCreateResult + { + Success = true, + Path = normalizedPath, + CreatedNew = !fileExists + }; + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.RecordException(ex); + + return new FileCreateResult + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + + public async Task CreateFolderAsync(string path) + { + using var activity = VsixTelemetry.Tracer.StartActivity("CreateFolder"); + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + try + { + // Normalize the path + var normalizedPath = NormalizePath(path); + + if (Directory.Exists(normalizedPath)) + { + return true; + } + + Directory.CreateDirectory(normalizedPath); + return true; + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.RecordException(ex); + return false; + } + } + public async Task OpenDocumentAsync(string path) { using var activity = VsixTelemetry.Tracer.StartActivity("OpenDocument");