첫 번째 MCP 앱 만들기

발행: (2025년 12월 13일 오전 08:05 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

TL;DR

MCP 앱은 대화형 에이전트와 기타 MCP 클라이언트에 인터랙티브 UI를 제공합니다. 이 튜토리얼에서는 간단하지만 강력한 앱(소스 코드 here)을 만드는 방법을 보여주며, 이는 더 큰 프로젝트의 템플릿으로 사용할 수 있습니다. 이 앱은 공항에 도착한 최신 항공편을 표시합니다.

앱은 다음 세 단계로 구축합니다:

  1. MCP 서버 만들기
  2. 도구 등록하기
  3. 리소스를 등록하고 도구와 연결하기

Step 1 – 프로젝트 초기화

npm init --yes && npm pkg set type="module"
tsc --init

.gitignore 파일을 생성합니다:

echo -e "node_modules\ndist" > .gitignore

필요한 의존성을 설치합니다:

npm i @modelcontextprotocol/sdk @modelcontextprotocol/ext-apps express zod \
    && npm i -D @types/express nodemon

Step 2 – MCP 서버 만들기

server.ts 파일을 생성합니다:

// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { registerGetFlightsTool } from "./tools/get-flights.js";
import { registerFlightCardResource } from "./resources/flight-card/flight-card.js";

const server = new McpServer({
  name: "My First MCP App",
  version: "0.0.1",
});

// Register tools and resources.
registerGetFlightsTool(server);
registerFlightCardResource(server);

// Set up Express server to handle MCP requests.
const app = express();
app.use(express.json());

app.use("/mcp", async (req, res, next) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
    enableJsonResponse: true,
  });

  res.on("close", () => {
    transport.close();
  });

  await server.connect(transport);
  await transport.handleRequest(req, res, req.body).catch(next);
});

// Start the server.
app.listen(3000, () => {
  console.log("MCP server listening on http://localhost:3000/mcp");
});

nodemon을 이용한 개발 워크플로우

nodemon.json 파일을 생성합니다:

{
  "watch": ["."],
  "ext": "ts,html,css",
  "ignore": ["dist", "node_modules"],
  "exec": "tsc && node dist/server.js"
}

package.jsondev 스크립트를 추가합니다:

{
  "scripts": {
    "dev": "nodemon"
  }
}

컴파일된 파일이 dist 폴더에 들어가도록 tsconfig.json에 다음 라인을 주석 해제(또는 추가)합니다:

{
  "compilerOptions": {
    "outDir": "./dist"
  }
}

서버를 실행합니다:

npm run dev

MCP Jam으로 서버 테스트하기

npx @mcpjam/inspector@latest

URL http://localhost:3000/mcp 로 서버를 추가합니다. 연결이 성공해야 합니다.

Step 3 – Get Flights 도구 만들기

tools/get-flights.ts 파일을 생성합니다:

// tools/get-flights.ts
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import z from "zod";
import { flightCardResourceUri } from "../resources/flight-card/flight-card.js";

export function registerGetFlightsTool(server: McpServer) {
  server.registerTool(
    "get-flights",
    {
      description: "Retrieves flight arrivals for a given airport code",
      inputSchema: {
        code: z.string().describe("The ICAO airport code, e.g. 'KJFK'"),
      },
    },
    async (input: { code: string }) => {
      // Mock flight data – replace with a real API call in production.
      const mockFlights = [
        { flightNumber: "AA100", airline: "American Airlines" },
        { flightNumber: "DL200", airline: "Delta Airlines" },
        { flightNumber: "UA300", airline: "United Airlines" },
      ];

      return {
        content: [{ type: "text", text: JSON.stringify(mockFlights, null, 2) }],
        structuredContent: { flights: mockFlights },
      };
    }
  );
}

도구는 server.ts(Step 2)에서 이미 등록되었습니다. 서버를 재시작하면 MCP‑호환 채팅 플레이그라운드 어디서든 이 도구를 호출할 수 있습니다.

Step 4 – Vite와 Flight‑Card 리소스로 UI 추가하기

추가 의존성 설치

npm i vite vite-plugin-singlefile glob

Vite 설정

vite.config.js 파일을 생성합니다:

// vite.config.js
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";

export default defineConfig({
  plugins: [viteSingleFile()],
  build: {
    outDir: "dist",
    emptyOutDir: true,
    rollupOptions: {
      input: {
        "flight-card": "resources/flight-card/mcp-app/flight-card-mcp-app.html",
        // Add more HTML resources here as needed
      },
    },
  },
});

UI를 빌드한 뒤 서버를 시작하도록 nodemon.json을 업데이트합니다:

{
  "watch": ["."],
  "ext": "ts,html,css",
  "ignore": ["dist", "node_modules"],
  "exec": "vite build && tsc && node dist/server.js"
}

Flight‑Card 리소스 구현

resources/flight-card/flight-card.ts 파일을 생성합니다:

// resources/flight-card/flight-card.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import fs from "node:fs/promises";
import path from "node:path";

export const flightCardResourceUri = "ui://flight-card.html";

export function registerFlightCardResource(server: McpServer) {
  server.registerResource(
    flightCardResourceUri,
    flightCardResourceUri,
    {},
    async () => {
      const html = await fs.readFile(
        path.join(import.meta.dirname, "mcp-app/flight-card-mcp-app.html"),
        "utf-8"
      );

      return {
        contents: [
          {
            uri: flightCardResourceUri,
            mimeType: "text/html;profile=mcp-app",
            text: html,
          },
        ],
      };
    }
  );
}

리소스는 server.ts(Step 2)에서 이미 등록되었습니다.

Flight‑Card HTML 템플릿

resources/flight-card/mcp-app/flight-card-mcp-app.html 파일을 생성합니다:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Flight Cards</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      background: #f0f2f5;
      padding: 40px;
    }
    .container {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
      gap: 1rem;
    }
    .card {
      background: #fff;
      border-radius: 8px;
      padding: 1rem;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }
    .flight-number {
      font-weight: bold;
      font-size: 1.2rem;
    }
    .airline {
      color: #555;
    }
  </style>
</head>
<body>
  <h1>Flight Cards</h1>
  <div id="cards" class="container"></div>

  <script>
    // Placeholder script – replace with real rendering logic.
    const mockFlights = [
      { flightNumber: "AA100", airline: "American Airlines" },
      { flightNumber: "DL200", airline: "Delta Airlines" },
      { flightNumber: "UA300", airline: "United Airlines" },
    ];

    const container = document.getElementById("cards");
    mockFlights.forEach((f) => {
      const card = document.createElement("div");
      card.className = "card";
      card.innerHTML = `
        <div class="flight-number">${f.flightNumber}</div>
        <div class="airline">${f.airline}</div>
      `;
      container?.appendChild(card);
    });
  </script>
</body>
</html>

(원한다면 인라인 스크립트를 별도의 flight-card-mcp-app.ts 모듈로 교체할 수 있습니다.)

최종 점검

  1. 개발 서버를 실행합니다:

    npm run dev
  2. MCP Jam(npx @mcpjam/inspector@latest)을 열고 http://localhost:3000/mcp에 연결합니다.

  3. get‑flights 도구를 공항 코드(예: KJFK)와 함께 호출합니다.

  4. 도구가 모의 항공편 데이터를 반환하고, flight‑card 리소스에 정의된 UI가 이를 인터랙티브 카드 형태로 렌더링합니다.

축하합니다! 백엔드 도구와 프론트엔드 UI를 갖춘 기능적인 MCP 앱을 만들었습니다. 이 프로젝트를 기반으로 더 복잡한 통합을 진행해 보세요.

Back to Blog

관련 글

더 보기 »

실험적인 Hono auth npm 패키지

제가 만들고 있는 것은 개발자들이 일반적인 보일러플레이트인 login, register, JWT, email verification 등을 작성하지 않고도 앱에 바로 넣을 수 있는 auth 패키지입니다.