.NET 10 파일 기반 앱 + SQLite + MCP — Azure Cloud Shell에서 원격 AI 도구 서버를 띄우기까지
MCP 서버를 로컬에서 실행하고 테스트 하는 핸즈온랩은 https://gist.github.com/rkttu/7d757e82da46adae3af16071a23a1f05 에서 확인하실 수 있습니다.
| 항목 | 내용 |
|---|---|
| 소요 시간 | 50분 |
| 난이도 | 초중급 |
| 대상 | C# 기초 문법을 아는 개발자 |
| 목표 | Azure VM에 MCP 서버를 배포하고, Microsoft Foundry에서 에이전트를 만들어 MCP 도구를 연결 |
| 준비물 | Azure 구독, 웹 브라우저 (Azure 포털 접속용) |
이 핸즈온랩을 완료하면 다음을 할 수 있습니다.
- .NET 10의 파일 기반 앱이 무엇인지 설명하고, 프로젝트 파일 없이 C# 파일 하나로 애플리케이션을 빌드할 수 있다.
- MCP(Model Context Protocol) 서버의 기본 구조를 이해하고, AI 클라이언트가 호출할 수 있는 도구(Tool)를 등록할 수 있다.
- Azure Cloud Shell을 사용하여 리눅스 VM을 생성하고, SSH·HTTP·HTTPS 포트를 개방할 수 있다.
- Docker Compose로 MCP 서버와 Caddy 리버스 프록시를 구성하여, Azure VM의 기본 도메인으로 자동 HTTPS를 활성화할 수 있다.
- Microsoft Foundry에서 모델을 배포하고, 에이전트를 생성하여 원격 MCP 서버를 도구로 연결할 수 있다.
이 랩에서 구축하는 전체 아키텍처는 다음과 같습니다.
┌──────────────┐
┌─────────────┐ HTTPS (443) ┌───────────────┐ HTTP (8080) │ MCP Server │
│ Microsoft │ ───────────────────→ │ Caddy │ ────────────────→ │ (.NET 10) │
│ Foundry │ │ (자동 TLS) │ └──────────────┘
│ Agent │ └───────────────┘ │
└─────────────┘ │ ┌──────────────┐
Azure Linux VM │ SQLite DB │
<dns-label>.<region> │ (Chinook) │
.cloudapp.azure.com └──────────────┘
모든 구성은 Azure 포털의 Cloud Shell (Bash) 과 Microsoft Foundry 포털에서 수행합니다.
대부분의 개발자는 "MCP 서버를 100줄로 만든다"는 문제를 보면 불가능하다고 판단합니다. .csproj 파일을 만들고, 프로젝트 구조를 잡고, NuGet 패키지를 구성하는 것만으로도 상당한 분량이 필요하다고 생각하기 때문입니다.
그러나 .NET 10의 파일 기반 앱(File-based App) 을 알고 있다면 이야기가 달라집니다. 파일 기반 앱은 다음과 같은 특성을 갖습니다.
- 프로젝트 파일이 필요 없습니다.
.csproj없이.cs파일 하나로 실행됩니다. .NET SDK가 소스 파일의 지시문을 기반으로 프로젝트 구성을 자동 생성합니다. - NuGet 패키지를 파일 내에서 선언합니다.
#:package지시문으로 의존성을 직접 지정합니다. - 실행 방법이 간결합니다.
dotnet run app.cs,dotnet app.cs, 또는 Unix에서는 shebang을 통해./app.cs로 직접 실행할 수 있습니다.
이 세 가지 특성 덕분에 보일러플레이트가 극적으로 줄어들고, 핵심 로직에만 집중할 수 있습니다.
파일 기반 앱은 #: 접두사가 붙는 지시문을 소스 파일 상단에 배치하여 빌드와 실행을 구성합니다. 이 랩에서 사용하는 지시문을 포함하여 주요 지시문은 다음과 같습니다.
| 지시문 | 역할 | 예시 |
|---|---|---|
#:package |
NuGet 패키지 참조 추가 | #:package Newtonsoft.Json@13.0.3 |
#:sdk |
사용할 SDK 지정 (기본값: Microsoft.NET.Sdk) |
#:sdk Microsoft.NET.Sdk.Web |
#:property |
MSBuild 속성 설정 | #:property PublishAot=false |
#:project |
다른 프로젝트 파일 참조 | #:project ../Shared/Shared.csproj |
MCP 서버는 두 가지 전송 방식을 지원합니다.
| 항목 | stdio | HTTP (Streamable HTTP) |
|---|---|---|
| 통신 방식 | 표준 입출력 (stdin/stdout) | HTTP 요청/응답 |
| 클라이언트 실행 | 클라이언트가 서버 프로세스를 직접 실행 | 독립적으로 실행 중인 서버에 네트워크로 연결 |
| 배포 | 로컬 전용, 클라이언트와 같은 머신 | 원격 서버, 클라우드, 컨테이너 배포 가능 |
| 다중 클라이언트 | 프로세스당 1개 클라이언트 | 여러 클라이언트가 동시에 접속 가능 |
| 필요 SDK | Microsoft.NET.Sdk |
Microsoft.NET.Sdk.Web |
| 패키지 | ModelContextProtocol |
ModelContextProtocol.AspNetCore |
| 적합한 용도 | 개발/테스트, 단일 사용자 | 프로덕션, 팀 공유, 서비스 배포 |
이 핸즈온랩에서는 HTTP 전송 방식을 사용하여 Azure VM에서 원격 접근 가능한 MCP 서버를 구현합니다.
- 브라우저에서 Azure 포털에 로그인합니다.
- 처음 사용하는 경우 스토리지 계정을 생성하라는 안내가 나오면 지시에 따라 생성합니다.
Cloud Shell이 열리면 az CLI가 이미 인증된 상태이므로 별도 로그인 없이 바로 명령을 실행할 수 있습니다.
📌 체크포인트: Cloud Shell 터미널에서
az account show --query name -o tsv가 사용 중인 구독 이름을 출력하면 준비 완료입니다.
Cloud Shell에서 다음 변수를 설정합니다. DNS_LABEL은 전 세계적으로 고유해야 하므로 본인만의 접미사를 붙이십시오.
RESOURCE_GROUP=rg-mcp-lab
LOCATION=koreacentral
VM_NAME=mcp-lab-vm
DNS_LABEL=mcp-lab-$RANDOM # Example: mcp-lab-28451참고:
LOCATION은 가까운 Azure 리전으로 변경할 수 있습니다. 예:eastus,japaneast,southeastasia등.
az group create --name $RESOURCE_GROUP --location $LOCATIONVM이 처음 부팅될 때 Docker Engine을 자동으로 설치하도록 cloud-init 스크립트를 파일로 먼저 작성합니다. Cloud Shell 상단의 편집기 아이콘({})을 클릭하거나 code cloud-init.sh 명령으로 편집기를 열고 다음 내용을 입력합니다.
cloud-init.sh:
#!/bin/bash
curl -fsSL https://get.docker.com | sh
usermod -aG docker azureuser파일을 저장한 뒤, 내용을 확인합니다.
cat cloud-init.shaz vm create \
--resource-group $RESOURCE_GROUP \
--name $VM_NAME \
--image Ubuntu2404 \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-address-dns-name $DNS_LABEL \
--custom-data @cloud-init.sh이 명령은 다음을 수행합니다.
| 옵션 | 설명 |
|---|---|
--image Ubuntu2404 |
Ubuntu 24.04 LTS 이미지 사용 |
--size Standard_B2s |
2 vCPU, 4 GiB RAM — 핸즈온랩에 충분한 크기 |
--generate-ssh-keys |
Cloud Shell에서 SSH 키 자동 생성 및 VM에 등록 |
--public-ip-address-dns-name |
VM의 공인 IP에 DNS 레이블 할당 → <DNS_LABEL>.<LOCATION>.cloudapp.azure.com |
--custom-data @cloud-init.sh |
파일을 참조하여 VM 최초 부팅 시 cloud-init으로 Docker Engine 자동 설치 |
az vm open-port \
--resource-group $RESOURCE_GROUP \
--name $VM_NAME \
--port 80,443 \
--priority 1010SSH(22번 포트)는 VM 생성 시 자동으로 개방됩니다. 이 명령으로 HTTP(80)와 HTTPS(443) 포트를 추가로 엽니다.
FQDN=$(az vm show \
--resource-group $RESOURCE_GROUP \
--name $VM_NAME \
--show-details \
--query fqdns \
--output tsv)
echo "VM Domain: $FQDN"출력 예시:
VM Domain: mcp-lab-28451.koreacentral.cloudapp.azure.com
이 도메인은 Azure가 자동으로 공인 IP에 연결해주므로, DNS 설정이 필요 없습니다. Caddy가 이 도메인으로 Let's Encrypt 인증서를 HTTP-01 챌린지로 즉시 발급받을 수 있습니다.
📌 체크포인트:
echo $FQDN이<dns-label>.<region>.cloudapp.azure.com형식의 도메인을 출력하면 성공입니다.
Cloud Shell에서 VM에 접속합니다.
ssh azureuser@$FQDN처음 접속 시 fingerprint 확인 메시지가 나오면 yes를 입력합니다.
cloud-init이 완료될 때까지 잠시 기다린 뒤 확인합니다.
cloud-init status --wait
docker --version
docker compose versionDocker version 2x.x.x와 Docker Compose version v2.x.x가 출력되면 정상입니다.
📌 체크포인트: VM에 SSH 접속이 성공하고,
docker --version이 정상 출력되면 준비 완료입니다.cloud-init이 아직 진행 중이라면cloud-init status --wait명령으로 완료를 기다릴 수 있습니다.
Docker가 정상 설치되었음을 확인했으면, Cloud Shell로 돌아와 프로젝트 파일을 작성합니다.
exitCloud Shell의 내장 편집기(code 명령)를 사용하여 프로젝트 파일을 작성합니다. 완성된 파일은 SCP로 VM에 한꺼번에 전송합니다. 이렇게 하면 셸에서 여러 줄의 텍스트를 직접 입력하다 발생하는 오탈자를 방지할 수 있습니다.
Cloud Shell에서 작업 디렉토리를 만듭니다.
mkdir ~/mcp-lab && cd ~/mcp-labCloud Shell 상단의 편집기 아이콘({}) 을 클릭하거나 code app.cs 명령으로 편집기를 열고, 다음 내용을 입력합니다.
app.cs:
#!/usr/bin/env dotnet
#:sdk Microsoft.NET.Sdk.Web
#:package Microsoft.Data.Sqlite@9.*
#:package ModelContextProtocol.AspNetCore@1.*
#:property PublishAot=false
using Microsoft.Data.Sqlite;
using ModelContextProtocol.Server;
using System.ComponentModel;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddMcpServer()
.WithHttpTransport()
.WithToolsFromAssembly();
var app = builder.Build();
app.MapMcp();
await app.RunAsync();
[McpServerToolType]
public static class ChinookTools
{
private const string DbPath = "Chinook_Sqlite.sqlite";
[McpServerTool, Description("아티스트를 이름으로 검색하고, 해당 아티스트의 앨범 목록을 함께 반환합니다.")]
public static string SearchArtists(
[Description("검색할 아티스트 이름 (부분 일치)")] string name)
{
using var conn = new SqliteConnection($"Data Source={DbPath}");
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = """
SELECT ar.ArtistId, ar.Name, al.Title
FROM Artist ar
LEFT JOIN Album al ON ar.ArtistId = al.ArtistId
WHERE ar.Name LIKE @name
ORDER BY ar.Name, al.Title
""";
cmd.Parameters.AddWithValue("@name", $"%{name}%");
using var reader = cmd.ExecuteReader();
var results = new System.Text.StringBuilder();
while (reader.Read())
{
var album = reader.IsDBNull(2) ? "(앨범 없음)" : reader.GetString(2);
results.AppendLine($"[{reader.GetInt32(0)}] {reader.GetString(1)} — {album}");
}
return results.Length > 0 ? results.ToString() : $"'{name}'에 해당하는 아티스트를 찾을 수 없습니다.";
}
[McpServerTool, Description("트랙을 이름으로 검색하고, 앨범명, 아티스트명, 장르, 재생 시간을 함께 반환합니다.")]
public static string SearchTracks(
[Description("검색할 트랙 이름 (부분 일치)")] string keyword)
{
using var conn = new SqliteConnection($"Data Source={DbPath}");
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = """
SELECT t.Name, al.Title, ar.Name, g.Name,
t.Milliseconds / 1000 / 60 || ':' ||
substr('0' || (t.Milliseconds / 1000 % 60), -2) AS Duration
FROM Track t
JOIN Album al ON t.AlbumId = al.AlbumId
JOIN Artist ar ON al.ArtistId = ar.ArtistId
JOIN Genre g ON t.GenreId = g.GenreId
WHERE t.Name LIKE @kw
ORDER BY ar.Name, al.Title
LIMIT 20
""";
cmd.Parameters.AddWithValue("@kw", $"%{keyword}%");
using var reader = cmd.ExecuteReader();
var results = new System.Text.StringBuilder();
while (reader.Read())
{
results.AppendLine(
$"{reader.GetString(0)} [{reader.GetString(4)}] — {reader.GetString(2)} / {reader.GetString(1)} ({reader.GetString(3)})");
}
return results.Length > 0 ? results.ToString() : $"'{keyword}'에 해당하는 트랙을 찾을 수 없습니다.";
}
[McpServerTool, Description("장르별 트랙 수와 총 재생 시간을 조회합니다. 특정 장르를 지정하면 해당 장르의 트랙 목록을 반환합니다.")]
public static string GetGenreStats(
[Description("특정 장르 이름 (생략하면 전체 장르 통계를 반환)")] string? genre = null)
{
using var conn = new SqliteConnection($"Data Source={DbPath}");
conn.Open();
using var cmd = conn.CreateCommand();
if (string.IsNullOrWhiteSpace(genre))
{
cmd.CommandText = """
SELECT g.Name, COUNT(*) AS TrackCount,
SUM(t.Milliseconds) / 1000 / 3600 || '시간 ' ||
(SUM(t.Milliseconds) / 1000 % 3600) / 60 || '분' AS TotalDuration
FROM Track t
JOIN Genre g ON t.GenreId = g.GenreId
GROUP BY g.Name
ORDER BY TrackCount DESC
""";
}
else
{
cmd.CommandText = """
SELECT t.Name, ar.Name, al.Title,
t.Milliseconds / 1000 / 60 || ':' ||
substr('0' || (t.Milliseconds / 1000 % 60), -2) AS Duration
FROM Track t
JOIN Genre g ON t.GenreId = g.GenreId
JOIN Album al ON t.AlbumId = al.AlbumId
JOIN Artist ar ON al.ArtistId = ar.ArtistId
WHERE g.Name = @genre
ORDER BY ar.Name, t.Name
LIMIT 30
""";
cmd.Parameters.AddWithValue("@genre", genre);
}
using var reader = cmd.ExecuteReader();
var results = new System.Text.StringBuilder();
if (string.IsNullOrWhiteSpace(genre))
{
while (reader.Read())
results.AppendLine($"{reader.GetString(0)}: {reader.GetInt32(1)}곡 ({reader.GetString(2)})");
}
else
{
while (reader.Read())
results.AppendLine($"{reader.GetString(0)} [{reader.GetString(3)}] — {reader.GetString(1)} / {reader.GetString(2)}");
}
return results.Length > 0 ? results.ToString() : $"'{genre}' 장르를 찾을 수 없습니다.";
}
}파일을 저장합니다 (Ctrl+S 또는 편집기 메뉴에서 저장).
이 코드의 핵심 요소를 살펴봅니다.
파일 상단 — 지시문:
#!/usr/bin/env dotnet
#:sdk Microsoft.NET.Sdk.Web
#:package Microsoft.Data.Sqlite@9.*
#:package ModelContextProtocol.AspNetCore@1.*
#:property PublishAot=false#:sdk Microsoft.NET.Sdk.Web: HTTP 전송을 위해 ASP.NET Core 웹 SDK를 사용합니다.#:package ModelContextProtocol.AspNetCore@1.*: HTTP(Streamable HTTP) 전송을 지원하는 MCP 패키지입니다.
호스트 구성:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddMcpServer()
.WithHttpTransport()
.WithToolsFromAssembly();
var app = builder.Build();
app.MapMcp();
await app.RunAsync();WebApplication.CreateBuilder: ASP.NET Core 웹 앱 빌더를 생성합니다.WithHttpTransport(): Streamable HTTP를 전송 계층으로 사용합니다.app.MapMcp(): MCP 엔드포인트를 루트 경로(/)에 매핑합니다.
도구 구현:
[McpServerToolType]이 붙은 ChinookTools 클래스에 세 가지 도구가 정의되어 있습니다.
| 도구 | 설명 |
|---|---|
SearchArtists(name) |
아티스트를 이름으로 검색하고 앨범 목록을 반환 |
SearchTracks(keyword) |
트랙을 이름으로 검색하고 상세 정보를 반환 |
GetGenreStats(genre?) |
장르별 통계 조회, 특정 장르의 트랙 목록 반환 |
각 도구는 [Description] 어트리뷰트로 AI가 도구의 용도와 매개변수를 이해할 수 있도록 설명을 제공합니다.
같은 방법으로 code Dockerfile 명령으로 편집기를 열고 다음 내용을 입력합니다.
Dockerfile:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY app.cs .
RUN dotnet publish app.cs -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
COPY Chinook_Sqlite.sqlite .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["./app"]멀티스테이지 빌드를 사용합니다.
- build 스테이지: .NET SDK 이미지에서
dotnet publish로 파일 기반 앱을 릴리스 바이너리로 컴파일합니다. - 런타임 스테이지: 가벼운 ASP.NET Core 런타임 이미지에 빌드 산출물과 SQLite 파일을 복사합니다. 컨테이너 내부에서 8080 포트로 수신합니다.
code Caddyfile 명령으로 편집기를 열고 다음 내용을 입력합니다.
Caddyfile:
{$MCP_DOMAIN} {
reverse_proxy mcp-server:8080
}
{$MCP_DOMAIN}은 환경 변수로 전달받는 도메인입니다. Docker Compose에서 설정합니다.- Caddy는 이 도메인에 대해 자동으로 Let's Encrypt TLS 인증서를 HTTP-01 챌린지로 발급하고, HTTPS를 활성화합니다.
reverse_proxy mcp-server:8080은 Docker Compose 네트워크 내에서 MCP 서버 컨테이너의 8080 포트로 프록시합니다.
code docker-compose.yml 명령으로 편집기를 열고 다음 내용을 입력합니다.
docker-compose.yml:
services:
mcp-server:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
expose:
- "8080"
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
environment:
- MCP_DOMAIN=${MCP_DOMAIN}
depends_on:
- mcp-server
volumes:
caddy_data:
caddy_config:| 서비스 | 역할 |
|---|---|
mcp-server |
MCP 서버 컨테이너. 내부 포트 8080으로만 노출 (외부 직접 접근 불가) |
caddy |
리버스 프록시 + 자동 HTTPS. 80/443 포트를 외부에 노출 |
expose는 Docker 네트워크 내부에서만 접근 가능하게 합니다 (ports와 달리 호스트에 바인딩하지 않음).MCP_DOMAIN환경 변수를 Caddy 컨테이너에 전달합니다..env파일 또는 셸 변수로 설정합니다.caddy_data볼륨에 TLS 인증서가 저장되므로, 컨테이너를 재시작해도 인증서가 유지됩니다.
Step 2에서 설정한 $FQDN 변수를 사용하여 .env 파일을 만듭니다. 이 값은 Caddy가 TLS 인증서를 발급받을 도메인으로 사용됩니다.
echo "MCP_DOMAIN=$FQDN" > .env
cat .env출력 예시:
MCP_DOMAIN=mcp-lab-28451.koreacentral.cloudapp.azure.com
모든 파일이 올바르게 생성되었는지 확인합니다.
ls -a ~/mcp-lab/app.cs, Dockerfile, Caddyfile, docker-compose.yml, .env 다섯 파일이 보여야 합니다.
📌 체크포인트:
ls -a ~/mcp-lab/에서 다섯 파일이 모두 존재하면 성공입니다.
Cloud Shell에서 작성한 파일을 VM으로 전송합니다.
scp -r ~/mcp-lab/ azureuser@$FQDN:~/
$FQDN은 Step 2에서 설정한 값입니다. Cloud Shell 세션이 끊어졌다면FQDN=<도메인>형식으로 다시 설정하십시오.
ssh azureuser@$FQDN전송된 파일을 확인합니다.
cd ~/mcp-lab && ls -a .app.cs, Dockerfile, Caddyfile, docker-compose.yml, .env 다섯 파일이 보여야 합니다.
MCP 서버가 질의할 Chinook 데이터베이스를 다운로드합니다. Chinook은 디지털 음원 매장을 모델링한 샘플 데이터베이스로, 아티스트, 앨범, 트랙, 장르, 고객, 인보이스 등 11개 테이블과 실제 iTunes 라이브러리 기반의 데이터를 포함합니다.
curl -L -o Chinook_Sqlite.sqlite \
https://github.com/lerocha/chinook-database/releases/download/v1.4.5/Chinook_Sqlite.sqlite파일이 다운로드되었는지 확인합니다.
ls -lh Chinook_Sqlite.sqlite📌 체크포인트:
Chinook_Sqlite.sqlite파일이 약 1MB 크기로 존재하면 성공입니다.
Cloud Shell에서 SCP로 전송한 .env 파일이 정상적으로 존재하는지 확인합니다.
cat .envMCP_DOMAIN=<도메인> 형식이 출력되어야 합니다.
docker compose up -d --build첫 실행 시 .NET SDK 이미지 다운로드와 빌드에 시간이 소요됩니다. 진행 상황을 확인합니다.
docker compose logs -f다음 두 가지를 확인합니다.
- MCP 서버 시작:
Now listening on: http://[::]:8080 - Caddy TLS 자동 발급:
certificate obtained successfully또는serving initial certificate
두 메시지가 모두 나타나면 Ctrl+C로 로그 추적을 종료합니다.
참고: Let's Encrypt HTTP-01 챌린지는 80번 포트로 들어오는 요청을 Caddy가 처리하여 도메인 소유권을 증명합니다. Azure NSG에서 80번 포트를 열었고, DNS가 Azure에 의해 자동 설정되었으므로, 추가 구성 없이 인증서가 즉시 발급됩니다.
docker compose psmcp-server와 caddy 두 컨테이너가 Up 상태여야 합니다.
📌 체크포인트:
docker compose ps에서 두 컨테이너가 정상 실행 중이면 성공입니다.
VM 내에서 또는 Cloud Shell에서 MCP 엔드포인트를 호출합니다.
mcp-lab-31550.koreacentral.cloudapp.azure.com부분을 실제 FQDN으로 교체하십시오.
curl -N -X POST https://mcp-lab-31550.koreacentral.cloudapp.azure.com/ \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {"name": "test", "version": "1.0"}
}
}'참고:
-N(--no-buffer) 옵션은 curl의 출력 버퍼링을 비활성화합니다. MCP Streamable HTTP 전송은text/event-stream(SSE) 형식으로 응답할 수 있는데, SSE 응답은 curl이 기본적으로 버퍼링하여 아무것도 출력되지 않는 것처럼 보일 수 있습니다.-N옵션을 추가하면 서버 응답이 즉시 터미널에 표시됩니다.
MCP 프로토콜의 initialize 응답이 SSE 이벤트 형식으로 반환되면 서버가 HTTPS로 정상 동작하는 것입니다. 응답 예시:
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"...","version":"..."}}}
curl -vI https://mcp-lab-31550.koreacentral.cloudapp.azure.com 2>&1 | grep -E 'issuer|subject|expire'issuer에 Let's Encrypt가 포함되어 있으면 자동 인증서 발급이 정상적으로 완료된 것입니다.
📌 체크포인트: MCP 엔드포인트가 HTTPS로 응답하고, TLS 인증서가 Let's Encrypt에서 발급되었음을 확인하면 성공입니다.
이제 Azure VM에 배포한 MCP 서버를 Microsoft Foundry의 에이전트에 연결합니다. Foundry Agent Service는 원격 MCP 서버를 도구로 등록할 수 있어, 포털의 플레이그라운드에서 바로 에이전트를 테스트할 수 있습니다.
- 브라우저에서 Microsoft Foundry 포털에 로그인합니다.
- 왼쪽 상단의 프로젝트 이름을 클릭하고 Create new project를 선택합니다.
- 프로젝트 이름(예:
mcp-lab-project)을 입력합니다. - Advanced options를 열어 다음을 설정합니다.
- Resource group:
rg-mcp-lab(Step 2에서 만든 리소스 그룹) - Location: VM과 같은 리전 (예:
koreacentral)
- Resource group:
- Create project를 클릭하고 프로젝트가 생성될 때까지 기다립니다.
참고: Foundry 프로젝트를
rg-mcp-lab에 만들면 나중에 리소스 그룹을 삭제할 때 Foundry 리소스도 함께 정리됩니다.
에이전트가 사용할 LLM 모델을 배포합니다.
- 프로젝트 페이지에서 상단 내비게이션의 Discover를 클릭합니다.
- Models를 선택합니다.
- gpt-5.2를 검색합니다.
- Deploy > Default settings를 클릭하여 프로젝트에 배포합니다.
- 배포 이름(예:
gpt-5.2)을 확인합니다.
배포가 완료되면 상태가 Succeeded로 표시됩니다.
참고:
gpt-5.2모델의 리전별 가용성은 다를 수 있습니다. 해당 리전에서 사용할 수 없는 경우gpt-4.1-mini등 다른 모델을 선택하십시오.
- 상단 내비게이션에서 Build를 클릭합니다.
- Create agent를 선택합니다.
- 에이전트 기본 설정을 구성합니다.
| 필드 | 값 |
|---|---|
| Agent name | chinook-agent |
| Model | gpt-5.2 (방금 배포한 모델) |
| Instructions | 아래 내용 참조 |
Instructions 필드에 다음 시스템 프롬프트를 입력합니다.
당신은 Chinook 음원 데이터베이스 전문가입니다.
사용자가 아티스트, 트랙, 장르에 대해 질문하면 MCP 도구를 사용하여 정확한 데이터를 조회한 뒤 한국어로 답변합니다.
규칙:
- 데이터를 추측하지 말고, 항상 도구를 호출하여 결과를 확인하십시오.
- 검색 결과가 없으면 "해당 데이터를 찾을 수 없습니다"라고 안내하십시오.
- 결과가 많으면 상위 10건을 요약하고 전체 건수를 알려주십시오.
에이전트에 이 핸즈온랩에서 만든 MCP 서버를 도구로 등록합니다.
-
Playground 탭에서 Tools 드롭다운을 펼치고 Add를 클릭합니다.
-
Custom 탭에서 Model Context Protocol (MCP) > Create를 선택합니다.
-
모든 도구 살펴보기 를 클릭한 후, 사용자 지정 탭을 클릭한 후, MCP(모델 컨텍스트 프로토콜) 항목을 선택하고, 만들기 버튼을 클릭합니다.
-
다음 정보를 입력합니다. | 필드 | 값 | 설명 | | ------ | ---- | ----- | | Name |
chinook-mcp| MCP 서버의 고유 식별자 | | Remote MCP Server endpoint |https://<FQDN>/| 예:https://mcp-lab-28451.koreacentral.cloudapp.azure.com/(<FQDN>부분을 Step 2에서 확인한 실제 도메인으로 교체하십시오.) | | Authentication |인증되지 않음| 이 핸즈온랩에서는 인증 없이 연결 | -
연결을 클릭합니다. 연결이 성공하면 Tools 목록에
chinook-mcp서버가 표시됩니다. -
Save를 클릭하여 에이전트 설정을 저장합니다.
연결이 완료되면 도구 목록에서 SearchArtists, SearchTracks, GetGenreStats 세 가지 도구가 표시되는 것을 확인합니다.
Playground의 채팅 창에서 다음 프롬프트를 입력하여 에이전트를 테스트합니다.
AC/DC의 앨범 목록을 보여주세요.
에이전트가 MCP 도구 호출을 요청하면 도구 이름과 인수를 확인한 뒤 Approve를 클릭합니다.
love라는 단어가 들어간 트랙을 찾아주세요.
전체 장르별 트랙 통계를 보여주고, Rock 장르의 트랙 상위 10개를 알려주세요.
에이전트가 GetGenreStats 도구를 두 번 호출(전체 통계 + Rock 장르)하여 결합된 답변을 생성하는 것을 확인합니다.
📌 체크포인트: Foundry 플레이그라운드에서 에이전트가 MCP 도구를 호출하고, Chinook 데이터베이스의 실제 데이터를 기반으로 답변하면 성공입니다. 또한, 도구의 호출을 승인할 것인지 묻는 질문이 나타날 수 있습니다. 이 경우 도구 호출을 승인해주어야 테스트가 가능합니다.
Foundry는 에이전트가 실행한 모든 단계를 추적(Trace) 으로 기록합니다. 에이전트가 MCP 도구를 실제로 호출했는지, 어떤 입력과 출력이 오갔는지를 확인할 수 있습니다.
추적 데이터를 저장하려면 Application Insights 리소스를 프로젝트에 연결해야 합니다.
- Foundry 프로젝트의 왼쪽 탐색에서 Agents를 클릭합니다.
- 상단의 Traces 탭을 선택합니다.
- 오른쪽의 Connect를 클릭합니다.
- 기존 Application Insights 리소스가 있으면 선택 후 Connect를 클릭합니다.
- 없으면 Create new를 선택하여 새로 생성합니다.
- 연결이 완료되면 확인 메시지가 표시됩니다.
참고: Foundry는 에이전트의 서버 측 추적을 별도 코드 변경 없이 자동으로 기록합니다. Application Insights만 연결되면 즉시 추적이 시작됩니다.
9-5에서 테스트한 대화가 끝난 뒤, 추적 결과를 확인합니다.
-
Traces 탭에서 최근 추적 목록이 표시됩니다. 방금 실행한 대화에 해당하는 항목을 클릭합니다.
-
추적 상세 보기에서 다음을 확인합니다. | 확인 항목 | 설명 | | ----------- | ------ | | 대화 이력 | 사용자 메시지와 에이전트 응답의 전체 흐름 | | 실행 단계 | 에이전트가 수행한 각 단계가 순서대로 표시 | | 도구 호출 |
SearchArtists,SearchTracks,GetGenreStats등 MCP 도구 호출 이름, 전달된 인수, 반환된 결과 | | 응답 토큰 | 각 응답에 사용된 토큰 수 | -
Conversation ID를 클릭하면 대화 전체의 입출력을 타임라인 형태로 확인할 수 있습니다.
예를 들어, "AC/DC의 앨범 목록을 보여주세요"라는 질의에 대해 추적에서 다음이 표시되어야 합니다.
- 도구 호출:
SearchArtists— 인수:{"name": "AC/DC"} - 도구 결과: AC/DC의 앨범 목록 (For Those About to Rock, Let There Be Rock 등)
- 에이전트 응답: 도구 결과를 기반으로 생성된 한국어 답변
📌 체크포인트: Traces 탭에서 에이전트의 MCP 도구 호출 이름, 인수, 결과가 모두 기록되어 있으면 추적 설정이 완료된 것입니다.
핸즈온랩이 끝나면 비용이 발생하지 않도록 모든 리소스를 삭제합니다.
- Microsoft Foundry 포털에서 프로젝트를 선택합니다.
- Build 탭에서
chinook-agent를 선택하고 Delete를 클릭합니다. - 상단 내비게이션에서 Discover > Models + endpoints로 이동합니다.
gpt-5.2배포를 선택하고 Delete를 클릭합니다.
<foundry-resource-name>은 Foundry 프로젝트 생성 시 자동으로 만들어진 Cognitive Services 리소스 이름입니다. Azure 포털의rg-mcp-lab리소스 그룹에서 확인할 수 있습니다.
Cloud Shell에서 실행합니다.
az group delete --name rg-mcp-lab --yes --no-wait이 명령은 리소스 그룹과 그 안의 모든 리소스(VM, 공인 IP, NSG, 디스크, Foundry 프로젝트 및 관련 리소스 등)를 한 번에 삭제합니다.
참고: Step 8에서 Foundry 프로젝트를
rg-mcp-lab리소스 그룹에 만들었으므로, 리소스 그룹 삭제 시 Foundry 관련 리소스도 함께 삭제됩니다. 별도 리소스 그룹에 만들었다면 해당 리소스 그룹도 별도로 삭제해야 합니다.
이 핸즈온랩에서는 .NET 10 파일 기반 앱 하나로 MCP 서버를 만들고, Azure VM에 배포하여 AI 에이전트의 도구로 연결하는 전체 흐름을 체험했습니다. 여기서 주목해야 할 점은 이 모든 과정이 프로젝트 파일 하나 없이, C# 파일 100줄로 완성되었다는 사실입니다.
.csproj 파일을 만들고, NuGet 패키지를 구성하고, 프로젝트 구조를 설계하는 기존의 의례적인 과정 없이도 — #:package 지시문 몇 줄이면 즉시 MCP 서버가 만들어집니다. 이것이 .NET 10 파일 기반 앱이 가져온 가장 큰 변화입니다.
그런데 이 핸즈온랩의 진짜 의미는 이 간결함이 레거시 시스템에도 그대로 적용된다는 데 있습니다.
많은 기업에는 수십 년간 운영되어 온 Oracle, SQL Server, DB2, AS/400 같은 레거시 데이터베이스가 있습니다. 이 데이터베이스들은 보안상 퍼블릭 인터넷에 노출할 수 없고, 사내 네트워크에서만 접근 가능합니다. 그래서 AI와 연결하려면 복잡한 아키텍처가 필요하다고 생각하기 쉽습니다.
하지만 실제로는 이렇게 할 수 있습니다:
- 레거시 DB가 있는 사내 네트워크에 MCP 서버를 배포합니다 — 이 핸즈온랩에서 만든 것처럼
.cs파일 하나와 Docker Compose로 충분합니다. - Tailscale, NetBird 같은 제로트러스트 네트워크 도구로 사내 MCP 서버와 Azure VM을 안전하게 연결합니다. VPN 장비를 설치하거나 방화벽 규칙을 변경할 필요 없이, 에이전트 설치만으로 전 구간 암호화된 사설 네트워크가 구성됩니다.
- Azure VM의 Caddy 리버스 프록시가 HTTPS 종단점을 제공하고, Microsoft Foundry 에이전트나 다른 MCP 클라이언트가 이 종단점을 통해 레거시 DB에 안전하게 접근합니다.
┌──────────────┐ ┌──────────────┐
│ Microsoft │ HTTPS (443) │ Azure VM │
│ Foundry │ ───────────────────→ │ Caddy + │
│ Agent │ │ MCP Server │
└──────────────┘ └──────┬───────┘
│
Tailscale / NetBird
(전 구간 암호화)
│
┌──────┴───────┐
│ 사내 네트워크 │
│ Legacy DB │
│ (Oracle, │
│ SQL Server │
│ 등) │
└──────────────┘
이 접근 방식의 핵심은 다음과 같습니다:
| 관점 | 이점 |
|---|---|
| 개발 비용 | .cs 파일 하나로 MCP 서버 완성 — 프로젝트 구조, 빌드 파이프라인 불필요 |
| 보안 | 레거시 DB를 퍼블릭 인터넷에 노출하지 않음 — Tailscale/NetBird로 전 구간 암호화 |
| 운영 | Docker Compose로 배포 — 기존 인프라 변경 최소화 |
| AI 활용 | Foundry 에이전트가 자연어로 레거시 DB를 질의 — SQL 지식 없이도 데이터 접근 가능 |
아무리 오래된 레거시 시스템이라도, MCP 서버를 하나 부착하면 AI 에이전트가 즉시 활용할 수 있는 도구가 됩니다. 이 핸즈온랩에서 Chinook SQLite 데이터베이스에 했던 것과 동일한 패턴 — SqliteConnection을 OracleConnection이나 SqlConnection으로 바꾸고, 쿼리를 실제 비즈니스 데이터에 맞게 수정하면 됩니다. 나머지는 MCP 프로토콜이 알아서 처리합니다.
| 기존 방식 | 파일 기반 앱 |
|---|---|
.csproj + Program.cs + NuGet 구성 |
.cs 파일 1개 |
dotnet new → dotnet add package → dotnet run |
dotnet run app.cs |
| 프로젝트 단위 빌드 | 파일 단위 실행 |
| 별도 publish 설정 필요 | dotnet publish app.cs로 바로 배포 |
| 구성 요소 | 역할 | 코드에서의 위치 |
|---|---|---|
| Web Host | 서버 수명 주기 + HTTP 파이프라인 관리 | WebApplication.CreateBuilder |
| Transport | 클라이언트와의 통신 방식 | WithHttpTransport() |
| Endpoint | HTTP 라우트 매핑 | app.MapMcp() |
| Tool Type | 도구 메서드를 포함하는 클래스 | [McpServerToolType] 어트리뷰트 |
| Tool | AI가 호출하는 기능 단위 | [McpServerTool] 어트리뷰트 |
| Description | AI가 도구의 용도를 이해하기 위한 설명 | [Description] 어트리뷰트 |
| 구성 요소 | 역할 |
|---|---|
| Foundry 프로젝트 | 모델, 에이전트, 연결을 관리하는 작업 공간 |
| 모델 배포 | 에이전트가 추론에 사용할 LLM (예: gpt-5.2) |
| 에이전트 (Agent) | 시스템 프롬프트 + 모델 + 도구를 결합한 AI 어시스턴트 |
| MCP 도구 연결 | 원격 MCP 서버의 URL을 에이전트에 등록하여 도구로 사용 |
| Playground | Foundry 포털에서 에이전트를 즉시 테스트할 수 있는 채팅 UI |
에이전트가 MCP 도구를 호출할 때는 Approve/Reject 워크플로를 통해 어떤 도구가 어떤 인수로 호출되는지 사전에 확인할 수 있습니다. 프로덕션에서는 require_approval 설정으로 자동 승인 정책을 구성할 수 있습니다.
| 단계 | 명령어 |
|---|---|
| 리소스 그룹 생성 | az group create --name rg-mcp-lab --location koreacentral |
| VM 생성 | az vm create --resource-group rg-mcp-lab --name mcp-lab-vm --image Ubuntu2404 ... |
| 포트 개방 | az vm open-port --resource-group rg-mcp-lab --name mcp-lab-vm --port 80,443 |
| FQDN 확인 | az vm show ... --query fqdns --output tsv |
| 서비스 시작 | docker compose up -d --build |
| Foundry 프로젝트 생성 | Foundry 포털 > Create new project |
| 모델 배포 | Foundry 포털 > Discover > Models > Deploy |
| 에이전트 + MCP 연결 | Foundry 포털 > Build > Create agent > Tools > MCP |
| 리소스 삭제 | az group delete --name rg-mcp-lab --yes --no-wait |
이 핸즈온랩에서는 파일 기반 앱의 간결함을 체험하는 데 집중했지만, 실무에서 더 복잡한 MCP 서버를 구축할 때는 공식 프로젝트 템플릿을 사용하는 것이 효율적입니다.
dotnet new install Microsoft.McpServer.ProjectTemplates
dotnet new mcpserver -n MyMcpServer이 템플릿은 stdio와 HTTP 전송 중 선택할 수 있고, Native AOT 및 Self-contained 배포 옵션을 제공하며, NuGet 배포를 위한 server.json 파일이 포함됩니다.
- .NET 10 파일 기반 앱 공식 문서
- 튜토리얼: 파일 기반 C# 프로그램 만들기
- .NET AI 및 MCP 시작하기
- 퀵스타트: C#으로 MCP 서버 만들기
- NuGet의 MCP 서버 패키지
- Model Context Protocol 공식 사이트
- MCP C# SDK (GitHub)
- ModelContextProtocol.AspNetCore NuGet 패키지
- Chinook 데이터베이스 (GitHub)
- Microsoft.Data.Sqlite 문서
- Caddy 공식 문서
- Docker Compose 공식 문서
- Azure VM 만들기 - Azure CLI
- Azure Cloud Shell 개요
- Microsoft Foundry 리소스 설정 퀵스타트
- Foundry 에이전트에 MCP 서버 연결
- Azure Functions MCP 서버를 Foundry 에이전트에 연결
- Foundry 에이전트 추적 설정
- Tailscale — 제로트러스트 네트워킹
- NetBird — 오픈소스 제로트러스트 네트워킹
이 핸즈온랩에서는 MCP 서버를 직접 만드는 것에 집중했습니다. 하지만 실무에서는 이미 만들어진 MCP 서버를 AI 코딩 에이전트에 연결하여 바이브 코딩(Vibe Coding)의 정확도를 높이는 것이 훨씬 빈번한 사용 사례입니다.
AI 코딩 에이전트는 학습 데이터에 기반하여 API를 추측하기 때문에, 실제로 존재하지 않는 메서드를 생성하거나 매개변수 시그니처를 틀리는 환각(hallucination)이 발생합니다. MCP 서버를 연결하면 에이전트가 실제 문서와 어셈블리 메타데이터를 직접 조회할 수 있으므로, 이러한 오류를 크게 줄일 수 있습니다.
여기서는 .NET 바이브 코딩에 특히 유용한 두 가지 MCP 서버를 소개합니다.
Microsoft Learn MCP 서버는 Microsoft 공식 문서를 AI 에이전트가 직접 검색하고 참조할 수 있게 해주는 원격(Remote) MCP 서버입니다. .NET, Azure, C#, ASP.NET Core, Entity Framework 등 Microsoft 기술 스택 전반에 대한 최신 공식 문서에 접근할 수 있습니다.
제공하는 주요 도구는 다음과 같습니다.
| 도구 | 용도 |
|---|---|
microsoft_docs_search |
Microsoft Learn 문서 키워드 검색 |
microsoft_docs_fetch |
특정 문서 페이지의 전체 내용 조회 |
microsoft_code_sample_search |
공식 코드 샘플 검색 |
HandMirrorMcp는 .NET 어셈블리와 NuGet 패키지를 직접 검사(inspect)하는 로컬(Local) MCP 서버입니다. "Mirror, mirror, on the wall — show me what this API really calls"라는 태그라인처럼, AI 에이전트가 코드를 작성하기 전에 실제 API의 타입, 메서드 시그니처, 네임스페이스를 확인할 수 있게 해줍니다.
제공하는 주요 도구 카테고리는 다음과 같습니다.
| 카테고리 | 주요 도구 | 용도 |
|---|---|---|
| 어셈블리 검사 | inspect_assembly, get_type_info, list_namespaces |
어셈블리의 공개 타입, 멤버, XML 문서 분석 |
| NuGet 패키지 탐색 | search_nuget_packages, inspect_nuget_package, inspect_nuget_package_type |
패키지 검색, 어셈블리 분석, 타입 상세 조회 |
| 네이티브 인터롭 | inspect_native_dependencies |
P/Invoke, COM 타입 의존성 분석 |
| 프로젝트 분석 | analyze_csproj, analyze_solution, explain_build_error |
프로젝트 파일 분석, 빌드 오류 진단 |
| 파일 기반 앱 분석 | analyze_file_based_app |
.NET 10 파일 기반 앱 분석 |
프로젝트 루트의 .vscode/mcp.json 파일에 다음을 추가합니다.
{
"servers": {
"microsoft-learn": {
"type": "http",
"url": "https://learn.microsoft.com/api/mcp"
},
"HandMirrorMcp": {
"type": "stdio",
"command": "dnx",
"args": ["HandMirrorMcp@0.1.1", "--yes"]
}
}
}설정 후 GitHub Copilot Chat에서 Agent 모드로 전환하고, 도구 선택(Select Tools) 아이콘에서 두 서버가 모두 표시되는지 확인합니다. VS Code Command Palette(Ctrl+Shift+P / Cmd+Shift+P)에서 MCP: Add Server를 통해 UI로 추가할 수도 있습니다.
참고:
dnx는 .NET 10 SDK에 포함된 명령어로, NuGet에 게시된 .NET 도구를 사전 설치 없이 바로 실행합니다.--yes플래그는 최초 실행 시 확인 프롬프트를 건너뜁니다.
프로젝트 루트의 .cursor/mcp.json 파일에 다음을 추가합니다.
{
"mcpServers": {
"microsoft-learn": {
"url": "https://learn.microsoft.com/api/mcp"
},
"HandMirrorMcp": {
"command": "dnx",
"args": ["HandMirrorMcp@0.1.1", "--yes"]
}
}
}Cursor Settings → MCP 탭에서 서버가 활성화되었는지 확인합니다.
claude_desktop_config.json 파일에 다음을 추가합니다.
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"HandMirrorMcp": {
"command": "dnx",
"args": ["HandMirrorMcp@0.1.1", "--yes"]
}
}
}참고: Claude Desktop은 현재 stdio 전송 방식의 로컬 MCP 서버를 지원합니다. Microsoft Learn MCP 서버는 원격(HTTP) 방식이므로 Claude Desktop에서는 직접 연결할 수 없지만, Claude Code에서는 플러그인 방식으로 사용할 수 있습니다.
Claude Code에서는 플러그인 방식으로 Microsoft Learn MCP 서버를 설치할 수 있습니다.
/plugin marketplace add microsoftdocs/mcp
/plugin install microsoft-docs@microsoft-docs-marketplaceHandMirrorMcp는 다음과 같이 추가합니다.
claude mcp add HandMirrorMcp -- dnx HandMirrorMcp@0.1.1 --yes두 MCP 서버가 연결된 상태에서, AI 에이전트에게 다음과 같이 요청할 수 있습니다.
"Microsoft.Data.Sqlite의 SqliteConnection 클래스가 제공하는 메서드를 확인하고, 트랜잭션을 사용한 배치 INSERT 코드를 작성해줘."
에이전트는 HandMirrorMcp의 inspect_nuget_package_type으로 실제 API를 확인한 뒤 코드를 생성합니다.
"CS1061 오류가 발생하는데, ModelContextProtocol 패키지의 실제 메서드 시그니처를 확인해서 수정해줘."
에이전트는 HandMirrorMcp의 explain_build_error와 inspect_nuget_package를 조합하여 오류 원인을 파악합니다.
".NET 10 파일 기반 앱에서 launch profile을 설정하는 방법을 찾아서 app.run.json 파일을 만들어줘."
에이전트는 Microsoft Learn MCP 서버의 microsoft_docs_search로 공식 문서를 조회한 뒤, 정확한 형식으로 파일을 생성합니다.