이 자습서에서는 Express 및 MCP TypeScript SDK를 사용하여 작업 관리 도구를 노출하는 MCP(모델 컨텍스트 프로토콜) 서버를 빌드합니다. 서버를 Azure Container Apps에 배포하고 VS Code의 GitHub Copilot 채팅에서 연결합니다.
이 자습서에서는 다음을 수행합니다.
- MCP 도구를 노출하는 Express 앱 만들기
- GitHub Copilot를 사용하여 로컬로 MCP 서버 테스트
- Azure Container Apps에 앱 컨테이너화 및 배포
- 배포된 MCP 서버에 GitHub Copilot 연결
필수 조건
- 활성 구독이 있는 Azure 계정. 체험 계정 만들기
- Azure CLI 버전 2.62.0 이상.
- Node.js 20 LTS 이상.
- GitHub Copilot 확장이 있는 Visual Studio Code.
- Docker Desktop (선택 사항 - 컨테이너를 로컬로 테스트하는 데만 필요).
앱 스캐폴드 만들기
이 섹션에서는 Express 및 MCP TypeScript SDK를 사용하여 새 Node.js 프로젝트를 만듭니다.
프로젝트 디렉터리를 만들고 초기화합니다.
mkdir tasks-mcp-server && cd tasks-mcp-server npm init -y종속성을 설치합니다.
npm install @modelcontextprotocol/sdk express zod npm install -D typescript @types/node @types/express tsx만들기
tsconfig.json:{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "declaration": true }, "include": ["src/**/*"] }이 구성은 Node.js 모듈 해석을 사용하여 ES2022를
dist/대상으로 하고, 컴파일된 파일을 출력하며, 엄격한 타입 검사를 사용하도록 설정합니다.ES 모듈을 사용하도록 업데이트
package.json하고 빌드 및 시작 스크립트를 추가합니다. 필드type및scripts추가하거나 교체하기:{ "type": "module", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsx watch src/index.ts" } }중요합니다
"type": "module"을 설정합니다. MCP 서버 코드는 ES 모듈에서만 지원되는 최상위 수준을await사용합니다.메모리 내 데이터 저장소를 만들
src/taskStore.ts.export interface TaskItem { id: number; title: string; description: string; isComplete: boolean; createdAt: string; } class TaskStore { private tasks: TaskItem[] = [ { id: 1, title: "Buy groceries", description: "Milk, eggs, bread", isComplete: false, createdAt: new Date().toISOString(), }, { id: 2, title: "Write docs", description: "Draft the MCP tutorial", isComplete: true, createdAt: new Date(Date.now() - 86400000).toISOString(), }, ]; private nextId = 3; getAll(): TaskItem[] { return [...this.tasks]; } getById(id: number): TaskItem | undefined { return this.tasks.find((t) => t.id === id); } create(title: string, description: string): TaskItem { const task: TaskItem = { id: this.nextId++, title, description, isComplete: false, createdAt: new Date().toISOString(), }; this.tasks.push(task); return task; } toggleComplete(id: number): TaskItem | undefined { const task = this.tasks.find((t) => t.id === id); if (!task) return undefined; task.isComplete = !task.isComplete; return task; } delete(id: number): boolean { const index = this.tasks.findIndex((t) => t.id === id); if (index < 0) return false; this.tasks.splice(index, 1); return true; } } export const store = new TaskStore();인터페이스는
TaskItem작업 데이터 셰이프를 정의합니다. 클래스는TaskStore샘플 데이터로 미리 채워진 메모리 내 배열을 관리하고 작업을 나열, 찾기, 만들기, 전환 및 삭제하는 메서드를 제공합니다. MCP 도구에서 사용하기 위해 모듈 수준 싱글톤을 내보냅니다.
MCP 도구 정의
다음으로, AI 클라이언트에 작업 저장소를 노출하는 도구 등록을 사용하여 MCP 서버를 정의합니다.
만들기
src/index.ts:import express, { Request, Response } from "express"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; import { store } from "./taskStore.js"; const app = express(); app.use(express.json()); // Health endpoint for Container Apps probes app.get("/health", (_req: Request, res: Response) => { res.json({ status: "healthy" }); }); // Create the MCP server const mcpServer = new McpServer({ name: "TasksMCP", version: "1.0.0", }); // Register tools mcpServer.tool("list_tasks", "List all tasks with their ID, title, description, and completion status.", {}, async () => { return { content: [{ type: "text", text: JSON.stringify(store.getAll(), null, 2) }], }; }); mcpServer.tool( "get_task", "Get a single task by its numeric ID.", { task_id: z.number().describe("The numeric ID of the task to retrieve") }, async ({ task_id }) => { const task = store.getById(task_id); return { content: [ { type: "text", text: task ? JSON.stringify(task, null, 2) : `Task with ID ${task_id} not found.`, }, ], }; } ); mcpServer.tool( "create_task", "Create a new task with the given title and description. Returns the created task.", { title: z.string().describe("A short title for the task"), description: z.string().describe("A detailed description of what the task involves"), }, async ({ title, description }) => { const task = store.create(title, description); return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }], }; } ); mcpServer.tool( "toggle_task_complete", "Toggle a task's completion status between complete and incomplete.", { task_id: z.number().describe("The numeric ID of the task to toggle") }, async ({ task_id }) => { const task = store.toggleComplete(task_id); const msg = task ? `Task ${task.id} is now ${task.isComplete ? "complete" : "incomplete"}.` : `Task with ID ${task_id} not found.`; return { content: [{ type: "text", text: msg }] }; } ); mcpServer.tool( "delete_task", "Delete a task by its numeric ID.", { task_id: z.number().describe("The numeric ID of the task to delete") }, async ({ task_id }) => { const deleted = store.delete(task_id); const msg = deleted ? `Task ${task_id} deleted.` : `Task with ID ${task_id} not found.`; return { content: [{ type: "text", text: msg }] }; } ); // Mount the MCP streamable HTTP transport const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); app.post("/mcp", async (req: Request, res: Response) => { await transport.handleRequest(req, res, req.body); }); app.get("/mcp", async (req: Request, res: Response) => { await transport.handleRequest(req, res); }); app.delete("/mcp", async (req: Request, res: Response) => { await transport.handleRequest(req, res); }); // Connect the transport to the MCP server await mcpServer.connect(transport); // Start the Express server const PORT = parseInt(process.env.PORT || "3000", 10); app.listen(PORT, () => { console.log(`MCP server running on http://localhost:${PORT}/mcp`); });주요 정보:
-
McpServerTypeScript SDK에서 도구 등록을 사용하여 MCP 서버를 정의합니다. -
StreamableHTTPServerTransport는 MCP 스트리밍 가능한 HTTP 프로토콜을 처리합니다.sessionIdGenerator: undefined을 설정하면 서버가 상태 없는 모드로 실행됩니다. - 도구는 Zod 스키마를 사용하여 설명이 포함된 입력 매개 변수를 정의합니다.
- Container Apps 상태 프로브에는 별도의
/health엔드포인트가 필요합니다.
-
로컬에서 MCP 서버 테스트
Azure에 배포하기 전에 MCP 서버가 로컬로 실행하고 GitHub Copilot에서 연결하여 작동하는지 확인합니다.
개발 서버를 시작합니다.
npx tsx src/index.tsVS Code를 열고 , 코필로트 채팅을 열고, 에이전트 모드를 선택합니다.
도구 단추를 선택한 다음, 추가 도구 추가...를 선택합니다.>MCP 서버를 추가합니다.
HTTP(HTTP 또는 Server-Sent 이벤트)를 선택합니다.
서버 URL을 입력합니다.
http://localhost:3000/mcp비고
로컬 개발 서버는 기본적으로 포트 3000으로 설정됩니다. 컨테이너화되면 Dockerfile은 환경 변수를 Container Apps 대상 포트와 일치하도록 8080으로 설정합니다
PORT.서버 ID를 입력합니다.
tasks-mcp작업 영역 설정을 선택합니다.
프롬프트를 사용하여 테스트: "모든 작업 표시"
Copilot가 도구 호출 확인을 요청할 때 계속 을 선택합니다.
코필로트가 메모리 내 저장소에서 작업 목록을 반환하는 것을 볼 수 있습니다.
팁 (조언)
"PR을 검토할 작업 만들기", "작업 1을 완료로 표시" 또는 "작업 2 삭제"와 같은 다른 프롬프트를 사용해 보세요.
애플리케이션 컨테이너화
Azure에 배포하기 전에 로컬에서 테스트할 수 있도록 애플리케이션을 Docker 컨테이너로 패키지합니다.
Dockerfile를 만듭니다.FROM node:20-slim AS build WORKDIR /app COPY package*.json . RUN npm ci COPY tsconfig.json . COPY src/ src/ RUN npm run build FROM node:20-slim WORKDIR /app COPY package*.json . RUN npm ci --omit=dev COPY --from=build /app/dist ./dist ENV PORT=8080 EXPOSE 8080 CMD ["node", "dist/index.js"]다단계 빌드는 첫 번째 단계에서 TypeScript를 컴파일한 다음 런타임 종속성 및 컴파일된 JavaScript 출력만 있는 프로덕션 이미지를 만듭니다.
PORT환경 변수는 Container Apps 대상 포트와 일치하도록 8080으로 설정됩니다.로컬로 확인:
docker build -t tasks-mcp-server . docker run -p 8080:8080 tasks-mcp-server확인:
curl http://localhost:8080/health
Azure Container Apps에 배포
애플리케이션을 컨테이너화한 후 Azure CLI를 사용하여 Azure Container Apps에 배포합니다. 이 az containerapp up 명령은 클라우드에서 컨테이너 이미지를 빌드하므로 이 단계를 위해 컴퓨터에 Docker가 필요하지 않습니다.
환경 변수 설정:
RESOURCE_GROUP="mcp-tutorial-rg" LOCATION="eastus" ENVIRONMENT_NAME="mcp-env" APP_NAME="tasks-mcp-server-node"리소스 그룹을 만듭니다.
az group create --name $RESOURCE_GROUP --location $LOCATIONContainer Apps 환경을 만듭니다.
az containerapp env create \ --name $ENVIRONMENT_NAME \ --resource-group $RESOURCE_GROUP \ --location $LOCATION컨테이너 앱을 배포합니다.
az containerapp up \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --environment $ENVIRONMENT_NAME \ --source . \ --ingress external \ --target-port 8080GitHub Copilot 요청을 허용하도록 CORS를 구성합니다.
az containerapp ingress cors enable \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --allowed-origins "*" \ --allowed-methods "GET,POST,DELETE,OPTIONS" \ --allowed-headers "*"비고
프로덕션의 경우 와일드카드
*원본을 신뢰할 수 있는 특정 원본으로 바꿉니다. 지침은 Container Apps의 보안 MCP 서버를 참조하세요.배포 확인:
APP_URL=$(az containerapp show \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --query "properties.configuration.ingress.fqdn" -o tsv) curl https://$APP_URL/health
배포된 서버에 GitHub Copilot 연결
이제 MCP 서버가 Azure에서 실행 중이므로 배포된 엔드포인트에 GitHub Copilot를 연결하도록 VS Code를 구성합니다.
프로젝트에서
.vscode/mcp.json을(를) 만들거나 업데이트합니다.{ "servers": { "tasks-mcp-server": { "type": "http", "url": "https://<your-app-fqdn>/mcp" } } }배포 출력에서
<your-app-fqdn>을 FQDN으로 교체하십시오.VS Code에서 에이전트 모드에서 코필로트 채팅을 엽니다.
서버가 자동으로 표시되지 않으면 도구 단추를 선택하고
tasks-mcp-server이 나열되어 있는지 확인합니다. 필요한 경우 시작을 선택합니다."내 모든 작업 나열"과 같은 프롬프트를 사용하여 테스트하여 배포된 MCP 서버가 응답하는지 확인합니다.
대화형 사용을 위한 크기 조정 구성
기본적으로 Azure Container Apps는 0개의 복제본으로 확장할 수 있습니다. 코필로트와 같은 대화형 클라이언트를 제공하는 MCP 서버의 경우 콜드 시작으로 인해 눈에 띄는 지연이 발생합니다. 하나 이상의 인스턴스를 계속 실행하도록 최소 복제본 수를 설정합니다.
az containerapp update \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--min-replicas 1
보안 고려 사항
이 자습서에서는 간단히 하기 위해 인증되지 않은 MCP 서버를 사용합니다. 프로덕션 환경에서 MCP 서버를 실행하기 전에 다음 권장 사항을 검토합니다. LLM(대규모 언어 모델)에서 구동되는 에이전트가 MCP 서버를 호출하는 경우 프롬프트 삽입 공격에 유의해야 합니다.
- 인증 및 권한 부여: Microsoft Entra ID를 사용하여 MCP 서버를 보호합니다. Container Apps에서 보안 MCP 서버를 참조하세요.
- 입력 유효성 검사: Zod 스키마는 형식 안전을 제공하지만 도구 매개 변수에 대한 비즈니스 규칙 유효성 검사를 추가합니다. 요청 수준 유효성 검사를 위해 zod-express-middleware 와 같은 라이브러리를 고려합니다.
- HTTPS: Azure Container Apps는 기본적으로 자동 TLS 인증서를 사용하여 HTTPS를 적용합니다.
- 최소 권한: 사용 사례에 필요한 도구만 노출합니다. 확인 없이 파괴적인 작업을 수행하는 도구를 사용하지 않습니다.
- CORS: 프로덕션 환경에서 허용된 원본을 신뢰할 수 있는 도메인으로 제한합니다.
- 로깅 및 모니터링: 감사를 위한 MCP 도구 호출을 기록합니다. Azure Monitor 및 Log Analytics를 사용합니다.
자원을 정리하세요
이 애플리케이션을 계속 사용하지 않으려면 리소스 그룹을 삭제하여 이 자습서에서 만든 모든 리소스를 제거합니다.
az group delete --resource-group $RESOURCE_GROUP --yes --no-wait