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
Markdown
---
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.