diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..3ac1f28 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,15 @@ +{ + "mcpServers": { + "csharp-mcp": { + "type": "stdio", + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "ghcr.io/infinityflowapp/csharp-mcp:latest" + ], + "env": {} + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 13f70b4..68a5556 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,76 @@ The following namespaces are automatically available: ## MCP Configuration +### Cursor + +Add to your Cursor settings (`.cursor/mcp_settings.json` or via Settings UI): + +```json +{ + "mcpServers": { + "csharp-eval": { + "command": "docker", + "args": ["run", "-i", "--rm", "ghcr.io/infinityflowapp/csharp-mcp:latest"], + "env": { + "CSX_ALLOWED_PATH": "/scripts" + } + } + } +} +``` + +Or if installed as a dotnet tool: + +```json +{ + "mcpServers": { + "csharp-eval": { + "command": "infinityflow-csharp-eval", + "env": { + "CSX_ALLOWED_PATH": "${workspaceFolder}/scripts" + } + } + } +} +``` + +### Claude Code + +Add to your Claude Code configuration (`claude_desktop_config.json`): + +- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` +- **Linux**: `~/.config/claude/claude_desktop_config.json` + +```json +{ + "mcpServers": { + "csharp-eval": { + "command": "docker", + "args": ["run", "-i", "--rm", "ghcr.io/infinityflowapp/csharp-mcp:latest"], + "env": { + "CSX_ALLOWED_PATH": "/scripts" + } + } + } +} +``` + +Or if installed as a dotnet tool: + +```json +{ + "mcpServers": { + "csharp-eval": { + "command": "infinityflow-csharp-eval", + "env": { + "CSX_ALLOWED_PATH": "/Users/your-username/scripts" + } + } + } +} +``` + ### VS Code Create `.vscode/mcp.json`: diff --git a/src/InfinityFlow.CSharp.Eval/Tools/CSharpEvalTools.cs b/src/InfinityFlow.CSharp.Eval/Tools/CSharpEvalTools.cs index b755542..e40544a 100644 --- a/src/InfinityFlow.CSharp.Eval/Tools/CSharpEvalTools.cs +++ b/src/InfinityFlow.CSharp.Eval/Tools/CSharpEvalTools.cs @@ -9,7 +9,9 @@ namespace InfinityFlow.CSharp.Eval.Tools; /// /// MCP tool for evaluating and executing C# scripts using Roslyn. /// -internal class CSharpEvalTools + +[McpServerToolType] +public class CSharpEvalTools { [McpServerTool] [Description("Evaluates and executes C# script code and returns the output. Can either execute code directly or from a file.")] @@ -29,7 +31,7 @@ public async Task EvalCSharp( } string scriptCode; - + try { if (!string.IsNullOrWhiteSpace(csxFile)) @@ -38,13 +40,13 @@ public async Task EvalCSharp( try { var fullPath = Path.GetFullPath(csxFile); - + // Ensure the file has .csx extension if (!fullPath.EndsWith(".csx", StringComparison.OrdinalIgnoreCase)) { return $"Error: Only .csx files are allowed. Provided: {csxFile}"; } - + // Optional: Restrict to specific directories for additional security // This can be configured via environment variable var allowedPath = Environment.GetEnvironmentVariable("CSX_ALLOWED_PATH"); @@ -56,12 +58,12 @@ public async Task EvalCSharp( return $"Error: File access is restricted to {normalizedAllowedPath}"; } } - + if (!File.Exists(fullPath)) { return $"Error: File not found: {fullPath}"; } - + scriptCode = await File.ReadAllTextAsync(fullPath); } catch (Exception ex) @@ -101,7 +103,7 @@ public async Task EvalCSharp( // Capture console output var originalOut = Console.Out; var outputBuilder = new StringBuilder(); - + try { using var stringWriter = new StringWriter(outputBuilder); @@ -109,21 +111,21 @@ public async Task EvalCSharp( // Execute the script with timeout using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); - + // Run script in a task so we can properly handle timeout - var scriptTask = Task.Run(async () => - await CSharpScript.EvaluateAsync(scriptCode, scriptOptions, cancellationToken: cts.Token), + var scriptTask = Task.Run(async () => + await CSharpScript.EvaluateAsync(scriptCode, scriptOptions, cancellationToken: cts.Token), cts.Token); - + var timeoutTask = Task.Delay(TimeSpan.FromSeconds(timeoutSeconds)); var completedTask = await Task.WhenAny(scriptTask, timeoutTask); - + if (completedTask == timeoutTask) { cts.Cancel(); throw new OperationCanceledException(); } - + var result = await scriptTask; // Add the result value if it's not null @@ -149,18 +151,18 @@ await CSharpScript.EvaluateAsync(scriptCode, scriptOptions, cancellationToken: c var errorBuilder = new StringBuilder(); errorBuilder.AppendLine("Compilation Error(s):"); errorBuilder.AppendLine(); - + foreach (var diagnostic in e.Diagnostics) { var lineSpan = diagnostic.Location.GetLineSpan(); var line = lineSpan.StartLinePosition.Line + 1; // Convert to 1-based var column = lineSpan.StartLinePosition.Character + 1; - + errorBuilder.AppendLine($" Line {line}, Column {column}: {diagnostic.Id} - {diagnostic.GetMessage()}"); - + // Try to show the problematic code if available if (!diagnostic.Location.IsInSource) continue; - + var sourceText = diagnostic.Location.SourceTree?.GetText(); if (sourceText != null) { @@ -168,7 +170,7 @@ await CSharpScript.EvaluateAsync(scriptCode, scriptOptions, cancellationToken: c if (!string.IsNullOrWhiteSpace(lineText)) { errorBuilder.AppendLine($" Code: {lineText.Trim()}"); - + // Add a pointer to the error position if (column > 0 && column <= lineText.Length) { @@ -179,7 +181,7 @@ await CSharpScript.EvaluateAsync(scriptCode, scriptOptions, cancellationToken: c } errorBuilder.AppendLine(); } - + return errorBuilder.ToString().TrimEnd(); } catch (OperationCanceledException) @@ -191,7 +193,7 @@ await CSharpScript.EvaluateAsync(scriptCode, scriptOptions, cancellationToken: c var errorBuilder = new StringBuilder(); errorBuilder.AppendLine($"Runtime Error: {e.GetType().Name}"); errorBuilder.AppendLine($"Message: {e.Message}"); - + // Try to extract the line number from the stack trace if it's a script error if (e.StackTrace != null && e.StackTrace.Contains("Submission#0")) { @@ -209,16 +211,16 @@ await CSharpScript.EvaluateAsync(scriptCode, scriptOptions, cancellationToken: c } } } - + if (e.InnerException != null) { errorBuilder.AppendLine($"Inner Exception: {e.InnerException.GetType().Name}: {e.InnerException.Message}"); } - + errorBuilder.AppendLine(); errorBuilder.AppendLine("Stack Trace:"); errorBuilder.AppendLine(e.StackTrace); - + return errorBuilder.ToString().TrimEnd(); } }