Skip to main content

나만의 첫 MCP 서버 만들기: Python과 TypeScript로 차근차근

지난 장에서는 좋은 MCP 서버를 어떻게 설계할지 머리로 먼저 그려봤다. 이제는 손을 움직일 차례다. MCP는 설명만 들으면 그럴듯한 개념처럼 느껴지지만, 직접 서버 하나를 띄워 보는 순간 갑자기 성격이 바뀐다. 그때부터 MCP는 "남의 기술"이 아니라 "내가 쓸 수 있는 기술"이 된다.

이번 장의 목표는 아주 분명하다. 공식 SDK 흐름에 맞춰 최소 동작 MCP 서버를 직접 만들고, 도구를 노출하고, 실제 MCP 클라이언트에서 불러볼 수 있게 만드는 것이다. Python으로 한 번, TypeScript로 한 번 해보면 감이 단단해진다. 마치 자전거 보조 바퀴를 양쪽에 달아 보는 느낌이다. 어느 쪽으로 기울어도 중심을 잡을 수 있다.

먼저 한 가지 안심할 점부터 말해 두자. MCP SDK는 버전 변화가 꽤 빠른 편이라, 설치 명령이나 import 경로, 헬퍼 함수 이름은 시간이 지나며 조금씩 달라질 수 있다. 하지만 이 장에서 익히는 핵심 패턴은 그대로다. 서버를 만들고, 도구를 등록하고, stdio로 연결하고, 호스트에서 호출해 본다. 세부 문법이 조금 달라도 골격은 바뀌지 않는다.

이번 장에서 만들 것

이번 장에서는 복잡한 외부 API 대신, 로컬에서 바로 검증 가능한 아주 작은 서버를 만든다. 이유는 간단하다. 첫 서버에서 중요한 것은 "멋진 기능"이 아니라 "MCP 흐름을 정확히 이해하는 것"이기 때문이다.

우리가 만들 서버는 두 개의 도구를 가진다. 하나는 숫자 두 개를 더하는 add 도구이고, 다른 하나는 문장을 파일명이나 URL에 쓰기 좋은 형태로 바꾸는 slugify 도구다. 기능은 소박하지만, MCP 서버의 핵심은 모두 들어 있다. 서버 이름이 있고, 도구를 등록하고, 입력 스키마를 제공하고, 클라이언트가 호출하면 결과를 돌려준다.

이 작은 서버를 완성하고 나면, 그다음부터는 파일 읽기 도구를 붙이든, 사내 API를 감싸든, 데이터베이스를 연결하든 결국 같은 패턴을 반복하게 된다.

왜 이렇게 작게 시작할까

처음부터 날씨 API, 인증, 데이터베이스, 배포까지 한꺼번에 얹으면 MCP가 어렵게 느껴진다. 사실 어려운 것은 MCP 자체보다, MCP 바깥의 복잡한 주변 요소들인 경우가 많다. 그래서 첫 서버는 일부러 작아야 한다.

작은 서버로 시작하면 세 가지를 아주 또렷하게 볼 수 있다.

  • 서버가 어떻게 시작되는지

  • 도구가 어떤 방식으로 등록되는지

  • 클라이언트가 왜 그 도구를 이해하고 호출할 수 있는지

한마디로 말하면, 첫 서버는 "기능 개발"이 아니라 "배관 공사 이해"다. 수도꼭지가 반짝이는지보다 물이 제대로 흐르는지가 먼저다.

MCP 서버의 최소 흐름 먼저 머리에 넣기

코드를 쓰기 전에 전체 흐름을 짧게 정리해 보자.

사용자가 Claude나 Cursor 같은 호스트에서 요청을 보낸다. 그러면 호스트 안의 MCP 클라이언트가 서버와 연결된다. 서버는 자신이 가진 도구 목록을 알려준다. 모델은 도구 설명과 입력 형식을 보고 "아, 이럴 때 이 도구를 쓰면 되겠군" 하고 판단한다. 실제 호출이 일어나면 서버의 함수가 실행되고, 결과가 다시 클라이언트를 거쳐 사용자에게 돌아온다.

이 과정을 아주 거칠게 그리면 다음과 같다.

사용자 질문 → 호스트/클라이언트 → MCP 서버의 도구 실행 → 결과 반환

여기서 중요한 점이 하나 있다. 서버는 그냥 함수를 만들어 두는 프로그램이 아니다. "이 함수는 도구입니다"라고 MCP 규칙에 맞게 소개하는 프로그램이다. 바로 그 소개서를 SDK가 도와준다.

Python으로 첫 MCP 서버 만들기

Python 쪽은 FastMCP를 쓰면 아주 빠르게 시작할 수 있다. 장점은 분명하다. 함수에 타입 힌트와 독스트링만 잘 적어도 도구 설명과 입력 구조가 자연스럽게 만들어진다. 즉, 사람이 읽기 좋은 코드가 곧 모델이 이해하기 좋은 인터페이스가 된다.

개발 환경 준비

먼저 프로젝트를 만든다. Python 3.10 이상을 권장한다.

uv init my-first-mcp-python
cd my-first-mcp-python
uv venv

가상환경을 활성화한 뒤 MCP SDK를 설치한다.

# Windows
.venvScriptsactivate

# macOS / Linux
source .venv/bin/activate

uv add "mcp[cli]"

이제 server.py 파일을 만든다.

가장 작은 서버 코드 작성하기

아래 코드는 최소 동작 예제다.

import re
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("starter-tools")


@mcp.tool()
def add(a: int, b: int) -> int:
    """두 정수를 더합니다."""
    return a + b


@mcp.tool()
def slugify(text: str) -> str:
    """문장을 파일명이나 URL에 쓰기 좋은 slug 형태로 바꿉니다."""
    text = text.strip().lower()
    text = re.sub(r"[^a-z0-9가-힣s-]", "", text)
    text = re.sub(r"[s_-]+", "-", text)
    return text.strip("-")


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

처음 보면 놀랄 만큼 짧다. 그런데 이 짧은 코드 안에 MCP의 핵심이 다 들어 있다.

FastMCP("starter-tools")는 서버 인스턴스를 만든다. 여기의 이름은 클라이언트가 서버를 식별할 때 쓰인다. @mcp.tool()은 바로 이 함수가 MCP 도구라는 뜻이다. 그리고 함수의 매개변수 타입과 반환 타입, 독스트링이 클라이언트에게 전달될 힌트가 된다.

즉, Python에서는 "코드를 잘 쓰는 것"이 곧 "도구 설명을 잘 쓰는 것"과 거의 이어진다.

이 코드에서 꼭 봐야 할 포인트

첫째, add(a: int, b: int) 같은 타입 힌트는 단순한 취향 문제가 아니다. 모델과 클라이언트가 이 도구의 입력 구조를 이해하는 데 도움이 된다.

둘째, 독스트링은 장식이 아니다. 모델은 도구 이름만 보는 것이 아니라 설명도 함께 본다. 설명이 부실하면 도구를 잘못 고르거나 엉뚱하게 쓸 수 있다.

셋째, transport="stdio"는 가장 쉬운 시작점이다. 호스트가 이 서버를 로컬 프로세스로 실행하고 표준 입력과 출력으로 통신한다. 즉, 지금은 네트워크 서버를 띄우는 것이 아니라 "말 잘 듣는 로컬 프로그램"을 만드는 단계라고 생각하면 된다.

Python 서버 실행하기

다음 명령으로 실행한다.

uv run server.py

터미널이 조용히 멈춘 것처럼 보여도 놀라지 말자. 망가진 것이 아니라, 지금 서버가 MCP 메시지를 기다리는 중이다. 첫 서버에서 가장 흔한 오해가 바로 이것이다. 아무 출력이 없다고 실패한 것이 아니다. 오히려 정상일 가능성이 높다.

여기서 아주 중요한 규칙이 하나 있다. stdio 서버에서는 print()로 표준 출력에 로그를 찍지 않는 편이 안전하다. 표준 출력은 MCP 메시지가 오가는 통로라서, 잡담이 섞이면 통신이 깨질 수 있다. 디버깅 로그가 꼭 필요하다면 표준 에러 쪽을 사용하는 습관을 들이는 것이 좋다.

Python 서버를 클라이언트에 연결해 보기

이미 앞 장에서 Claude Desktop이나 Cursor에 MCP를 붙여봤다면, 이번에는 "사용자"가 아니라 "제작자"의 마음으로 같은 설정을 다시 보게 된다.

예를 들어 Claude Desktop 설정은 대략 이런 형태다.

{
  "mcpServers": {
    "starter-tools-python": {
      "command": "uv",
      "args": [
        "--directory",
        "C:\path\to\my-first-mcp-python",
        "run",
        "server.py"
      ]
    }
  }
}

경로는 자신의 프로젝트 위치로 바꾸면 된다. 설정 후 Claude Desktop을 재시작하면 도구 목록에 서버가 보일 수 있다.

이제 다음처럼 물어보면 된다.

"12와 30을 더해줘."

또는 이렇게도 해볼 수 있다.

"MCP 첫 서버 만들기!를 slug로 바꿔줘."

도구가 잘 등록됐다면 모델은 상황에 맞게 add 또는 slugify를 고른다. 이 순간이 꽤 짜릿하다. 내가 만든 작은 함수가 AI의 손발이 되는 장면이기 때문이다.

TypeScript로 같은 서버 다시 만들기

이번에는 같은 아이디어를 TypeScript로 옮겨 보자. TypeScript 쪽은 Python보다 조금 더 명시적이다. Python이 "타입 힌트와 독스트링으로 자연스럽게 설명하는 스타일"이라면, TypeScript는 "스키마를 분명하게 선언하는 스타일"에 가깝다.

한 번 만들어 보면 두 언어의 차이가 머리에 쏙 들어온다.

프로젝트 준비

새 폴더를 만들고 의존성을 설치한다.

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

그리고 package.json은 이런 느낌으로 정리하면 편하다.

{
  "name": "my-first-mcp-ts",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js"
  }
}

tsconfig.json도 최소한 아래 정도로 맞춰 두자.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

이제 src/index.ts 파일을 만든다.

TypeScript 서버 코드 작성하기

아래 예시는 전형적인 고수준 SDK 스타일을 보여 주는 예제다. 실제 프로젝트에서는 SDK 버전에 따라 패키지 이름이나 import 경로가 조금 달라질 수 있다.

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: "starter-tools",
  version: "1.0.0",
});

function slugifyText(text: string): string {
  return text
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9가-힣s-]/g, "")
    .replace(/[s_-]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

server.registerTool(
  "add",
  {
    description: "두 정수를 더합니다.",
    inputSchema: {
      a: z.number().describe("첫 번째 정수"),
      b: z.number().describe("두 번째 정수"),
    },
  },
  async ({ a, b }) => ({
    content: [{ type: "text", text: String(a + b) }],
  })
);

server.registerTool(
  "slugify",
  {
    description: "문장을 파일명이나 URL에 쓰기 좋은 slug 형태로 바꿉니다.",
    inputSchema: {
      text: z.string().describe("slug로 바꿀 문장"),
    },
  },
  async ({ text }) => ({
    content: [{ type: "text", text: slugifyText(text) }],
  })
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("starter-tools MCP server started");
}

main().catch(console.error);

이 코드도 구조는 똑같다. 서버를 만들고, 도구를 등록하고, stdio 전송 방식으로 연결한다. 다만 Python과 달리 TypeScript에서는 zod를 사용해 입력 스키마를 더 명시적으로 적는다.

이게 왜 좋을까. 첫째, 타입이 분명하다. 둘째, 설명을 필드마다 붙일 수 있다. 셋째, 나중에 도구가 복잡해졌을 때 입력 검증을 훨씬 세밀하게 다룰 수 있다. TypeScript 쪽이 조금 장황해 보여도, 규모가 커지면 이 명시성이 큰 힘을 발휘한다.

TypeScript 서버 실행하기

먼저 빌드한다.

npm run build

그다음 실행한다.

node build/index.js

역시 마찬가지로, 실행 후 조용히 대기하는 것은 정상이다. 서버는 지금 클라이언트를 기다리고 있다.

여기서도 규칙은 같다. console.log()로 표준 출력에 로그를 마구 찍지 말자. TypeScript stdio 서버에서 로그는 console.error() 쪽으로 보내는 습관이 안전하다. JSON-RPC 통신선에 잡담을 흘려보내면 서버가 갑자기 말을 더듬기 시작한다.

TypeScript 서버를 클라이언트에 연결하기

Claude Desktop 설정 예시는 대략 다음과 같다.

{
  "mcpServers": {
    "starter-tools-ts": {
      "command": "node",
      "args": [
        "C:\path\to\my-first-mcp-ts\build\index.js"
      ]
    }
  }
}

설정 후 재시작하고 같은 질문을 던져 보자.

"100과 23을 더해줘."

"Cursor용 MCP 서버 초안을 slug로 바꿔줘."

잘 된다면, Python 서버에서 느꼈던 그 감각이 다시 온다. 언어는 달라도 패턴은 같다. 이 반복이 중요하다. 패턴이 몸에 붙으면, 이후의 복잡한 서버도 결국 같은 골격 위에 올라간다는 걸 알게 된다.

Python과 TypeScript는 무엇이 다를까

둘 다 공식 SDK 생태계 안에서 MCP 서버를 만들 수 있지만, 개발 감각은 조금 다르다.

항목PythonTypeScript
시작 속도매우 빠름약간의 초기 설정 필요
도구 정의 방식데코레이터, 타입 힌트, 독스트링 중심registerTool()과 Zod 스키마 중심
입력 설명 방식함수 시그니처와 문서화에 기대는 편스키마에 명시적으로 적는 편
코드 느낌간결하고 부드러움명시적이고 단단함
추천 상황빠른 프로토타입, 내부 자동화규모 있는 프로젝트, 타입 안정성 중시

어느 쪽이 더 낫다고 단정할 필요는 없다. 이미 Python 생태계에 익숙하다면 Python으로 시작하는 것이 빠르다. 프런트엔드나 Node 기반 도구와 붙일 일이 많고 타입 안정성이 중요하다면 TypeScript가 잘 맞는다.

중요한 것은 언어가 아니라 감각이다. "도구를 어떻게 설명해야 모델이 잘 쓰는가"를 배우는 것이 진짜 핵심이다.

도구를 잘 노출하는 사람의 작은 습관

이제 막 첫 서버를 만들었더라도, 바로 실전에 도움이 되는 습관들이 있다.

첫째, 도구 이름은 짧고 분명하게 짓는 편이 좋다. doSomethingCool보다는 add, read_file, search_orders 같은 이름이 낫다. 모델도 사람도 덜 헷갈린다.

둘째, 설명은 "무엇을 하는가"뿐 아니라 "언제 써야 하는가"까지 적어 주는 편이 좋다. 예를 들어 slugify라면 단순히 "문자열을 변환합니다"보다 "문장을 파일명이나 URL에 쓰기 좋은 slug 형태로 바꿉니다"가 훨씬 낫다.

셋째, 입력 필드는 친절할수록 좋다. TypeScript라면 Zod의 .describe()를 적극적으로 쓰고, Python이라면 함수 이름, 매개변수 이름, 독스트링을 허술하게 넘기지 말자.

넷째, 첫 서버는 읽기 전용이거나 부작용이 적은 도구부터 시작하는 것이 좋다. 파일 삭제, 결제 실행, 데이터 수정처럼 무거운 도구는 나중에 붙이는 편이 안전하다.

초보자가 가장 자주 만나는 함정

첫 서버는 작지만, 함정은 은근히 많다. 그래도 미리 알고 가면 대부분 5분 안에 해결된다.

표준 출력 오염

가장 대표적인 문제다. stdio 서버는 표준 출력으로 프로토콜 메시지를 주고받는다. 여기에 print()나 console.log()가 섞이면 메시지가 깨진다. 증상은 다양하다. 서버가 연결되지 않거나, 갑자기 끊기거나, 도구 목록이 보이지 않을 수 있다.

기억할 문장은 하나면 충분하다. stdio 서버의 stdout은 채팅방이 아니라 배관이다.

빌드는 됐는데 도구가 안 보임

보통은 설정 파일 경로가 잘못됐거나, 서버를 재시작하지 않았거나, 실행 명령이 틀린 경우가 많다. Python은 uv --directory ... run server.py 경로를 다시 확인하고, TypeScript는 build/index.js가 실제로 생성됐는지 확인하자.

설명을 너무 대충 씀

코드는 멀쩡한데 모델이 도구를 이상하게 고르는 경우가 있다. 이때는 종종 설명이 약하다. 모델은 이름만 보고 찍는 존재가 아니다. 설명과 스키마를 읽는다. 좋은 설명은 도구의 성능을 올리는 가장 싼 방법이다.

너무 빨리 외부 API를 붙임

첫날부터 인증, 토큰, 네트워크 오류, 속도 제한까지 얹으면 어디서 문제가 났는지 분간하기 어렵다. 첫 서버는 "도구 등록과 호출"만 확인되는 수준이면 충분하다. 그다음에 외부 세계와 연결해도 늦지 않다.

직접 만들어 보면 보이는 것들

이쯤에서 중요한 깨달음이 하나 생긴다. MCP 서버는 거창한 새 세계라기보다, 기존의 함수를 AI가 이해할 수 있게 포장하는 방식에 가깝다. 함수를 잘 만들고, 설명을 잘 쓰고, 입력과 출력을 분명히 하면 된다.

다시 말해, MCP 서버 개발은 완전히 낯선 마법이 아니다. 우리가 원래 하던 백엔드 개발, 자동화 스크립트 작성, API 래핑 작업이 "AI 친화적인 인터페이스"를 얻는 과정에 가깝다.

이 관점이 생기면 두려움이 줄어든다. "MCP 서버를 만들어야 한다"가 아니라 "내 기능을 MCP 도구로 노출하면 된다"로 보이기 시작하기 때문이다.

다음 단계로 어떻게 키울까

지금 만든 서버는 아주 작다. 하지만 뼈대는 이미 충분하다. 여기서 다음과 같은 방향으로 자연스럽게 확장할 수 있다.

  • add 대신 계산기 도구 묶음을 만들기

  • slugify 대신 파일명 정리, 텍스트 정규화, 코드 생성 보조 도구 만들기

  • 외부 API를 붙여 진짜 데이터 가져오기

  • 데이터베이스 조회 도구 추가하기

  • 읽기 전용 리소스와 재사용 가능한 프롬프트 넣기

여기서부터는 "함수 몇 개 더 추가하기"가 아니라 "좋은 MCP 서버답게 기능을 넓혀 가기"의 영역이 된다. 그리고 바로 그다음 단계가 다음 장의 주제다.

마무리

이번 장에서 우리는 아주 작은 MCP 서버를 Python과 TypeScript로 각각 만들어 봤다. 복잡한 일을 하지는 않았지만, 오히려 그래서 본질이 잘 보였다. 서버를 만들고, 도구를 등록하고, 클라이언트가 그것을 발견하고, 모델이 호출하는 흐름을 직접 손으로 만져 봤다.

이 경험이 중요하다. 한 번이라도 직접 만들어 보면 MCP는 더 이상 추상적인 표준 문서가 아니다. 내가 설계하고, 구현하고, 연결하고, 고칠 수 있는 기술이 된다.

이제 당신은 "MCP가 뭔지 아는 사람"에서 한 걸음 더 나아갔다. 이제는 "MCP 서버를 직접 띄워 본 사람"이다. 그리고 이 차이는 생각보다 크다. 개념은 잊혀도, 손으로 만든 감각은 오래 남는다.

책 목록

MCP, 진짜로 써먹는 법: 연결부터 서버 제작, 운영까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기