C#로 MCP 서버 만들기: 도구를 한 번만 작성하여 모든 Claude에서 사용하기
요약
C#을 사용하여 Model Context Protocol(MCP) 서버를 구축하는 방법을 설명합니다. MCP를 통해 도구를 독립적인 서버로 구현함으로써 Claude Desktop, Claude Code 등 다양한 클라이언트에서 재사용할 수 있는 구조를 만드는 과정을 다룹니다.
핵심 포인트
- MCP는 도구와 클라이언트를 분리하여 재사용성을 높이는 프로토콜입니다.
- 서버는 도구(tools), 리소스(resources), 프롬프트(prompts)를 노출할 수 있습니다.
- stdio 및 HTTP 전송 계층을 통해 클라이언트와 통신합니다.
- C# .NET 환경에서 MCP 서버를 구현하여 도구의 결합도를 낮출 수 있습니다.
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 클라이언트를 여러분의 기능과 연결하기 위한 작은 클라이언트/서버 프로토콜(내부적으로는 JSON-RPC 사용)입니다. 세 가지 아이디어만 있으면 바로 시작할 수 있습니다:
- 클라이언트와 서버는 분리된 프로세스입니다. _클라이언트 (Client)_는 AI 앱(Claude Desktop, Claude Code)이며, _서버 (Server)_는 당신의 코드입니다. 이들은 공유 함수 테이블이 아니라 전송 계층 (Transport)을 통해 통신합니다.
- 서버는 세 가지를 노출합니다: 도구 (tools) (모델이 호출할 수 있는 동작 — 우리의 핵심 초점), 리소스 (resources) (파일이나 행과 같이 읽을 수 있는 데이터), 그리고 프롬프트 (prompts) (재사용 가능한 프롬프트 템플릿)입니다. 현재는 모두 도구 (tools) 중심입니다.
- 두 가지 전송 계층 (Transports). stdio — 클라이언트가 당신의 서버를 자식 프로세스로 실행하고 표준 입출력을 통해 통신합니다. 로컬 도구에 이상적입니다. HTTP — 서버가 어딘가에서 실행되고 클라이언트가 네트워크를 통해 연결됩니다. 공유 도구 또는 원격 도구에 적합합니다.
이전 글을 읽으셨다면, 이것이 동일한 도구 사용 (tool use) 루프임을 알 수 있을 것입니다. 즉, 도구를 선언하고, 모델이 이를 호출하며, 당신의 코드가 이를 실행하고, 결과를 반환하는 과정입니다. 하지만 이제 도구는 단일 앱 내에 하드코딩되는 대신, _프로세스 경계 너머_에 존재하며 모든 클라이언트에서 재사용할 수 있습니다.
우리가 구축할 서버
**Aurora Coffee Co.**를 기억하시나요? 그들의 두 가지 도구는 메모리 상의 데이터에 대한 지원 질문에 답변했습니다:
get_order_status(order_id)— 주문을 조회합니다.check_stock(sku)— 제품의 재고를 확인합니다.
지난번에는 이 도구들이 에이전트 루프 안에 묻혀 있는 메서드였습니다. 이제는 어떠한 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) 및 구성 방식입니다.
주입 가능한 서비스로서의 데이터
우리는 Aurora의 "데이터베이스"를 싱글톤 (singleton)으로 저장하여, 실제 레포지토리 (repository)와 마찬가지로 도구들이 생성자 주입 (constructor injection)을 통해 이를 전달받을 수 있도록 합니다:
public sealed class CoffeeShopData
{
public Dictionary<string, Order> Orders { get; } = new()
...
도구 (Tools)
이 부분은 지난번의 수동으로 작성한 JSON Schema와 switch 문을 대체하는 부분입니다. [McpServerToolType]으로 표시된 클래스, [McpServerTool]로 표시된 메서드, 그리고 메서드와 그 매개변수 모두에 적용된 [Description]이 사용됩니다. SDK는 여러분의 **메서드 시그니처 (method signature)**를 읽어 JSON Schema를 생성합니다:
using System.ComponentModel;
using ModelContextProtocol.Server;
...
이 [Description] 속성들은 단순한 장식이 아닙니다. 이전 글에서 다루었던 무게감 있는 docstring과 동일한 역할을 하며, 단지 더 보기 좋은 곳에 작성되었을 뿐입니다. 메서드에 붙은 설명은 Claude에게 도구를 언제 호출해야 하는지 알려주고, 매개변수에 붙은 설명은 무엇을 전달해야 하는지 알려줍니다. 여기서 모호한 설명을 사용하는 것은 모델이 완벽하게 작동하는 도구를 무시하게 만드는 첫 번째 원인이 됩니다.
무엇이 사라졌는지 주목해 보세요: input_schema 객체도, required 배열도, 인수의 수동 파싱 (manual parsing)도 없습니다. orderId는 타입이 지정된 string 매개변수이므로, SDK는 이것이 필수적인 문자열임을 알고 이미 파싱된 값을 전달해 줍니다. 지난번 '접근 방식 1 (Approach 1)'에서 다루었던 복잡한 배관 작업(plumbing)은 이제 프레임워크의 몫이 되었습니다.
모두 결합하기
호스트 빌더 (host builder)는 데이터, MCP 서버, stdio 전송 계층 (transport)을 등록하고 어셈블리(assembly) 내의 모든 [McpServerToolType]을 자동으로 검색합니다:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
...
이것이 서버의 전체 모습입니다. 로깅(logging) 라인을 다시 한번 읽어보세요. 왜냐하면 여기에 stdio 방식 MCP에서 가장 흔히 발생하는 함정이 있기 때문입니다: stdout은 JSON-RPC 메시지를 전송합니다. 실수로 Console.WriteLine을 사용하여 stdout으로 메시지를 보내면, Claude는 JSON을 기대하던 곳에서 노이즈를 발견하게 됩니다. 이는 프로토콜 관점에서 보면 입에 음식을 가득 물고 말하는 것과 같습니다. 모든 로그를 stderr로 보내면 문제가 발생하지 않습니다.
Claude에 연결하기
아무도 연결하지 않는 서버는 그저 아주 외로운 콘솔 앱일 뿐입니다. 이제 두 개의 클라이언트에 연결해 보겠습니다.
Claude Desktop은 JSON 설정 파일(%APPDATA%\Claude\의 Windows 또는 ~/Library/Application Support/Claude/의 macOS에 있는 claude_desktop_config.json)을 읽습니다. mcpServers 항목 아래에 귀하의 서버를 추가하세요:
{
"mcpServers": {
"aurora-coffee": {
...
클라이언트가 해당 명령을 실행하고 자식 프로세스와 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를 사용하여 서버를 확인하세요. 이는 브라우저 UI를 통해 프로토콜로 통신하며, 도구 목록을 확인하고 수동으로 호출할 수 있게 해줍니다:
npx @modelcontextprotocol/inspector dotnet run --project ./AuroraCoffee.Mcp
서버를 실행하고 get_order_status와 check_stock을 나열하면 테스트 호출을 실행할 수 있습니다. 도구가 제대로 작동하지 않는다면, 채팅 트랜스크립트(transcript)의 세 단계 깊숙한 곳이 아니라 바로 여기서 확인할 수 있습니다.
외부 세계로 나아가기: DI 및 HttpClient
메모리 내 딕셔너리(dictionaries)는 데모용으로는 적합하지만, 실제 도구(tools)는 실제 시스템을 호출합니다. 서버가 일반적인 .NET 호스트이므로, 타입이 지정된 HttpClient를 포함하여 의존성 주입 (Dependency Injection, DI)이 기대하는 대로 작동합니다. Aurora가 테이스팅 노트를 위한 토스터 API를 가지고 있다고 가정해 봅시다:
using System.ComponentModel;
using ModelContextProtocol.Server;
...
이를 타입 지정된 클라이언트로 등록하기만 하면 끝납니다. SDK가 도구의 생성자에 HttpClient를 주입합니다:
builder.Services.AddHttpClient<RoastTools>();
두 가지 강조할 점이 있습니다. 첫째, 도구는 async일 수 있으며 Task<string>을 반환할 수 있습니다. 복잡한 절차 없이 그저 await만 사용하면 됩니다. 둘째, 호출마다 HttpClient를 인스턴스화하는 대신 AddHttpClient<T> (또는 IHttpClientFactory)를 사용하십시오. 팩토리(factory)는 연결을 재사용하여 부하가 걸릴 때 소켓(sockets)이 고갈되지 않도록 합니다. .NET에서 항상 지키던 위생 수칙을 적용하십시오. MCP는 그 무엇도 바꾸지 않습니다.
원격 모드로 전환하기: HTTP 전송 (transport)
클라이언트가 로컬에서 서버를 실행할 수 있는 경우에는 stdio가 훌륭합니다. 하지만 팀이 공유하는 하나의 서버를 원하거나, 컨테이너에서 실행하고 싶다면 HTTP가 필요합니다. 전송 패키지와 두 줄의 코드만 변경하면 됩니다:
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) 루프가 전송 계층(transport)이나 별도의 프로세스 없이 더 단순하고 빠릅니다. 유일한 클라이언트가 자기 자신뿐인 서버는 프로토콜의 탈을 쓴 함수 호출에 불과합니다.
일반 원칙: 앱이 모델과 도구를 모두 소유하고 있다면 앱 내부에서 도구 사용 (tool use)을 수행하고, 둘 이상의 클라이언트가 동일한 도구를 필요로 하는 시점에 MCP 서버를 구축하세요.
핵심 포인트
- MCP는 도구와 클라이언트를 분리합니다 — 도구를 서버로서 한 번만 작성하면, 각 앱마다 재구현할 필요 없이 모든 MCP 클라이언트(Claude Desktop, Claude Code, 자체 에이전트 등)가 이를 사용할 수 있습니다.
- SDK는 속성 (attributes) 기반으로 작동합니다 — 클래스에는
[McpServerToolType], 메서드에는[McpServerTool]을 사용하며, JSON Schema는 메서드 시그니처(signature)를 기반으로 자동 생성됩니다. 수동으로 스키마를 작성하거나 인자를 직접 파싱할 필요가 없습니다. - 설명 (descriptions)은 여전히 중요합니다 — 메서드와 각 매개변수에 작성하는
[Description]은 Claude가 언제 도구를 호출할지, 그리고 무엇을 전달할지를 결정하는 기준이 됩니다. 이를 독스트링 (docstrings)처럼 작성하세요. - stdout을 주의하세요 — stdio 모드에서 프로토콜 자체가 곧 stdout입니다. 로그를 stderr로 보내지 않으면 디버그 출력으로 인해 스트림이 손상될 수 있습니다.
- 도구는 동일하게, 전송 방식(transport)만 변경 — 자식 프로세스를 통한 로컬 도구에는 stdio를, 공유 또는 원격 도구에는 HTTP (
ModelContextProtocol.AspNetCore)를 사용하세요. 도구의 코드는 변경되지 않습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기