Перейти к содержимому

C# SDK (.NET)

Создание MCP серверов на C# с официальным Microsoft SDK

Официальный C# SDK обеспечивает полную интеграцию MCP протокола с .NET экосистемой, включая ASP.NET Core и dependency injection с атрибутами для регистрации.

Terminal
dotnet add package ModelContextProtocol

Требования:

  • .NET 8.0+
  • Visual Studio 2022 или VS Code
Program.cs
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/CalculatorTools.cs
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;
}
}
Program.cs
var builder = McpServerBuilder.Create("calculator", "1.0.0");
builder.AddTools<CalculatorTools>();
var server = builder.Build();
await server.RunStdioAsync();
Tools/HttpTools.cs
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();
}
}
Tools/FileTools.cs
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()!;
}
}
Tools/DatabaseTools.cs
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/ConfigResources.cs
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"]
});
}
}
Resources/FileResources.cs
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/PromptTemplates.cs
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);
}
}
Program.cs
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();
Program.cs
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 для MCP
app.MapMcpServer("/mcp");
// SSE endpoint
app.MapMcpSse("/mcp/sse");
app.Run();
Tools/SecureTools.cs
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,
"Внутренняя ошибка сервера"
);
}
}
}
Tools/LoggedTools.cs
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.md
Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["src/MyMcpServer/MyMcpServer.csproj", "MyMcpServer/"]
RUN dotnet restore "MyMcpServer/MyMcpServer.csproj"
COPY src/ .
RUN dotnet build "MyMcpServer/MyMcpServer.csproj" -c Release -o /app/build
RUN dotnet publish "MyMcpServer/MyMcpServer.csproj" -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/runtime:8.0
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyMcpServer.dll"]
claude_desktop_config.json
{
"mcpServers": {
"dotnet-server": {
"command": "dotnet",
"args": ["run", "--project", "/path/to/MyMcpServer"]
}
}
}
claude_desktop_config.json
{
"mcpServers": {
"dotnet-server": {
"command": "/path/to/MyMcpServer"
}
}
}
Tests/CalculatorToolsTests.cs
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);
}
}