C# SDK (.NET)
Создание MCP серверов на C# с официальным Microsoft SDK
Официальный C# SDK обеспечивает полную интеграцию MCP протокола с .NET экосистемой, включая ASP.NET Core и dependency injection с атрибутами для регистрации.
Установка
Заголовок раздела «Установка»dotnet add package ModelContextProtocoldotnet add package McpDotNetТребования:
- .NET 8.0+
- Visual Studio 2022 или VS Code
Базовый сервер
Заголовок раздела «Базовый сервер»using ModelContextProtocol;using ModelContextProtocol.Server;
var builder = McpServerBuilder.Create("my-server", "1.0.0");builder.WithDescription("MCP сервер на C#");
var server = builder.Build();await server.RunStdioAsync();Инструменты (Tools)
Заголовок раздела «Инструменты (Tools)»Базовый инструмент
Заголовок раздела «Базовый инструмент»using ModelContextProtocol.Server;using System.ComponentModel;
[McpServerToolType] // Атрибут класса для автоматического обнаруженияpublic class CalculatorTools{ [McpServerTool("add"), Description("Сложение двух чисел")] public static double Add(double a, double b) { return a + b; }
[McpServerTool("divide"), Description("Деление чисел")] public static double Divide(double a, double b) { if (b == 0) throw new McpException(McpErrorCode.InvalidParams, "Деление на ноль");
return a / b; }}Регистрация инструментов
Заголовок раздела «Регистрация инструментов»var builder = McpServerBuilder.Create("calculator", "1.0.0");builder.AddTools<CalculatorTools>();
var server = builder.Build();await server.RunStdioAsync();Async инструмент
Заголовок раздела «Async инструмент»public class HttpTools{ private readonly HttpClient _client;
public HttpTools(HttpClient client) { _client = client; }
[McpTool("fetch_url", "Загрузка содержимого URL")] public async Task<string> FetchUrlAsync( [McpParameter("URL для загрузки")] string url) { var response = await _client.GetAsync(url); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); }}Инструмент с файловой системой
Заголовок раздела «Инструмент с файловой системой»public class FileTools{ private readonly string _allowedPath;
public FileTools(IConfiguration config) { _allowedPath = config["AllowedPath"] ?? "/allowed"; }
[McpTool("read_file", "Чтение файла")] public async Task<string> ReadFileAsync( [McpParameter("Путь к файлу")] string path) { var fullPath = Path.GetFullPath(path);
if (!fullPath.StartsWith(_allowedPath)) throw new McpException(McpErrorCode.Forbidden, "Доступ запрещён");
if (!File.Exists(fullPath)) throw new McpException(McpErrorCode.NotFound, $"Файл не найден: {path}");
return await File.ReadAllTextAsync(fullPath); }
[McpTool("list_files", "Список файлов в директории")] public string[] ListFiles( [McpParameter("Путь к директории")] string path, [McpParameter("Паттерн поиска", Required = false)] string? pattern = "*") { var fullPath = Path.GetFullPath(path);
if (!fullPath.StartsWith(_allowedPath)) throw new McpException(McpErrorCode.Forbidden, "Доступ запрещён");
return Directory.GetFiles(fullPath, pattern ?? "*") .Select(Path.GetFileName) .ToArray()!; }}Инструмент с базой данных
Заголовок раздела «Инструмент с базой данных»using Microsoft.EntityFrameworkCore;
public class DatabaseTools{ private readonly AppDbContext _db;
public DatabaseTools(AppDbContext db) { _db = db; }
[McpTool("get_users", "Получение списка пользователей")] public async Task<object[]> GetUsersAsync( [McpParameter("Лимит", Required = false)] int limit = 10, [McpParameter("Смещение", Required = false)] int offset = 0) { return await _db.Users .Skip(offset) .Take(limit) .Select(u => new { u.Id, u.Name, u.Email }) .ToArrayAsync(); }
[McpTool("find_user", "Поиск пользователя")] public async Task<object?> FindUserAsync( [McpParameter("ID пользователя")] int id) { var user = await _db.Users.FindAsync(id);
if (user == null) throw new McpException(McpErrorCode.NotFound, "Пользователь не найден");
return new { user.Id, user.Name, user.Email, user.CreatedAt }; }}Ресурсы (Resources)
Заголовок раздела «Ресурсы (Resources)»public class ConfigResources{ private readonly IConfiguration _config;
public ConfigResources(IConfiguration config) { _config = config; }
[McpResource("config://app", "App Configuration", "Конфигурация приложения")] public string GetAppConfig() { var config = new { Version = _config["Version"], Environment = _config["Environment"], Features = _config.GetSection("Features").Get<string[]>() };
return JsonSerializer.Serialize(config); }
[McpResource("config://database", "Database Configuration", "Конфигурация БД")] public string GetDatabaseConfig() { return JsonSerializer.Serialize(new { Provider = _config["Database:Provider"], Host = _config["Database:Host"], Database = _config["Database:Name"] }); }}Динамический ресурс
Заголовок раздела «Динамический ресурс»public class FileResources{ [McpResource("file://{path}", "File Reader", "Чтение файлов")] public async Task<ResourceContent> ReadFileAsync(string path) { var content = await File.ReadAllTextAsync(path); var mimeType = GetMimeType(path);
return new ResourceContent { Uri = $"file://{path}", MimeType = mimeType, Text = content }; }
private static string GetMimeType(string path) { return Path.GetExtension(path).ToLower() switch { ".json" => "application/json", ".xml" => "application/xml", ".html" => "text/html", ".css" => "text/css", ".js" => "application/javascript", _ => "text/plain" }; }}Промпты (Prompts)
Заголовок раздела «Промпты (Prompts)»public class PromptTemplates{ [McpPrompt("code_review", "Промпт для код-ревью")] public static IEnumerable<Message> CodeReview( [McpParameter("Язык программирования")] string language, [McpParameter("Код для ревью")] string code) { yield return Message.User($@"Проведи код-ревью следующего {language} кода:
```{language}{code}```
Обрати внимание на:1. Потенциальные баги2. Производительность3. Безопасность4. Best practices для {language}"); }
[McpPrompt("sql_expert", "Помощник по SQL")] public static IEnumerable<Message> SqlExpert( [McpParameter("Вопрос о SQL")] string question, [McpParameter("Схема БД", Required = false)] string? schema = null) { yield return Message.Assistant("Я SQL эксперт. Помогу с запросами и оптимизацией.");
var prompt = schema != null ? $"Схема БД:\n{schema}\n\nВопрос: {question}" : question;
yield return Message.User(prompt); }}Dependency Injection
Заголовок раздела «Dependency Injection»using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// Регистрация сервисовbuilder.Services.AddHttpClient();builder.Services.AddDbContext<AppDbContext>();builder.Services.AddTransient<FileTools>();builder.Services.AddTransient<DatabaseTools>();builder.Services.AddTransient<HttpTools>();
// Настройка MCP сервераbuilder.Services.AddMcpServer("my-server", "1.0.0", mcp =>{ mcp.AddTools<CalculatorTools>(); mcp.AddTools<FileTools>(); mcp.AddTools<DatabaseTools>(); mcp.AddTools<HttpTools>(); mcp.AddResources<ConfigResources>(); mcp.AddPrompts<PromptTemplates>();});
var host = builder.Build();await host.RunAsync();ASP.NET Core интеграция
Заголовок раздела «ASP.NET Core интеграция»var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer("web-server", "1.0.0", mcp =>{ mcp.AddTools<ApiTools>(); mcp.AddResources<WebResources>();});
var app = builder.Build();
// HTTP endpoint для MCPapp.MapMcpServer("/mcp");
// SSE endpointapp.MapMcpSse("/mcp/sse");
app.Run();Обработка ошибок
Заголовок раздела «Обработка ошибок»public class SecureTools{ [McpTool("secure_operation", "Безопасная операция")] public async Task<string> SecureOperationAsync( [McpParameter("Данные")] string data) { try { // Валидация if (string.IsNullOrWhiteSpace(data)) throw new McpException( McpErrorCode.InvalidParams, "Данные не могут быть пустыми" );
// Авторизация if (!await IsAuthorizedAsync()) throw new McpException( McpErrorCode.Forbidden, "Недостаточно прав" );
// Выполнение return await ProcessDataAsync(data); } catch (OperationCanceledException) { throw new McpException( McpErrorCode.RequestCancelled, "Операция отменена" ); } catch (Exception ex) when (ex is not McpException) { // Логирование _logger.LogError(ex, "Ошибка при выполнении операции");
throw new McpException( McpErrorCode.InternalError, "Внутренняя ошибка сервера" ); } }}Логирование
Заголовок раздела «Логирование»using Microsoft.Extensions.Logging;
public class LoggedTools{ private readonly ILogger<LoggedTools> _logger;
public LoggedTools(ILogger<LoggedTools> logger) { _logger = logger; }
[McpTool("process", "Обработка данных")] public async Task<string> ProcessAsync(string data) { _logger.LogInformation("Начало обработки данных: {Length} символов", data.Length);
try { var result = await DoProcessAsync(data); _logger.LogInformation("Обработка завершена успешно"); return result; } catch (Exception ex) { _logger.LogError(ex, "Ошибка обработки"); throw; } }}Структура проекта
Заголовок раздела «Структура проекта»MyMcpServer/├── src/│ ├── MyMcpServer/│ │ ├── Program.cs│ │ ├── Tools/│ │ │ ├── CalculatorTools.cs│ │ │ └── FileTools.cs│ │ ├── Resources/│ │ │ └── ConfigResources.cs│ │ ├── Prompts/│ │ │ └── PromptTemplates.cs│ │ └── MyMcpServer.csproj│ └── MyMcpServer.Tests/├── Dockerfile├── MyMcpServer.sln└── README.mdFROM mcr.microsoft.com/dotnet/sdk:8.0 AS buildWORKDIR /srcCOPY ["src/MyMcpServer/MyMcpServer.csproj", "MyMcpServer/"]RUN dotnet restore "MyMcpServer/MyMcpServer.csproj"COPY src/ .RUN dotnet build "MyMcpServer/MyMcpServer.csproj" -c Release -o /app/buildRUN dotnet publish "MyMcpServer/MyMcpServer.csproj" -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/runtime:8.0WORKDIR /appCOPY --from=build /app/publish .ENTRYPOINT ["dotnet", "MyMcpServer.dll"]Конфигурация Claude Desktop
Заголовок раздела «Конфигурация Claude Desktop»{ "mcpServers": { "dotnet-server": { "command": "dotnet", "args": ["run", "--project", "/path/to/MyMcpServer"] } }}Опубликованное приложение
Заголовок раздела «Опубликованное приложение»{ "mcpServers": { "dotnet-server": { "command": "/path/to/MyMcpServer" } }}Тестирование
Заголовок раздела «Тестирование»using Xunit;using ModelContextProtocol.Testing;
public class CalculatorToolsTests{ [Fact] public void Add_ReturnsCorrectSum() { var result = CalculatorTools.Add(2, 3); Assert.Equal(5, result); }
[Fact] public void Divide_ThrowsOnZero() { var exception = Assert.Throws<McpException>(() => CalculatorTools.Divide(1, 0));
Assert.Equal(McpErrorCode.InvalidParams, exception.Code); }}