서론
ASP.NET Core로 MCP 서버를 구축해 보고 싶어서, TODO 리스트를 조작하는 최소 구성의 샘플을 만들어 보았습니다.
이번 샘플은 다음과 같습니다.
- REST API로서 TODO의 등록, 갱신, 삭제, 목록 조회를 공개한다
- 동일한 비즈니스 로직을 MCP 도구 (Tool)로도 공개한다
- ASP.NET Core의 Minimal API와
ModelContextProtocol.AspNetCore를 사용한다
MCP 서버 단독으로 만드는 것뿐만 아니라, Web API로서도 그대로 확인할 수 있도록 구성했습니다.
환경 정보
이번에 확인한 주요 환경 정보는 다음과 같습니다.
| 항목 | 버전 |
|---|
| Target Framework | .NET 10 (net10.0) |
| MCP C# SDK | ModelContextProtocol.AspNetCore 1.3.0 |
| MCP C# SDK(Core) | ModelContextProtocol 1.3.0 |
이번에 만드는 구성
하나의 ASP.NET Core 애플리케이션 안에서 일반적인 HTTP API와 MCP 엔드포인트 (Endpoint)를 모두 공개합니다.
- REST API:
/api/todos
- MCP 엔드포인트 (Endpoint):
/mcp
- TODO 데이터: 인메모리 (In-memory) 유지
REST와 MCP가 각각 별도의 구현을 갖는 것이 아니라, TodoStore에 처리를 집약하여 양쪽 모두에서 호출하는 형태로 만들었습니다.
MCP 전용의 별도 프로세스로 분리하는 구성도 가능하지만, 우선은 ASP.NET Core 웹 애플리케이션에 그대로 얹어버리는 형태가 동작 확인이나 디버깅이 훨씬 수월했습니다.
ASP.NET Core에 MCP 서버를 통합하기
먼저 AddMcpServer()와 WithHttpTransport()를 등록하고, 마지막에 MapMcp("/mcp")를 설정합니다.
using McpTodoServerSample;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<TodoStore>();
...
여기서 포인트가 되는 것은 다음 두 가지입니다.
WithHttpTransport()로 Streamable HTTP transport를 활성화한다
WithTools<TodoMcpTools>()로 공개할 MCP 도구 (Tool)를 등록한다
이렇게 하면 TodoMcpTools 내의 McpServerTool 속성 (Attribute)이 붙은 메서드가 list_tools / call_tool의 대상이 됩니다.
TODO 로직은 공유 서비스로 모은다
REST API와 MCP 도구 양쪽에서 동일한 로직을 사용하고 싶으므로, TODO의 상태는 TodoStore에 모읍니다.
public sealed class TodoStore
{
private readonly Lock _lock = new();
...
}
이번 샘플에서는 다음과 같이 구현했습니다.
- 영속화 (Persistence)는 사용하지 않고 인메모리 (In-memory)로만 완결한다
Lock과 List<T>를 사용하여 최소한의 배타 제어 (Exclusive control)를 넣는다
- 유효성 검사 (Validation)는
TodoStore 측에서 공통화한다
이렇게 하면 REST와 MCP 중 어느 쪽에서 호출하더라도 동일한 등록·갱신 규칙이 적용됩니다.
TODO 조작을 MCP 도구로 공개하기
MCP 도구 (Tool)는 TodoMcpTools에 모아두었습니다.
using System.ComponentModel;
using ModelContextProtocol;
using ModelContextProtocol.Server;
...
※ 일부만 발췌했습니다.
TodoStore store가 그대로 인자 (Argument)로 들어가는 이유는, MCP SDK 측에서 DI 컨테이너 (DI Container)로부터 서비스를 해결 (Resolve)해 주기 때문입니다.
이번에는 4개의 도구 (Tool)를 공개하고 있습니다.
| 도구 이름 | 용도 |
|---|
list_todos | TODO 목록을 반환한다 |
create_todo | TODO를 추가한다 |
update_todo | TODO를 갱신한다 |
delete_todo | TODO를 삭제한다 |
Description은 매우 중요하다
이곳은 이번에 직접 구동해보며 실감한 포인트입니다.
MCP 도구의 Description (설명)
은 단순한 설명문이 아니라, 모델이 "어떤 도구를 호출해야 하는가"를 판단하기 위한 중요한 단서가 됩니다. 특히 create_todo와 같은 쓰기 계열 도구는 설명이 너무 짧으면 모델이 용도를 오해하거나, 목록 조회만 하고 멈추거나, 다른 도구를 선택하는 등 동작이 불안정했습니다.
여러 가지 시도를 해본 결과, 각 도구의 Description (설명)에 다음 정보를 명시하고 있습니다.
- 해당 도구가 무엇 전용인지
- 어떤 요청이 있을 때 사용해야 하는지
- 반대로 무엇에는 사용하지 않는지
- 실행 전에
list_todos를 호출해야 하는 상황인지
예를 들어 create_todo에는 "TODO를 1건 신규 등록하는 전용 도구", "TODO를 추가하거나, 새로운 태스크를 등록하거나, 할 일을 작성하는 등의 요청 시에는 반드시 사용", "기존 TODO의 변경이나 삭제에는 사용하지 않음"이라고 적어두었습니다.
이 정도 수준까지 상세히 작성해두면 도구 선택의 정밀도가 상당히 높아집니다.
그렇다고 해도 자유도도 필요하므로, 실무에서는 도구의 Description (설명)뿐만 아니라 Skill (스킬)과 함께 사용법을 에이전트에게 설명하는 방식의 운용이 좋을 것 같습니다.
참고로, 검증되지 않은(wild) MCP 서버에서도 시도해 보았습니다만, 검색 계열 이외의 도구는 그대로 두면 잘 작동하지 않는 경우가 많았습니다. (MCP는 AI에게 있어 USB-C와 같다고들 하지만, Skill (스킬)과 같은 드라이버에 해당하는 개념도 필수적이라는 느낌이 듭니다. 꽂기만 한다고 해서 바로 잘 작동하지는 않습니다.)
REST API로도 동시에 공개하기
동일한 TodoStore를 사용하여 일반적인 HTTP API도 제공합니다.
RouteGroupBuilder todos = app.MapGroup("/api/todos");
todos.MapGet("/", (TodoStore store) => Results.Ok(store.List()));
todos.MapPost("/", (CreateTodoRequest request, TodoStore store) =>
...
이렇게 하면 HTTP 클라이언트에서는 일반적인 Web API로 사용할 수 있고, MCP 클라이언트에서는 도구 집합(toolset)으로 사용할 수 있습니다.
실행 방법
실행 명령어는 다음과 같습니다.
dotnet run --project .\McpTodoServerSample\ --urls http://127.0.0.1:5088
실행 후 루트(root) 경로에 접속하면 공개 대상의 개요를 반환합니다.
{
"service": "todo-mcp-server",
"restApi": "/api/todos",
...
MCP 클라이언트에서 사용하려면
이 샘플은 http://127.0.0.1:5088/mcp에 MCP endpoint (엔드포인트)를 공개하므로, MCP 클라이언트 측에서 이 URL을 등록하면 사용할 수 있습니다.
GitHub Copilot에서 사용하는 경우
VS Code의 Copilot Chat에서 사용하는 경우, GitHub Docs에 나와 있는 대로 .vscode/mcp.json에 서버를 정의할 수 있습니다. 이번 서버라면 예를 들어 다음과 같습니다.
{
"servers": {
"todoServer": {
...
설정 후에는 Agent mode (에이전트 모드)로 변경하여 서버를 활성화하면 사용할 수 있습니다.
REST API 확인
PowerShell에서 확인하려면 다음과 같이 호출할 수 있습니다.
$created = Invoke-RestMethod -Uri 'http://127.0.0.1:5088/api/todos' `
-Method Post `
-ContentType 'application/json' `
...
이번 확인 과정에서는 아래와 같이 등록 → 목록 조회 → 갱신 → 삭제까지 모두 통과했습니다.
{
"created": {
"id": 1,
...
MCP 공개 도구 확인
ModelContextProtocol.Client를 사용한 간이 프로브(probe)로 /mcp에 접속하여 list_tools를 호출하면 아래의 4개가 반환되었습니다.
create_todo|TODO를 1건 신규 등록하는 전용 도구입니다. 사용자가 "TODO를 추가해줘", "새로운 태스크를 등록해줘", "할 일을 만들어줘"와 같이 요청한 경우에는 반드시 이 도구를 사용하세요. 기존 TODO의 변경이나 삭제에는 사용하지 않습니다.
delete_todo|기존의 TODO를 1건 삭제하는 전용 도구입니다. 사용자가 "삭제해줘", "불필요하니 지워줘", "이 태스크를 취소해줘"라고 명시한 경우에만 사용합니다. 신규 생성이나 업데이트에는 사용하지 않습니다. 통상적으로 사전에 list_todos로 대상 ID를 확인한 후 사용합니다.
list_todos|TODO 목록을 가져오는 전용 도구입니다. 기존의 TODO를 확인하고 싶을 때, 업데이트나 삭제 전에 대상 ID를 조사하고 싶을 때, 현재 등록 상황을 보고 싶을 때 사용합니다. 신규 생성, 업데이트, 삭제는 할 수 없습니다.
...
ASP.NET Core 앱이 MCP 서버로서 도구(tool)를 올바르게 공개하고 있는지와
각 도구의 용도가 클라이언트에게 전달되는 형태로 공개되고 있음을 확인할 수 있었습니다.
요약
이번에는 ASP.NET Core로 간이 MCP 서버를 만들고, TODO 리스트 조작을 REST API와 MCP 도구 양쪽 모두로 공개했습니다.
확인할 수 있었던 포인트는 다음과 같습니다.
AddMcpServer(), WithHttpTransport(), MapMcp("/mcp")를 통해 ASP.NET Core에 MCP 엔드포인트(endpoint)를 추가할 수 있음
WithTools<T>()를 통해 일반적인 C# 메서드를 MCP 도구로 공개할 수 있음
- REST API와 MCP 도구에서 동일한
TodoStore를 공유하면 구현을 이중화하지 않아도 됨
list_tools를 통해 create_todo / delete_todo / list_todos / update_todo를 가져올 수 있음
- 도구의
Description은 모델의 도구 선택 정확도에 크게 영향을 미치므로, 용도, 사용하는 상황, 사용하지 않는 상황까지 작성하는 것이 좋음
실운영을 목표로 한다면 인메모리(in-memory) 구현을 SQLite나 Cosmos DB로 교체하거나, 인증(authentication) 및 인가(authorization) 기능을 추가하는 형태가 될 것입니다.
소스
Program.cs
using McpTodoServerSample;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<TodoStore>();
...
using System.ComponentModel;
using ModelContextProtocol;
using ModelContextProtocol.Server;
...
TodoStore.cs
namespace McpTodoServerSample;
public sealed class TodoStore
{
...
Discussion