UNPKG

agent-rules-kit

Version:

CLI tool to bootstrap AI agent rules for multiple IDEs and frameworks. Generates optimized rules for Cursor, VS Code, Claude, and 6+ other AI coding assistants.

509 lines (421 loc) 15 kB
--- globs: <root>/**/*.cs,<root>/*.csproj,<root>/*.sln alwaysApply: false --- # MCP C# SDK Implementation Guide ## C# SDK Features The C# SDK provides robust, enterprise-grade MCP implementations with strong typing, async/await patterns, and excellent .NET ecosystem integration. ### Project Configuration ```xml <!-- MyMCPServer.csproj --> <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <OutputType>Exe</OutputType> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="ModelContextProtocol.SDK" Version="1.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="System.Text.Json" Version="8.0.0" /> <!-- Testing --> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="Moq" Version="4.20.69" /> </ItemGroup> </Project> ``` ### Basic Server Implementation ```csharp using ModelContextProtocol.SDK.Server; using ModelContextProtocol.SDK.Types; using System.Text.Json; public class CSharpMCPServer : IMCPServerHandler { private readonly Dictionary<string, string> _resourceStorage = new(); private readonly ILogger<CSharpMCPServer> _logger; public CSharpMCPServer(ILogger<CSharpMCPServer> logger) { _logger = logger; InitializeResources(); } private void InitializeResources() { _resourceStorage["file://example.txt"] = "C# MCP Server content"; _resourceStorage["file://config.json"] = JsonSerializer.Serialize(new { Setting = "Value" }); } public async Task<IEnumerable<Resource>> ListResourcesAsync() { _logger.LogInformation("Listing {Count} resources", _resourceStorage.Count); return _resourceStorage.Keys.Select(uri => new Resource { Uri = uri, Name = Path.GetFileName(uri), Description = "Sample resource from C# server", MimeType = DetermineMimeType(uri) }); } public async Task<ResourceContent> ReadResourceAsync(string uri) { if (!_resourceStorage.TryGetValue(uri, out var content)) { throw new ResourceNotFoundException($"Resource not found: {uri}"); } _logger.LogInformation("Reading resource: {Uri}", uri); return new ResourceContent { Uri = uri, MimeType = DetermineMimeType(uri), Text = content }; } public async Task<IEnumerable<Tool>> ListToolsAsync() { return new[] { new Tool { Name = "echo", Description = "Echo back the input message", InputSchema = new JsonSchema { Type = "object", Properties = new Dictionary<string, JsonSchema> { ["message"] = new JsonSchema { Type = "string", Description = "Message to echo" } }, Required = new[] { "message" } } }, new Tool { Name = "calculate", Description = "Perform mathematical calculations", InputSchema = new JsonSchema { Type = "object", Properties = new Dictionary<string, JsonSchema> { ["expression"] = new JsonSchema { Type = "string", Description = "Math expression" }, ["precision"] = new JsonSchema { Type = "number", Description = "Decimal precision", Default = 2 } }, Required = new[] { "expression" } } } }; } public async Task<ToolResult> CallToolAsync(string name, Dictionary<string, object> arguments) { _logger.LogInformation("Calling tool: {ToolName} with {ArgCount} arguments", name, arguments.Count); return name switch { "echo" => await HandleEchoAsync(arguments), "calculate" => await HandleCalculateAsync(arguments), _ => throw new ToolNotFoundException($"Unknown tool: {name}") }; } private async Task<ToolResult> HandleEchoAsync(Dictionary<string, object> arguments) { if (!arguments.TryGetValue("message", out var messageObj) || messageObj is not string message) { throw new ArgumentException("Message parameter is required"); } return new ToolResult { Content = new[] { new TextContent { Type = "text", Text = $"Echo: {message}" } } }; } private async Task<ToolResult> HandleCalculateAsync(Dictionary<string, object> arguments) { try { if (!arguments.TryGetValue("expression", out var exprObj) || exprObj is not string expression) { throw new ArgumentException("Expression parameter is required"); } var precision = arguments.TryGetValue("precision", out var precObj) && precObj is JsonElement precElement ? precElement.GetInt32() : 2; // Simple expression evaluation (use a proper library in production) var result = EvaluateExpression(expression); var formattedResult = Math.Round(result, precision); return new ToolResult { Content = new[] { new TextContent { Type = "text", Text = $"Result: {formattedResult}" } } }; } catch (Exception ex) { _logger.LogError(ex, "Error calculating expression"); return new ToolResult { Content = new[] { new TextContent { Type = "text", Text = $"Error: {ex.Message}" } }, IsError = true }; } } private static string DetermineMimeType(string uri) => uri switch { var u when u.EndsWith(".json") => "application/json", var u when u.EndsWith(".txt") => "text/plain", var u when u.EndsWith(".xml") => "application/xml", _ => "application/octet-stream" }; private static double EvaluateExpression(string expression) { // Simplified expression evaluator - use a proper library like NCalc in production return double.Parse(expression); } } // Program.cs using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ModelContextProtocol.SDK.Server; using ModelContextProtocol.SDK.Transport; var builder = Host.CreateApplicationBuilder(args); builder.Services.AddLogging(); builder.Services.AddSingleton<CSharpMCPServer>(); builder.Services.AddSingleton<IMCPServer>(provider => { var handler = provider.GetRequiredService<CSharpMCPServer>(); var logger = provider.GetRequiredService<ILogger<MCPServer>>(); return new MCPServer( name: "csharp-mcp-server", version: "1.0.0", capabilities: new ServerCapabilities { Resources = new ResourceCapabilities { Subscribe = true, ListChanged = true }, Tools = new ToolCapabilities { ListChanged = true } }, handler: handler, logger: logger ); }); var host = builder.Build(); var server = host.Services.GetRequiredService<IMCPServer>(); var transport = new StdioTransport(); await server.StartAsync(transport); Console.WriteLine("C# MCP Server started"); await host.WaitForShutdownAsync(); ``` ### Client Implementation ```csharp using ModelContextProtocol.SDK.Client; using ModelContextProtocol.SDK.Types; using ModelContextProtocol.SDK.Transport; public class CSharpMCPClient : IDisposable { private readonly MCPClient _client; private StdioTransport? _transport; public CSharpMCPClient() { _client = new MCPClient( name: "csharp-mcp-client", version: "1.0.0", capabilities: new ClientCapabilities() ); } public async Task ConnectAsync(string serverCommand, params string[] args) { _transport = new StdioTransport(serverCommand, args); await _client.ConnectAsync(_transport); } public async Task<IEnumerable<Resource>> ListResourcesAsync() => await _client.ListResourcesAsync(); public async Task<ResourceContent> ReadResourceAsync(string uri) => await _client.ReadResourceAsync(uri); public async Task<IEnumerable<Tool>> ListToolsAsync() => await _client.ListToolsAsync(); public async Task<ToolResult> CallToolAsync(string name, Dictionary<string, object> arguments) => await _client.CallToolAsync(name, arguments); public async Task DisconnectAsync() { if (_client != null) await _client.DisconnectAsync(); } public void Dispose() { _transport?.Dispose(); _client?.Dispose(); } } // Usage example public static async Task Main(string[] args) { using var client = new CSharpMCPClient(); try { await client.ConnectAsync("dotnet", "run", "--project", "Server"); // List resources var resources = await client.ListResourcesAsync(); Console.WriteLine($"Resources: {string.Join(", ", resources.Select(r => r.Name))}"); // Call tools var echoResult = await client.CallToolAsync("echo", new Dictionary<string, object> { ["message"] = "Hello from C#!" }); Console.WriteLine($"Echo result: {echoResult.Content.First().Text}"); } finally { await client.DisconnectAsync(); } } ``` ### ASP.NET Core Integration ```csharp using Microsoft.AspNetCore.Mvc; using ModelContextProtocol.SDK.Server; using ModelContextProtocol.SDK.Transport; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddSingleton<CSharpMCPServer>(); builder.Services.AddSingleton<IMCPServer>(provider => { var handler = provider.GetRequiredService<CSharpMCPServer>(); return new MCPServer("aspnet-mcp-server", "1.0.0", new ServerCapabilities(), handler); }); var app = builder.Build(); app.MapControllers(); // Start MCP server on HTTP transport var mcpServer = app.Services.GetRequiredService<IMCPServer>(); var httpTransport = new HttpTransport(port: 8080, path: "/mcp"); _ = Task.Run(() => mcpServer.StartAsync(httpTransport)); app.Run(); [ApiController] [Route("api/[controller]")] public class MCPController : ControllerBase { private readonly IMCPServer _mcpServer; public MCPController(IMCPServer mcpServer) { _mcpServer = mcpServer; } [HttpGet("status")] public IActionResult GetStatus() { return Ok(new { Status = "Running", Server = _mcpServer.Name }); } } ``` ### Testing with xUnit ```csharp using Microsoft.Extensions.Logging; using Moq; using Xunit; public class CSharpMCPServerTests { private readonly CSharpMCPServer _server; private readonly Mock<ILogger<CSharpMCPServer>> _loggerMock; public CSharpMCPServerTests() { _loggerMock = new Mock<ILogger<CSharpMCPServer>>(); _server = new CSharpMCPServer(_loggerMock.Object); } [Fact] public async Task ListResourcesAsync_ShouldReturnResources() { // Act var resources = await _server.ListResourcesAsync(); // Assert Assert.NotEmpty(resources); Assert.Contains(resources, r => r.Name == "example.txt"); } [Fact] public async Task CallToolAsync_Echo_ShouldReturnEchoedMessage() { // Arrange var arguments = new Dictionary<string, object> { ["message"] = "test message" }; // Act var result = await _server.CallToolAsync("echo", arguments); // Assert Assert.False(result.IsError); Assert.Equal("Echo: test message", result.Content.First().Text); } [Fact] public async Task ReadResourceAsync_NonExistentResource_ShouldThrowException() { // Act & Assert await Assert.ThrowsAsync<ResourceNotFoundException>( () => _server.ReadResourceAsync("file://nonexistent.txt")); } } ``` ### Performance Optimization ```csharp using System.Collections.Concurrent; using Microsoft.Extensions.Caching.Memory; public class OptimizedCSharpMCPServer : IMCPServerHandler { private readonly IMemoryCache _cache; private readonly SemaphoreSlim _semaphore; private readonly ConcurrentDictionary<string, Task<string>> _loadingTasks; public OptimizedCSharpMCPServer(IMemoryCache cache) { _cache = cache; _semaphore = new SemaphoreSlim(10, 10); // Limit concurrent operations _loadingTasks = new ConcurrentDictionary<string, Task<string>>(); } public async Task<ResourceContent> ReadResourceAsync(string uri) { // Check cache first if (_cache.TryGetValue(uri, out string? cachedContent)) { return new ResourceContent { Uri = uri, Text = cachedContent }; } // Prevent duplicate loading var loadingTask = _loadingTasks.GetOrAdd(uri, async key => { await _semaphore.WaitAsync(); try { var content = await LoadResourceFromStorageAsync(key); // Cache with expiration _cache.Set(key, content, TimeSpan.FromMinutes(5)); return content; } finally { _semaphore.Release(); _loadingTasks.TryRemove(key, out _); } }); var result = await loadingTask; return new ResourceContent { Uri = uri, Text = result, MimeType = DetermineMimeType(uri) }; } private async Task<string> LoadResourceFromStorageAsync(string uri) { // Simulate expensive I/O operation await Task.Delay(100); return $"Loaded content for {uri}"; } } ``` This C# SDK implementation provides enterprise-grade patterns with strong typing, dependency injection, and comprehensive error handling for building robust MCP applications in the .NET ecosystem.