MCP 서버 직접 만들기 — Claude를 내 도구·DB에 연결하는 가장 깔끔한 길

2026-05-15꿀팁·노하우

MCP 서버 직접 만드는 가이드. Model Context Protocol 개념·TypeScript/Python SDK 최소 서버·Tools Resources Prompts 차이·stdio Streamable HTTP 전송·Claude Desktop 등록·실전 가드레일까지.

지난 글에서 MCP(Model Context Protocol) 가 Claude를 Gmail·Notion·GitHub 같은 외부 도구에 연결하는 "AI용 USB-C" 라고 짧게 짚었습니다. 공식 마켓플레이스에 있는 MCP 서버만 써도 충분히 강력한데, 이걸 직접 만들면 회사 내부 DB·사내 API·로컬 파일까지 Claude가 자연어로 조작할 수 있게 됩니다.

오늘은 MCP 서버를 직접 만드는 법을 한 번에 정리합니다 — 개념·TypeScript/Python SDK·최소 동작 코드·Claude Desktop/Code 등록·실전 가드레일까지. 작성 시점(2026-05-15) 기준 공식 docs와 주요 튜토리얼 요약이에요.

1. MCP 한 줄 정리 — 왜 표준이 되었나

MCP — Anthropic이 2024년 11월에 공개한 JSON-RPC over stdio/HTTP 프로토콜. Claude(또는 다른 LLM)와 외부 도구 사이를 표준 인터페이스 로 연결합니다. 2025년 OpenAI·Google·Cursor·Cline까지 채택하면서 "AI 통합의 USB-C" 자리가 거의 확정됐어요.

핵심 아이디어 — 모델 쪽 코드와 도구 쪽 코드를 분리하는 것. 예전엔 Claude에 새 도구를 붙이려면 Anthropic SDK를 쓰는 모든 앱이 그 도구를 따로 구현해야 했어요. MCP가 등장한 뒤로는 MCP 서버 하나 만들어 두면 Claude Desktop·Claude Code·Cursor·Codex·Custom 앱이 모두 같은 서버를 공유합니다.

MCP의 세 컴포넌트

컴포넌트역할누가 정함
ClientClaude Desktop·Code, Agent SDK, 사용자 앱사용자가 어느 LLM 도구를 쓰느냐
Transport통신 계층 (stdio 또는 Streamable HTTP)로컬이면 stdio, 원격이면 HTTP
Server도구·자원·프롬프트 노출여기를 우리가 만든다

오늘 글의 주제는 세 번째 Server 입니다.

2. 서버가 노출하는 3가지 — Tools · Resources · Prompts

MCP 서버는 클라이언트에 세 종류의 능력(capability) 을 노출합니다. 이 셋의 차이를 처음 한 번 정확히 잡으면 설계 실수를 99% 막을 수 있어요.

항목본질누가 호출?예시
Tools액션 — 실행되는 함수모델이 결정send_email, run_migration, query_revenue
Resources읽기 전용 데이터 — URI로 참조호스트(앱)가 결정postgres://customers/123, file:///docs/spec.md
Prompts재사용 가능 템플릿사용자가 명시적으로 호출/review-code, /summarize-pr

가장 흔한 실수 — 도구를 자원처럼 만들거나 자원을 도구처럼 만드는 것. "고객 정보 가져오기" 가 자원이냐 도구냐? 사용자가 "고객 정보 화면에 띄워줘" 라고 명시적으로 부르면 자원, 모델이 "이 분석을 위해 고객 정보가 필요하군" 하며 알아서 가져오면 도구예요. 같은 데이터라도 누가 부르느냐 로 갈립니다.

3. 두 가지 전송 방식 — stdio vs Streamable HTTP

서버를 Claude에 어떻게 연결 할지가 다음 결정. 두 가지 선택지가 있습니다.

3-1. stdio — 로컬 자식 프로세스

  • Claude Desktop·Code가 서버를 자식 프로세스 로 띄움
  • 표준 입출력(stdin·stdout)으로 JSON-RPC 메시지 주고받음
  • 네트워크 X, 포트 X, 인증 X — 가장 단순
  • 본인 노트북 안 자원에 접근할 때 99% 이쪽

3-2. Streamable HTTP — 원격 서버

  • 별도 HTTP 서버를 띄워 클라이언트가 URL로 접속
  • 인증(OAuth)·다중 사용자·클라우드 호스팅 가능
  • 참고: 2025-03 SSE 폐기. 새 프로젝트는 Streamable HTTP 로 시작하세요
  • 회사 동료가 같이 쓰는 사내 도구·SaaS 통합용

한 줄 결정 룰:

  • 본인 노트북 안에서만 쓰는 도구 → stdio
  • 팀·외부 사용자가 함께 쓰는 서비스 → Streamable HTTP

아래 코드 예시는 모두 stdio 기준입니다. HTTP는 같은 SDK에서 transport만 갈아끼면 됩니다.

4. TypeScript로 최소 서버 만들기

가장 인기 있는 SDK는 공식 TypeScript SDK (@modelcontextprotocol/sdk). npm 한 번 깔면 끝.

4-1. 프로젝트 초기화

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init

package.json"type": "module" 추가해 ESM 사용.

4-2. 날씨 도구 하나짜리 서버

src/server.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "weather-server",
  version: "1.0.0",
});

server.tool(
  "get_weather",
  "Get current weather for a city",
  {
    city: z.string().describe("City name, e.g. Seoul"),
    units: z.enum(["metric", "imperial"]).optional().default("metric"),
  },
  async ({ city, units }) => {
    const apiKey = process.env.OPENWEATHER_API_KEY!;
    const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${units}&appid=${apiKey}`;
    const res = await fetch(url);
    const data = await res.json();

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({
            city: data.name,
            temp: data.main.temp,
            condition: data.weather[0].description,
          }),
        },
      ],
    };
  },
);

// stdio 전송 시작
const transport = new StdioServerTransport();
await server.connect(transport);

핵심 패턴 4가지가 다 들어있어요:

  1. new McpServer({...}) — 서버 인스턴스 생성
  2. server.tool(name, desc, schema, handler) — 도구 등록
  3. Zod 스키마로 입력 검증
  4. StdioServerTransport 로 연결

빌드·실행:

npx tsc
node dist/server.js

4-3. Claude Desktop에 등록

홈 디렉토리(macOS: ~/Library/Application Support/Claude/) 의 claude_desktop_config.json 파일을 열어 추가:

{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/abs/path/to/dist/server.js"],
      "env": {
        "OPENWEATHER_API_KEY": "your_key_here"
      }
    }
  }
}

Claude Desktop 재시작 → 도구 팔레트에 get_weather 가 뜨면 성공. "서울 날씨 어때?" 라고 물어보면 자동으로 도구 호출.

4-4. Claude Code에 등록

Claude Code는 .mcp.json 또는 명령 한 줄로 등록 가능:

claude mcp add weather node /abs/path/to/dist/server.js \
  --env OPENWEATHER_API_KEY=your_key_here

또는 .claude/mcp_servers.json 에 같은 JSON을 박아두면 됩니다.

5. Python으로 최소 서버 만들기 — FastMCP

Python 진영은 FastMCP 가 사실상 표준. 데코레이터 기반이라 코드가 더 짧아져요.

5-1. 프로젝트 초기화

mkdir my-mcp-server && cd my-mcp-server
uv init  # 또는 python -m venv .venv
uv add "mcp[cli]"  # 또는 pip install mcp

5-2. 같은 날씨 도구

server.py:

import os
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather-server")

@mcp.tool()
async def get_weather(city: str, units: str = "metric") -> dict:
    """Get current weather for a city."""
    api_key = os.environ["OPENWEATHER_API_KEY"]
    url = "https://api.openweathermap.org/data/2.5/weather"
    params = {"q": city, "units": units, "appid": api_key}

    async with httpx.AsyncClient() as client:
        res = await client.get(url, params=params)
        data = res.json()

    return {
        "city": data["name"],
        "temp": data["main"]["temp"],
        "condition": data["weather"][0]["description"],
    }

if __name__ == "__main__":
    mcp.run()

데코레이터 @mcp.tool() 한 줄로 도구가 등록됩니다. 함수 시그니처·타입힌트·docstring을 자동으로 스키마로 변환해 줘서 별도 schema 정의 코드가 거의 없어요.

실행 — python server.py 또는 uv run server.py.

Claude Desktop 등록 시 commandpython 또는 uv 로 바뀌는 것 빼고 동일.

6. Resources와 Prompts도 추가하기

도구만 만들고 끝내는 게 보통이지만, 자원프롬프트 까지 같이 노출하면 사용 경험이 훨씬 좋아집니다.

6-1. Resources — URI로 식별되는 읽기 데이터

TypeScript:

server.resource(
  "user-profile",
  "app://users/{userId}/profile",
  async (uri, { userId }) => {
    const profile = await db.users.findById(userId);
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(profile),
        },
      ],
    };
  },
);

URI 템플릿({userId})으로 동적 자원 을 노출할 수 있어요. Claude Desktop의 Resources 메뉴에 자동으로 뜨고, 대화 중 "이 사용자 정보를 컨텍스트에 넣어줘" 같은 식으로 호출됩니다.

Python(FastMCP):

@mcp.resource("app://users/{user_id}/profile")
async def get_user_profile(user_id: str) -> dict:
    """User profile data."""
    return await db.users.find_by_id(user_id)

6-2. Prompts — 사용자가 명시적으로 부르는 템플릿

server.prompt(
  "review-code",
  "Review code for best practices and bugs",
  { code: z.string(), language: z.string().optional() },
  ({ code, language }) => ({
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: `Please review the following ${language ?? ""} code.\n\n\`\`\`\n${code}\n\`\`\``,
      },
    }],
  }),
);

사용자가 Claude Desktop에서 /review-code 슬래시 명령으로 호출하면 정해진 프롬프트가 그대로 들어가요. 팀 전체가 "코드 리뷰는 이 프롬프트로" 라는 표준을 공유할 수 있습니다.

7. 실전 패턴 — 가드레일·로깅·환경 변수

장난감 서버 → 프로덕션 서버로 넘어갈 때 박아야 할 5가지.

7-1. 환경 변수로 비밀 관리

API 키·DB 비밀번호는 절대 코드에 박지 마세요. claude_desktop_config.jsonenv 필드에 박는 게 표준이고, 더 진지한 시스템이라면 OS keychain·Vault를 쓰는 게 안전.

7-2. 입력 검증을 가장 먼저

const input = QuerySchema.parse(args);  // 실패하면 즉시 throw

Zod(TS)·Pydantic(Python)으로 검증하면 "SQL 인젝션이 들어온다면?" 같은 우려가 한 줄로 막힙니다.

7-3. Pre-check / Post-check 가드레일

위험한 도구(파일 삭제, DB 마이그레이션)는 실행 전후로 검증을 박아야 해요:

server.tool("delete_file", "...", schema, async (args) => {
  // Pre-check
  const check = await canDelete(args.path);
  if (!check.ok) {
    return { content: [{ type: "text", text: `Denied: ${check.reason}` }] };
  }

  // 실행
  const result = await fs.unlink(args.path);

  // Post-check
  logActivity({ tool: "delete_file", args, result });

  return { content: [{ type: "text", text: "OK" }] };
});

7-4. 활동 로그

모델이 어떤 도구를 언제·왜 호출했는지 디스크에 남겨두면 디버깅이 비교가 안 되게 빨라집니다. 한 작업당 JSON 한 줄이면 충분.

7-5. 도구별 권한 분리

같은 서버 안 도구라도 읽기만 허용·쓰기는 차단 같은 권한 분리가 필요할 수 있어요. 환경 변수로 허용 도구 목록(allowlist) 을 받아 필터링하는 패턴이 흔합니다:

const allowedTools = (process.env.ALLOWED_TOOLS ?? "")
  .split(",")
  .map(s => s.trim());

const filteredTools = allTools.filter(t => allowedTools.includes(t.name));

8. 디버깅 — 안 될 때 보는 곳

처음 서버 만들 때 가장 자주 만나는 문제와 점검 순서.

증상점검할 것
Claude Desktop에 서버가 안 뜸claude_desktop_config.json 경로·JSON 오타 확인, Claude 재시작
서버는 떴는데 도구가 안 보임server.tool(...) 등록 코드 다시, 스키마 유효성 확인
도구 호출 시 에러Claude Desktop "Open Developer Tools" 로 stdout 로그 확인
변경한 코드가 반영 안 됨TS는 npx tsc 재빌드, Python은 캐시 확인. Claude 재시작
command not foundcommand 절대경로로 박기 (node 대신 /usr/local/bin/node)

가장 유용한 도구 — Anthropic 공식 MCP Inspector. 서버를 Claude 거치지 않고 직접 띄워 도구·자원·프롬프트를 GUI로 호출·검증할 수 있어요:

npx @modelcontextprotocol/inspector node dist/server.js

브라우저가 자동으로 뜨면서 서버에 등록된 모든 능력을 클릭으로 테스트 가능. 본격 개발 들어가기 전에 한 번 깔아두면 디버깅 시간이 절반으로 줄어요.

9. 어디서 시작하는 게 좋나 — 첫 서버 추천

처음 만들 때 너무 야심차게 시작하면 중도 포기가 흔합니다. 다음 3개 중 하나로 시작하길 권합니다.

  1. Hello world 도구 (1시간) — 위 날씨 도구 코드 그대로. 등록·작동까지 한 번 끝내보기
  2. 로컬 메모 도구 (반나절) — 텍스트 파일에 메모 쓰기·읽기. 자원·도구 둘 다 노출하는 첫 연습
  3. 사내 DB 읽기 도구 (1~2일) — 회사 Postgres·MySQL에 read-only 권한으로 붙여 "고객 N명 매출 합산" 같은 도구. 가장 가치 큰 첫 서버

3번부터 시도해야 "왜 MCP 서버를 직접 만드나" 라는 질문에 대한 답이 손에 들어옵니다. "내 회사 데이터를 Claude가 자연어로 조작한다" — 그게 MCP의 진짜 매력이에요.

10. 마켓플레이스에 올려 공유하기 (옵션)

만든 서버를 팀·세계와 공유하고 싶으면 — 옵션 두 가지.

  • npm 공개npm publish 한 줄로 누구나 npx your-mcp-server 로 띄울 수 있게
  • GitHub MCP 마켓modelcontextprotocol/servers 리포에 PR로 등록, 또는 자체 마켓플레이스(anthropics/claude-plugins-official 등)에 플러그인으로 패키징

플러그인으로 만들 거면 — 다음 글에서 다룰 Claude Code 커스텀 스킬 작성법 과 같이 묶어 .claude-plugin/marketplace.json 으로 배포하면 MCP 서버 + 스킬 + 슬래시 명령 한 묶음이 됩니다.

11. 한 줄 정리

MCP 서버 = Claude(또는 다른 LLM)에 자신의 도구·데이터를 노출하는 표준 인터페이스. TypeScript 또는 Python SDK로 30분이면 최소 서버, 반나절이면 사내 도구, 하루면 프로덕션급 가드레일까지 완성할 수 있어요.

핵심 결정 3가지:

  1. Tools/Resources/Prompts 중 어느 카테고리 — 모델이 부르나, 호스트가 부르나, 사용자가 부르나
  2. stdio 또는 Streamable HTTP — 로컬이면 stdio, 팀·원격이면 HTTP
  3. TypeScript 또는 Python — 본인이 빠르게 다루는 언어 (둘 다 1급 SDK)

Claude 사용법 → Superpowers 플러그인 → MCP 서버로 한 단계씩 깊어지면, "AI 잘 쓰는 사람" 에서 "AI에 회사 시스템을 붙이는 엔지니어" 로 자리가 바뀝니다. 그 자리가 2026년 가장 희소한 직무 중 하나예요.


참고 자료

본 글은 일반 정보 정리용이며, MCP 사양·SDK는 빠르게 갱신됩니다. 최신 사양은 공식 사양 페이지와 각 SDK 리포의 RELEASE-NOTES에서 직접 확인하세요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!