MCP Deep Dive, Part 12: C# 및 .NET 9를 이용한 MCP 서버 구축 — SDK, DI 및 Native AOT
요약
C# 및 .NET 9 환경에서 MCP(Model Context Protocol) 서버를 효율적으로 구축하는 방법을 다룹니다. MCP C# SDK를 활용하여 JSON-RPC 파싱, 의존성 주입(DI), Native AOT 적용 등 복잡한 배관 작업을 자동화하는 기술적 상세 내용을 제공합니다.
핵심 포인트
- MCP C# SDK를 통한 JSON-RPC 및 스키마 생성 자동화
- 기존 .NET 도메인 로직을 재사용하기 위한 최적의 방법론
- Attribute 기반의 간결한 도구(Tools) 등록 방식
- Native AOT 및 의존성 주입(DI)을 통한 고성능 서버 구축
우리는 이 시리즈를 통해 서버, 클라이언트, 도구, 인증(auth), 그리고 거버넌스(governance)를 모두 C#으로 구축해 왔습니다. 하지만 .NET 툴링이 이를 얼마나 즐겁게 만드는지 그 '방식'을 살펴보느라 속도를 늦춘 적은 없었습니다. 이번 파트에서는 그 부분을 다룹니다. 만약 여러분의 백엔드가 이미 .NET이라면, MCP 서버는 여러분이 구축할 수 있는 가장 레버리지가 높은(high-leverage) 요소 중 하나가 될 것입니다. 즉, 이미 보유하고 있는 도메인 서비스(domain services) 위에 구축된 얇고, 어트리뷰트(attributed) 기반이며, DI(의존성 주입) 네이티브인 인터페이스가 되는 것입니다.
이것은 Model Context Protocol (MCP)에 대한 15부작 심층 분석 중 12번째 파트입니다. 파트 3에서는 C#을 수단으로 사용하는 프로덕션 서버를 구축했습니다. 이번 파트는 그 수단 자체, 즉 MCP C# SDK, 어트리뷰트-투-스키마(attribute-to-schema) 모델, 의존성 주입(dependency injection, DI), 두 가지 호스팅 모델, 테스트, 그리고 Native AOT에 대해 다룹니다.
요약 (TL;DR)
| 고려 사항 | 직접 구현 (이전) | MCP C# SDK (이후) |
|---|---|---|
| 프로토콜 | JSON-RPC를 수동으로 파싱 | AddMcpServer()가 담당 |
| ... |
이 작업에 .NET을 사용해야 할까요?
먼저 솔직하게 말씀드리자면, Python 및 TypeScript SDK가 가장 성숙한 MCP 생태계이며, 여러분의 스택이 Python/TS라면 그것들을 사용하십시오. .NET으로 MCP 서버를 구축해야 하는 이유는 단 하나이지만 결정적입니다. 바로 여러분의 도메인이 이미 그곳에 있기 때문입니다. MCP 서버는 자신이 노출하는 데이터 및 로직 옆에 위치하여 이를 재사용해야 합니다. 단지 MCP를 지원하기 위해 C# 도메인을 Python으로 다시 작성하는 것은 순전한 낭비입니다.
여러분이 작성하는 것 vs SDK가 수행하는 것
여러분이 작성하는 것 SDK가 수행하는 것
------------------ -----------------
Program.cs (AddMcpServer, -> JSON-RPC 프레이밍 + 초기화(initialize) + 협상(negotiation)
...
1. SDK가 프로토콜을 관리합니다
두 개의 NuGet 패키지를 추가하여 SDK가 JSON-RPC, initialize, 기능 협상(capability negotiation), 그리고 스키마 생성(schema generation)을 담당하게 하세요.
<PackageReference Include="ModelContextProtocol" Version="*" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="*" />
builder.Services
.AddMcpServer(o => o.ServerInfo = new() { Name = "mattrx-analytics", Version = "2.4.0" })
.WithHttpTransport()
...
WithToolsFromAssembly()는 특성(Attribute)이 지정된 클래스들을 도구(Tools)로 반영하고 JSON-RPC 디스패치(dispatch)를 연결합니다. 즉, 여러분은 배관(plumbing) 작업이 아닌 기능(capabilities)을 등록하는 것입니다.
2. 특성(Attributes)을 통한 스키마 생성
타입이 지정된 메서드에 특성을 부여하면, SDK가 해당 시그니처(signature)로부터 스키마를 생성합니다.
[McpServerToolType]
public sealed class AnalyticsTools(ICampaignQueries campaigns, AiPrincipal principal)
{
...
이것이 .NET의 강력한 기능(killer feature)입니다. 여러분의 타입이 지정된 시그니처 자체가 곧 스키마가 됩니다. 매개변수는 속성(properties)이 되고, null 허용되지 않는(non-nullable) 매개변수는 required가 되며, [Description]은 모델이 읽는 설명(descriptions)이 됩니다. 유지보수 과정에서 실제 코드와 어긋나기 쉬운(drift-prone) 수동 작성 JSON 스키마를 관리할 필요가 없습니다.
3. DI 네이티브 도구
생성자 주입(Constructor injection) 방식입니다. SDK는 앱의 나머지 부분과 동일한 DI 컨테이너로부터 호출당 하나의 스코프(scoped) 도구 인스턴스를 해결(resolve)합니다.
builder.Services.AddScoped<ICampaignQueries, CampaignQueries>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IPrincipalAccessor>().Current); // AiPrincipal
// AnalyticsTools의 생성자 매개변수(ICampaignQueries, AiPrincipal)는 호출 시마다 주입됩니다.
도구는 생성자 의존성을 가진 클래스이며, MVC 컨트롤러와 정확히 동일하게 호출에 맞춰 스코프(scoped) 단위로 해결됩니다. 따라서 이미 보유하고 있는 도메인 서비스, 요청의 AiPrincipal, 그리고 스코프된 DbContext를 재사용할 수 있습니다. MCP 서버는 백엔드의 재구현체가 아니라, 통제된 얇은 인터페이스(thin governed surface)가 됩니다. 도구가 기존의 ICampaignQueries를 주입받기 때문에, MCP를 통해 분석 도메인을 노출하는 데 새로운 쿼리 코드가 거의 추가되지 않았습니다.
4. 두 가지 호스팅 모델, 하나의 도구 코드베이스
동일한 AnalyticsTools를 두 가지 방식으로 호스팅할 수 있습니다: stdio를 통한 콘솔 앱, 또는 Streamable HTTP를 통한 ASP.NET Core 앱입니다.
// stdio 호스트 — 로컬 개발 및 데스크톱 도구를 위한 콘솔 앱. 동일한 AnalyticsTools 사용.
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMcpServer().WithStdioServerTransport().WithToolsFromAssembly();
...
// HTTP 호스트 — 프로덕션 / 멀티 테넌트(multi-tenant)를 위한 ASP.NET Core. 동일한 AnalyticsTools 사용.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer().WithHttpTransport().WithToolsFromAssembly();
...
기능(capability)은 한 번만 작성하고, 배포 방식에 따라 호스트를 선택하세요. ASP.NET Core 앱은 이미 실행 중인 인증(auth), OTel(OpenTelemetry), 상태 확인(health-check) 미들웨어와 MCP 엔드포인트가 결합되는 지점입니다.
5. 실제로 테스트 가능한 MCP 서버
도구(Tools)는 일반 메서드이므로 직접 단위 테스트(unit-test)를 수행할 수 있으며, 인메모리 전송(in-memory transport)을 사용하면 모델이나 네트워크 없이도 서버 전체를 통합 테스트(integration-test)할 수 있습니다.
// 단위 테스트: 도구는 가짜 객체(fakes)가 주입된 일반 메서드입니다.
[Fact]
public async Task GetCampaignKpis_is_tenant_scoped()
...
도구는 의존성 주입 (DI) 메서드이기 때문에, 프로토콜이나 모델 없이도 다른 서비스와 마찬가지로 로직을 단위 테스트할 수 있습니다. 또한 인메모리 전송을 사용하면 tools/list 및 tools/call을 엔드 투 엔드(end-to-end)로 빠르고 결정론적(deterministic)으로 실행할 수 있습니다.
6. Native AOT 및 패키징
트리밍(Trim)을 수행하고 (트리밍이 안전한 경우) Native AOT 게시(publish)를 통해 작고 빠르게 시작되는 컨테이너를 만드세요.
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<InvariantGlobalization>true</InvariantGlobalization>
...
Container Apps 상의 MCP 서버는 수요에 따라 확장(scale out)되므로, 콜드 스타트(cold start)는 실제 비용이 됩니다. 트리밍은 이미지를 축소하고 Native AOT는 시작 시간을 대폭 단축합니다. 단, 한 가지 주의사항이 있습니다. 리플렉션(reflection) 기반의 스키마 생성은 자동으로 트리밍 안전(trim-safe)이 보장되지 않으므로, PublishAot를 활성화하기 전에 반드시 검증하거나 소스 생성기(source generators)에 의존하세요. 무작정 따라 하기(cargo-cult)는 금물입니다.
지속해 나갈 모델
MCP C# SDK는 서버가 이미 보유하고 있는 도메인 서비스 위에 속성(attribute)이 지정된 얇은 DI 네이티브 인터페이스로 존재할 때 최상의 성능을 발휘합니다. 도구를 타입이 지정된 메서드로 작성하고, SDK가 스키마를 생성하고 프로토콜을 관리하도록 두며, stdio 또는 ASP.NET Core를 통해 호스팅하고, 일반 메서드처럼 테스트하세요. 여기서 .NET의 가치는 새로운 프레임워크를 배우는 것이 아니라, _이미 사용 중인 프레임워크를 재사용하는 것_에 있습니다. 이것이 바로 MCP 서버가 기존 .NET 백엔드에서 매우 높은 레버리지(leverage)를 갖는 이유입니다.
.NET MCP 서버를 깔끔하게 만드는 세 가지 습관:
- 어트리뷰트(Attribute)가 스키마를 생성하게 하세요.
[Description]이 포함된 타입 지정 메서드(typed method)가 곧 계약(contract)입니다. JSON 스키마(JSON Schema)를 직접 작성하지 마세요. - 도구(Tool)를 가볍게 유지하고 DI(의존성 주입)를 네이티브하게 활용하세요. 도메인 서비스(domain services)를 해결(resolve)하고,
CancellationToken을 전달하며, 비즈니스 로직은 도메인에 남겨두세요. - 도구를 메서드로서 테스트하세요. 로직은 단위 테스트(unit-test)하고, 프로토콜은 인메모리 테스트(in-memory-test)하세요. 루프 안에 모델을 넣지 마세요.
원문은 prepstack.co.in에 게시되었습니다. 파트 13에서는 이 서버를 프로덕션 인프라로 확장합니다: 실제 규모의 Azure에서 MCP 호스팅하기.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기