Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/RpcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ public Task ShutdownAsync()
public Task<List<DocumentInfo>> GetOpenDocumentsAsync() => Proxy.GetOpenDocumentsAsync();
public Task<DocumentInfo?> GetActiveDocumentAsync() => Proxy.GetActiveDocumentAsync();
public Task<bool> OpenDocumentAsync(string path) => Proxy.OpenDocumentAsync(path);
public Task<FileCreateResult> CreateFileAsync(string path, string? content = null) => Proxy.CreateFileAsync(path, content);
public Task<bool> CreateFolderAsync(string path) => Proxy.CreateFolderAsync(path);
public Task<bool> CloseDocumentAsync(string path, bool save) => Proxy.CloseDocumentAsync(path, save);
public Task<bool> SaveDocumentAsync(string path) => Proxy.SaveDocumentAsync(path);
public Task<bool> RunCodeCleanupAsync(string path) => Proxy.RunCodeCleanupAsync(path);
Expand Down
26 changes: 26 additions & 0 deletions src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,30 @@ public async Task<string> 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<string> 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<string> 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}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public interface IVisualStudioRpc
Task<List<DocumentInfo>> GetOpenDocumentsAsync();
Task<DocumentInfo?> GetActiveDocumentAsync();
Task<bool> OpenDocumentAsync(string path);
Task<FileCreateResult> CreateFileAsync(string path, string? content = null);
Task<bool> CreateFolderAsync(string path);
Task<bool> CloseDocumentAsync(string path, bool save);
Task<bool> SaveDocumentAsync(string path);
Task<bool> RunCodeCleanupAsync(string path);
Expand Down Expand Up @@ -85,3 +87,4 @@ public interface IServerRpc
Task<List<ToolInfo>> GetAvailableToolsAsync();
Task ShutdownAsync();
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public interface IVisualStudioService
Task<List<DocumentInfo>> GetOpenDocumentsAsync();
Task<DocumentInfo?> GetActiveDocumentAsync();
Task<bool> OpenDocumentAsync(string path);
Task<FileCreateResult> CreateFileAsync(string path, string? content = null);
Task<bool> CreateFolderAsync(string path);
Task<bool> CloseDocumentAsync(string path, bool save = true);
Task<bool> SaveDocumentAsync(string path);
Task<bool> RunCodeCleanupAsync(string path);
Expand Down Expand Up @@ -69,3 +71,4 @@ public interface IVisualStudioService
Task<bool> ShowToolWindowAsync(string name);
Task<bool> HideToolWindowAsync(string caption);
}

3 changes: 3 additions & 0 deletions src/CodingWithCalvin.MCPServer/Services/RpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ public async Task RequestShutdownAsync()
public Task<List<DocumentInfo>> GetOpenDocumentsAsync() => _vsService.GetOpenDocumentsAsync();
public Task<DocumentInfo?> GetActiveDocumentAsync() => _vsService.GetActiveDocumentAsync();
public Task<bool> OpenDocumentAsync(string path) => _vsService.OpenDocumentAsync(path);
public Task<FileCreateResult> CreateFileAsync(string path, string? content = null) => _vsService.CreateFileAsync(path, content);
public Task<bool> CreateFolderAsync(string path) => _vsService.CreateFolderAsync(path);
public Task<bool> CloseDocumentAsync(string path, bool save) => _vsService.CloseDocumentAsync(path, save);
public Task<bool> SaveDocumentAsync(string path) => _vsService.SaveDocumentAsync(path);
public Task<bool> RunCodeCleanupAsync(string path) => _vsService.RunCodeCleanupAsync(path);
Expand Down Expand Up @@ -228,3 +230,4 @@ public Task<bool> WriteOutputPaneAsync(string paneIdentifier, string message, bo
public Task<bool> ShowToolWindowAsync(string name) => _vsService.ShowToolWindowAsync(name);
public Task<bool> HideToolWindowAsync(string caption) => _vsService.HideToolWindowAsync(caption);
}

78 changes: 78 additions & 0 deletions src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,84 @@ public async Task<List<DocumentInfo>> GetOpenDocumentsAsync()
};
}

public async Task<FileCreateResult> 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<bool> 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<bool> OpenDocumentAsync(string path)
{
using var activity = VsixTelemetry.Tracer.StartActivity("OpenDocument");
Expand Down
Loading