
C#으로 MCP 서버 구축하기: 도구를 한 번만 작성하여 모든 Claude에서 사용하기
요약
C#을 사용하여 Model Context Protocol(MCP) 서버를 구축하는 방법을 설명합니다. MCP를 통해 도구를 독립적인 서버로 구현함으로써 Claude Desktop, Claude Code 등 다양한 AI 클라이언트에서 재사용할 수 있는 방법을 다룹니다.
핵심 포인트
- MCP는 도구를 독립형 서버로 만들어 다양한 AI 클라이언트와 연결하는 프로토콜입니다.
- 클라이언트와 서버는 별개의 프로세스로 동작하며 JSON-RPC를 통해 통신합니다.
- 서버는 도구(tools), 리소스(resources), 프롬프트(prompts)를 노출할 수 있습니다.
- stdio와 HTTP 두 가지 전송 계층을 통해 로컬 및 원격 통신을 지원합니다.
C#으로 MCP 서버 구축하기: 도구를 한 번만 작성하여 모든 Claude에서 사용하기
지난번에 우리는 C#으로 Claude 도구 사용 (tool-use) 에이전트를 구축했습니다. 잘 작동했지만, 모든 도구가 _그 하나의 콘솔 앱 내부_에만 존재했습니다. 만약 Claude Desktop, Claude Code, 또는 팀 동료의 에이전트 내에서 동일한 "주문 조회" 기능을 사용하고 싶다면, 도구와 그 배관(plumbing) 코드를 각각의 앱에 복사하여 붙여넣어야 했습니다. 도구가 단일 프로세스에 갇혀 모든 곳에서 재구현되어야 했던 것입니다. 이것이 바로 MCP가 해결하고자 하는 결합(coupling) 문제입니다.
**Model Context Protocol (MCP)**은 소유권의 개념을 뒤집습니다. 도구를 독립형 서버로서 한 번만 작성하고 표준 프로토콜을 통해 노출합니다. 그러면 Claude Desktop, Claude Code, 여러분의 자체 에이전트, 혹은 다음 분기에 출시될 그 어떤 것이든 MCP 클라이언트라면 무엇이든 연결하여 여러분의 도구를 무료로 사용할 수 있습니다. 이 포스트에서는 지난번에 사용했던 Aurora Coffee Co. 도구들을 그대로 가져와 .NET 기반의 실제 MCP 서버로 해방시킨 뒤, 이를 Claude에 연결해 보겠습니다.
MCP의 실제 정체
MCP는 AI 클라이언트를 여러분의 기능(capabilities)에 연결하기 위한 작은 클라이언트/서버 프로토콜(내부적으로는 JSON-RPC 사용)입니다. 시작하는 데 필요한 세 가지 개념은 다음과 같습니다:
- 클라이언트와 서버는 별개의 프로세스입니다. _클라이언트 (Client)_는 AI 앱(Claude Desktop, Claude Code)입니다. _서버 (Server)_는 여러분의 코드입니다. 이들은 공유된 함수 테이블이 아니라 전송 계층 (transport)을 통해 통신합니다.
- 서버는 세 가지를 노출합니다: 도구 (tools) (모델이 호출할 수 있는 동작 — 우리의 핵심 관심사), 리소스 (resources) (파일이나 행과 같이 읽을 수 있는 데이터), 그리고 프롬프트 (prompts) (재사용 가능한 프롬프트 템플릿)입니다. 오늘은 모두 도구에 집중합니다.
- 두 가지 전송 계층 (transports). stdio — 클라이언트가 여러분의 서버를 자식 프로세스로 실행하고 표준 입출력 (standard in/out)을 통해 통신합니다. 로컬 도구에 완벽합니다. HTTP — 서버가 어딘가에서 실행되고 클라이언트가 네트워크를 통해 연결됩니다. 공유되거나 원격에 있는 도구에 적합합니다.
이전 글을 읽으셨다면, 이것은 동일한 도구 사용 루프(tool-use loop) — 도구를 선언하고, 모델이 이를 호출하면, 여러분의 코드가 실행되고, 결과를 반환하는 과정 — 이지만, 이제 도구가 _프로세스 경계의 반대편_에 존재하여 특정 앱에 고정되지 않고 어떤 클라이언트든 재사용할 수 있게 됩니다.
우리가 구축할 서버
**Aurora Coffee Co.**를 기억하시나요? 해당 회사의 두 가지 도구는 인메모리 (in-memory) 데이터를 기반으로 지원 질문에 답변했습니다:
get_order_status(order_id)— 주문을 조회합니다.check_stock(sku)— 제품의 재고를 확인합니다.
지난번에는 이 도구들이 에이전트 루프 (agent loop) 안에 묻혀 있는 메서드였습니다. 이제 이것들은 어떤 Claude라도 호출할 수 있는 MCP 서버가 됩니다. 콘솔 앱을 시작하고 두 개의 패키지를 추가하세요:
dotnet new console -o AuroraCoffee.Mcp
cd AuroraCoffee.Mcp
dotnet add package ModelContextProtocol
...
ModelContextProtocol은 공식 C# SDK (1.0.0, Microsoft가 유지 관리)입니다. Microsoft.Extensions.Hosting은 제네릭 호스트 (generic host)를 제공하며, 이는 여러분이 어떤 .NET 서비스에서든 사용하는 것과 동일한 의존성 주입 (DI, Dependency Injection) 및 구성 방식을 제공합니다.
주입 가능한 서비스로서의 데이터
Aurora의 "데이터베이스"를 싱글톤 (Singleton)으로 유지하여, 도구들이 생성자 주입 (Constructor Injection)을 통해 이를 가져올 수 있도록 할 것입니다. 이는 실제 리포지토리 (Repository)와 정확히 동일한 방식입니다:
public sealed class CoffeeShopData
{
public Dictionary<string, Order> Orders { get; } = new()
...
도구 (The tools)
지난번의 수동으로 작성했던 JSON Schema와 switch 문을 대체하는 부분입니다. [McpServerToolType]으로 표시된 클래스, [McpServerTool]로 표시된 메서드, 그리고 메서드와 각 매개변수에 적용된 [Description]이 핵심입니다. SDK는 여러분의 **메서드 시그니처 (Method Signature)**를 읽어 JSON Schema를 자동으로 생성해 줍니다:
using System.ComponentModel;
using ModelContextProtocol.Server;
...
이 [Description] 속성들은 단순한 장식이 아닙니다. 지난 기사에서 다루었던, 실제 동작을 결정짓는 문서화 주석 (Docstrings)과 동일한 역할을 하며, 단지 더 보기 좋은 곳에 작성되었을 뿐입니다. 메서드에 붙은 설명은 Claude에게 도구를 언제 호출해야 하는지 알려주고, 매개변수에 붙은 설명은 무엇을 전달해야 하는지 알려줍니다. 여기서 모호한 설명을 사용하는 것은 모델이 멀쩡한 도구를 무시하게 만드는 가장 큰 원인이 됩니다.
사라진 부분들을 주목하세요: input_schema 객체도, required 배열도, 수동 인자 파싱 (Manual Argument Parsing)도 없습니다. orderId는 타입이 지정된 string 매개변수이므로, SDK는 이것이 필수적인 문자열임을 인지하고 파싱된 값을 여러분에게 전달합니다. 지난번 방식 1 (Approach 1)에서 직접 구현해야 했던 배관 작업 (Plumbing)은 이제 프레임워크의 역할이 되었습니다.
연결하기 (Wiring it together)
호스트 빌더 (Host Builder)는 데이터, MCP 서버, stdio 전송 계층 (Transport)을 등록하고, 어셈블리 (Assembly) 내의 모든 [McpServerToolType]을 자동으로 검색합니다:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
...
이것이 서버의 전부입니다. 로깅 (Logging) 라인을 다시 한번 읽어보세요. 왜냐하면 이것이 stdio MCP에서 가장 흔히 발생하는 실수 (Footgun)이기 때문입니다: stdout (표준 출력)은 JSON-RPC 메시지를 운반합니다. 만약 stdout으로 잘못된 Console.WriteLine을 한 번이라도 출력하면, Claude는 JSON이 와야 할 자리에 노이즈가 섞인 줄을 보게 됩니다. 이는 입에 음식을 가득 문 채로 말하는 것과 프로토콜 관점에서 동일한 상황입니다. 모든 로그를 stderr (표준 에러)로 라우팅하면 문제없이 작동합니다.
Claude에 연결하기
아무도 연결하지 않는 서버는 그저 매우 외로운 콘솔 앱일 뿐입니다. 이제 두 개의 클라이언트에 연결해 보겠습니다.
Claude Desktop은 JSON 설정 파일(claude_desktop_config.json, Windows의 경우 %APPDATA%\Claude\ 또는 macOS의 경우 ~/Library/Application Support/Claude/에 위치)을 읽습니다. mcpServers 항목 아래에 서버를 추가하세요:
{
"mcpServers": {
"aurora-coffee": {
...
클라이언트는 해당 명령어를 실행하고, 자식 프로세스(child process)와 stdio(표준 입출력)로 통신하며, 여러분의 도구(tools)들이 나타납니다. Claude Desktop을 재시작하고 _"주문 A-1001이 발송되었나요? 그리고 ETH-250 재고가 아직 남아 있나요?"_라고 물어보세요. 그러면 Claude가 두 도구를 모두 호출하여 실제로 답변할 것입니다.
Claude Code는 훨씬 더 간단합니다. 파일 편집 없이 CLI 명령어 하나로 충분합니다:
claude mcp add aurora-coffee -- dotnet run --project ./AuroraCoffee.Mcp
동일한 서버, 두 번째 클라이언트, 코드 변경 사항은 제로입니다. 이것이 MCP의 핵심입니다. 도구는 이동하지 않았지만, 이제 두 개의 서로 다른 Claude가 도구에 접근할 수 있으며, 아직 설치하지 않은 세 번째 Claude도 접근할 수 있습니다.
클라이언트 없이 테스트하기
클라이언트 설정을 건드리기 전에, MCP Inspector를 사용하여 서버를 건전성 검사(sanity-check)하세요. MCP Inspector는 프로토콜을 사용하는 브라우저 UI로, 도구 목록을 나열하고 수동으로 호출할 수 있습니다:
npx @modelcontextprotocol/inspector dotnet run --project ./AuroraCoffee.Mcp
이 명령은 서버를 실행하고, get_order_status와 check_stock을 나열하며, 테스트 호출을 보낼 수 있게 해줍니다. 만약 도구가 제대로 작동하지 않는다면, 채팅 기록의 세 단계 깊은 곳이 아니라 바로 여기서 문제를 발견할 수 있습니다.
외부 세계와 연결하기: DI 및 HttpClient
데모용으로는 인메모리 딕셔너리(in-memory dictionaries)도 괜찮지만, 실제 도구는 실제 시스템을 호출합니다. 서버는 일반적인 .NET 호스트(host)이므로, 타입이 지정된 HttpClient를 포함하여 의존성 주입 (Dependency Injection, DI)이 예상하는 대로 정확하게 작동합니다. Aurora에 테이스팅 노트를 위한 로스터리 API가 있다고 가정해 봅시다:
이를 타입이 지정된 클라이언트(typed client)로 등록하면 끝납니다. SDK가 도구의 생성자에 HttpClient를 주입(Injection)합니다.
builder.Services.AddHttpClient<RoastTools>();
두 가지 짚고 넘어갈 점이 있습니다. 첫째, 도구는 async일 수 있으며 Task<string>을 반환할 수 있습니다. 별도의 복잡한 절차 없이 그냥 await를 사용하면 됩니다. 둘째, 호출마다 HttpClient를 새로 생성(newing up)하기보다는 AddHttpClient<T>(또는 IHttpClientFactory)를 사용하십시오. 팩토리는 연결을 풀링(pooling)하여 부하가 걸리는 상황에서도 소켓이 고갈되지 않도록 합니다. 표준 .NET 위생 수칙(hygiene)이 그대로 적용되며, MCP는 이를 변경하지 않습니다.
원격으로 전환하기: HTTP 전송 (HTTP transport)
클라이언트가 로컬에서 서버를 실행할 수 있는 경우에는 stdio가 매우 훌륭합니다. 하지만 팀 전체가 공유하는 하나의 서버를 원하거나 컨테이너에서 실행하고 싶다면 HTTP가 필요합니다. 전송(transport) 패키지와 두 줄의 코드만 바꾸면 됩니다.
dotnet add package ModelContextProtocol.AspNetCore
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
...
동일한 CoffeeTools, 동일한 속성(attributes), 동일한 탐색(discovery) 방식이 유지됩니다. 오직 전송 방식과 호스트만 바뀌었을 뿐입니다. 이제 클라이언트는 프로세스를 실행하는 대신 URL을 통해 연결하며, ASP.NET Core가 이미 제공하는 모든 기능(인증 미들웨어, HTTPS, 호스팅, 확장성 등)을 사용할 수 있습니다. 여러분의 도구는 변화를 전혀 느끼지 못할 것입니다.
MCP 서버를 구축해야 할 때 — 그리고 구축하지 말아야 할 때
도구를 공유하거나 재사용해야 할 때 MCP를 고려하십시오. Claude Desktop과 Claude Code 모두에서 동일한 기능을 사용해야 하거나, 팀 전체가 연결하는 서버가 필요한 경우, 또는 특정 앱에 종속되지 않고 독자적인 릴리스 주기에 따라 진화시키고 싶은 도구의 경우에 해당합니다. 프로세스 경계(process boundary)는 하나의 기능입니다. 이를 통해 하나의 서버가 어떤 언어로 작성되었든 상관없이 여러 클라이언트에 서비스를 제공할 수 있습니다.
반면, 정확히 하나의 앱만 도구를 사용할 것이고 그 앱이 직접 모델 호출을 수행한다면 MCP를 건너뛰십시오. 그럴 때는 이전 글에서 다룬 앱 내부의 도구 사용(tool-use) 루프가 전송 계층도 필요 없고 별도의 프로세스도 필요 없으므로 더 단순하고 빠릅니다. 클라이언트가 자기 자신뿐인 서버는 그저 목걸이(lanyard)를 걸고 있는 함수 호출에 불과합니다.
경험 법칙(Rule of thumb): 하나의 앱이 모델과 도구를 모두 소유하고 있다면 앱 내 도구 사용(in-app tool use)을 사용하고, 두 개 이상의 클라이언트가 동일한 도구를 필요로 하는 순간 MCP 서버를 사용하세요.
핵심 요약 (Key Takeaways)
- MCP는 도구와 클라이언트를 분리합니다 — 도구를 서버로서 한 번만 작성하면, 각 앱에서 매번 재구현할 필요 없이 모든 MCP 클라이언트(Claude Desktop, Claude Code, 사용자 정의 에이전트 등)가 이를 사용할 수 있습니다.
- SDK는 속성(Attribute) 기반입니다 — 클래스에는
[McpServerToolType], 메서드에는[McpServerTool]을 사용하며, JSON 스키마(JSON Schema)는 메서드 시그니처(signature)로부터 자동으로 생성됩니다. 직접 스키마를 작성하거나 인자(argument)를 수동으로 파싱할 필요가 없습니다. - 설명(Description)은 여전히 매우 중요합니다 — 메서드와 각 매개변수(parameter)에 작성하는
[Description]은 Claude가 언제 도구를 호출할지, 그리고 무엇을 전달할지를 결정하는 근거가 됩니다. 이를 독스트링(docstring)처럼 작성하세요. - 표준 출력(stdout)을 보호하세요 — stdio 모드에서 프로토콜 자체가 곧 stdout입니다. 로그는 stderr로 보내야 하며, 그렇지 않으면 디버그 출력으로 인해 스트림(stream)이 손상될 수 있습니다.
- 도구는 그대로 두고 전송 방식(transport)만 교체하세요 — 로컬 자식 프로세스(child-process) 도구에는 stdio를 사용하고, 공유 또는 원격 도구에는 HTTP (
ModelContextProtocol.AspNetCore)를 사용하세요. 도구 코드는 변경되지 않습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기

