MCP 서버 직접 만들어 Claude에 연결하기
- MCP 서버는 Claude가 부를 수 있는 도구(함수)를 내놓는 프로그램이에요. 공식 Python SDK(
mcp)로 함수에@mcp.tool()만 붙이면 만들어져요. - 내 컴퓨터에서만 쓰면 stdio, 여러 사람이나 Claude 앱에서 쓰려면 Streamable HTTP(공용 HTTPS 주소)로 띄워요.
- Claude Code에는
claude mcp add로, Claude 앱에는 커스텀 커넥터(원격 서버 URL)로 연결해요. 로컬 stdio 서버는 Claude 앱 채팅(claude.ai)에서는 못 써요. - 서버가 돌려주는 글은 모델이 읽는 입력이에요. 읽기 전용 도구부터, 입력 검증, 비밀 값은 환경변수로, 인증 없는 공개는 금지가 기본이에요.
- 예제는 공식 문서 기준이며 2026-10-11에 문법을 검증했어요. 서버 동작도 로컬에서 직접 호출해 확인했어요.
이 튜토리얼로 할 수 있게 되는 것
섹션 제목: “이 튜토리얼로 할 수 있게 되는 것”- 공식 SDK로 재고 조회 도구 2개를 가진 MCP 서버를 만들고, 호출 테스트까지 할 수 있어요.
- 같은 서버를 Claude Code에
claude mcp add로 붙이고/mcp로 상태를 확인할 수 있어요. - HTTP로 띄운 서버를 Claude 앱의 커스텀 커넥터로 연결하는 절차와 조건을 알 수 있어요.
- MCP 서버를 만들 때 지켜야 할 보안 원칙(권한 최소화, 프롬프트 인젝션, 비밀 값)을 적용할 수 있어요.
먼저 알아둘 개념
섹션 제목: “먼저 알아둘 개념”| 용어 | 뜻 |
|---|---|
| MCP | Model Context Protocol. AI 앱과 외부 도구·데이터를 잇는 개방형 표준이에요. 쉬운 소개는 20. 커넥터(MCP)를 보세요. |
| MCP 서버 | 도구(tools), 자원(resources), 프롬프트를 내놓는 프로그램이에요. 이 편은 도구만 다뤄요. |
| 호스트와 클라이언트 | Claude Code나 Claude 앱 같은 AI 앱이 호스트이고, 그 안의 클라이언트가 서버와 1:1로 연결해요. |
| stdio | 호스트가 서버를 자기 컴퓨터에서 자식 프로세스로 실행하고 표준 입출력으로 대화하는 방식이에요. |
| Streamable HTTP | 서버가 URL(기본 경로 /mcp)로 떠 있고 여러 클라이언트가 접속하는 방식이에요. 원격 서버에 쓰는 방식이에요. |
| 커스텀 커넥터 | Claude 앱에서 원격 MCP 서버의 URL을 직접 넣어 연결하는 기능이에요. C7에서 개념을 다뤘어요. |
어디에 연결할 수 있나요?
섹션 제목: “어디에 연결할 수 있나요?”| 연결 대상 | 서버 방식 | 서버가 있는 곳 |
|---|---|---|
| Claude Code (터미널) | stdio 또는 HTTP | 내 컴퓨터 또는 원격 |
Claude Desktop의 로컬 설정 (claude_desktop_config.json) |
stdio | 내 컴퓨터 (Cowork와 claude.ai에서는 안 쓰여요) |
| Claude 앱 커스텀 커넥터 (claude.ai, Desktop, Cowork) | HTTP | Anthropic 클라우드에서 닿는 공용 인터넷 |
단계별 따라하기
섹션 제목: “단계별 따라하기”-
작업 폴더와 SDK 설치
Python 3.10 이상이 필요해요. 가상환경을 만들고 공식 SDK를 설치해요.
cli확장은 개발용mcp dev명령을 같이 깔아 줘요.터미널 창 mkdir inventory-mcp && cd inventory-mcppython3 -m venv .venvsource .venv/bin/activatepip install "mcp[cli]"uv를 쓴다면uv add "mcp[cli]"도 같은 결과예요. -
서버 만들기: 읽기 전용 도구 2개
server.py를 만들어요. 재고 조회 서버예요. 실제로는 DB나 사내 API를 읽겠지만, 예제는 메모리의 가짜 데이터를 써요."""재고 조회 MCP 서버 (읽기 전용 예제)."""import sysfrom mcp.server import MCPServerfrom mcp.server.mcpserver.exceptions import ToolErrorfrom mcp.types import ToolAnnotationsmcp = MCPServer("inventory")# 실제로는 DB나 사내 API를 읽겠지만, 예제는 메모리 데이터를 써요.STOCK = {"KB-100": {"name": "무선 키보드", "qty": 42, "location": "A-1"},"MS-200": {"name": "무선 마우스", "qty": 0, "location": "A-2"},"MN-300": {"name": "27인치 모니터", "qty": 7, "location": "B-1"},}@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))def get_stock(sku: str) -> str:"""SKU 코드(예: KB-100)로 재고 수량과 보관 위치를 조회한다."""print(f"get_stock called: {sku!r}", file=sys.stderr) # 로그는 stderr로item = STOCK.get(sku.strip().upper())if item is None:raise ToolError(f"알 수 없는 SKU예요: {sku}")return f"{item['name']}: {item['qty']}개 (위치 {item['location']})"@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))def search_items(keyword: str, max_results: int = 5) -> list[dict]:"""상품 이름에 keyword가 들어간 항목을 찾아 SKU와 수량을 돌려준다."""max_results = max(1, min(max_results, 20)) # 상한을 코드에서 강제hits = [{"sku": sku, "name": v["name"], "qty": v["qty"]}for sku, v in STOCK.items()if keyword in v["name"]]return hits[:max_results]if __name__ == "__main__":mcp.run() # 기본값은 stdio코드에서 눈여겨볼 점이에요.
- 타입 힌트가 곧 스키마예요.
sku: str,max_results: int = 5로 JSON 스키마가 만들어지고 입력 검증도 SDK가 해 줘요. docstring이 모델에게 보이는 도구 설명이에요. 설명을 구체적으로 쓸수록 Claude가 알맞게 불러요. readOnlyHint=True는 힌트일 뿐이에요. 클라이언트에게 “읽기만 하는 도구”라고 알려 주지만, MCP 명세는 믿을 수 있는 서버가 아니면 이런 주석을 신뢰하지 말라고 해요. 안전은 코드로 지켜야 해요.- 예상한 실패는
ToolError로 올려요.ToolError의 메시지는 모델에게 그대로 전달돼서 Claude가 다른 SKU로 다시 시도할 수 있어요. 일반 예외(ValueError등)는 모델에게 “Error executing tool …“만 전달되고 원문은 서버 로그에만 남아요. 직접 실행해서 둘의 차이를 확인했어요. - 로그는 stderr로 써요. stdio 서버에서 표준 출력(stdout)은 프로토콜 통로예요.
print()를 stdout으로 쓰면 통신이 깨져요.
문법을 확인해요.
터미널 창 python3 -m py_compile server.py && echo 문법 OK - 타입 힌트가 곧 스키마예요.
-
연결하기 전에 직접 호출해 보기
Claude에 붙이기 전에 서버 자체를 먼저 확인하면 문제를 나눠서 볼 수 있어요. SDK의 인메모리 클라이언트로
test_server.py를 만들어요.import asynciofrom mcp import Clientfrom server import mcpasync def main() -> None:async with Client(mcp) as client:tools = await client.list_tools()print([t.name for t in tools.tools])ok = await client.call_tool("get_stock", {"sku": "kb-100"})print(ok.content[0].text)found = await client.call_tool("search_items", {"keyword": "무선"})print(found.structured_content)bad = await client.call_tool("get_stock", {"sku": "zz"})print(bad.is_error, bad.content[0].text)asyncio.run(main())터미널 창 python test_server.py도구 목록,
무선 키보드: 42개 (위치 A-1), 검색 결과, 그리고 마지막 줄에True Error executing tool get_stock: 알 수 없는 SKU예요: zz가 보이면 정상이에요. 브라우저 화면으로 도구를 눌러 보고 싶다면mcp dev server.py로 MCP Inspector를 열 수 있어요(공식 README 안내). -
Claude Code에 연결하기 (stdio)
claude mcp add는--를 기준으로 앞은 Claude Code의 옵션, 뒤는 서버를 실행하는 명령이에요. 서버 실행 경로는 절대 경로로 쓰세요.터미널 창 claude mcp add inventory -- "$PWD/.venv/bin/python" "$PWD/server.py"claude mcp listinventory: … - Connected가 보이면 성공이에요. 세션을 열고/mcp로 상태를 보거나, 바로 물어보세요.이 프롬프트를 복사해서 붙여넣으세요inventory 서버로 KB-100 재고를 알려 주고, 이름에 “무선”이 들어간 상품 중 재고가 0인 걸 골라 줘.
Claude가 도구를 부르기 전에 승인을 물어요. 서버 도구는
mcp__inventory__get_stock처럼mcp__서버이름__도구이름형태로 권한 규칙과 훅에서 가리켜요.이 단계에서 보게 될 화면
/mcp 화면에서 inventory 서버가 Connected로 보이는 모습 Claude Code 문서에서 보기 ↗실제 화면은 서비스 업데이트에 따라 다를 수 있어요
범위(scope)는 이렇게 골라요. 자세한 설명은 C14에 있어요.
범위 옵션 저장 위치 쓰임 local (기본) --scope local~/.claude.json나만, 이 프로젝트에서 project --scope project프로젝트의 .mcp.json팀과 git으로 공유 user --scope user~/.claude.json내 모든 프로젝트 비밀 값이 필요한 서버라면
--env KEY=값으로 넘기거나.mcp.json에서${VAR}환경변수 확장을 쓰세요. 값 자체를 파일에 적어 커밋하지 마세요.{"mcpServers": {"inventory": {"type": "stdio","command": "${CLAUDE_PROJECT_DIR:-.}/.venv/bin/python","args": ["${CLAUDE_PROJECT_DIR:-.}/server.py"],"env": { "INVENTORY_API_TOKEN": "${INVENTORY_API_TOKEN}" }}}} -
HTTP로 띄우기 (원격 서버와 Claude 앱용)
같은 서버를 URL로 접속하는 방식으로 띄워요. 파일을 하나 더 만들어요.
from server import mcpmcp.run(transport="streamable-http", host="127.0.0.1", port=8000)이렇게
serve_http.py로 저장해python serve_http.py로 실행하면http://127.0.0.1:8000/mcp에서 접속할 수 있어요. 직접 실행해서 Claude Code로 확인했어요.터미널 창 claude mcp add --transport http inventory-http http://127.0.0.1:8000/mcpclaude mcp listConnected가 나오면 돼요. 서버를 끄면Failed to connect로 바뀌어요. -
Claude 앱에 커스텀 커넥터로 연결하기
Claude 앱의 커스텀 커넥터는 내 기기가 아니라 Anthropic의 클라우드에서 서버로 접속해요. 그래서 조건이 있어요.
- 서버가 공용 인터넷에서 HTTPS로 닿아야 해요.
127.0.0.1, 사내망, VPN 뒤의 서버는 안 돼요. - 사설 서버를 방화벽 뒤에 두려면 Anthropic IP 대역을 허용해야 해요(대역 목록은 platform.claude.com/docs/en/api/ip-addresses).
- 서버 주소는
https://도메인/mcp처럼 MCP 경로까지 써요.
조건이 갖춰졌다면 Pro/Max 개인 계정은 이렇게 해요(2026-10-11 지원센터 문서 기준, 화면 표기는 바뀔 수 있어요).
- 사용자 지정(Customize) > 커넥터(Connectors)에서 + Add > Add custom connector
- 이름과 서버 URL을 넣고 Continue
- 인증 설정을 확인해요. Sign in now, Sign in when needed, No sign in 중에서 골라요. OAuth나 요청 헤더(고정 자격 증명)도 쓸 수 있어요.
- Add를 누르고, 대화창의 + > Connectors에서 켜요.
Team/Enterprise는 Owner(또는 Primary Owner)가 조직 설정(Organization settings) > 커넥터(Connectors)에서 Add > Custom > Web으로 추가하고, 구성원은 Customize > Connectors에서 Connect를 눌러요. 수정은 제거했다가 다시 추가하는 방식이에요. Free 플랜은 커스텀 커넥터가 1개까지예요.
이 단계에서 보게 될 화면
커스텀 커넥터 추가 대화상자(이름, 서버 URL, 인증 선택) Anthropic 도움말에서 보기 ↗실제 화면은 서비스 업데이트에 따라 다를 수 있어요
Claude 앱에 커스텀 커넥터 추가하기 (Pro/Max) - Customize > Connectors+ Add 누르고 Add custom connector 고르기
- 추가 대화상자이름과 서버 URL(https://도메인/mcp) 넣고 Continue
- 인증 설정Sign in now / Sign in when needed / No sign in 중에서 고르기
- 추가 대화상자Add 누르기→ 대화창의 + > Connectors에서 켤 수 있어요
단순화한 흐름도예요. 실제 화면은 공식 도움말에서 확인하세요.
- 서버가 공용 인터넷에서 HTTPS로 닿아야 해요.
-
같은 서버를 TypeScript로 만든다면 (참고)
TypeScript SDK v2로는 이런 모양이에요. Node.js 20 이상이 필요하고,
npm pkg set type=module로 ESM을 켜야 해요.터미널 창 npm init -y && npm pkg set type=modulenpm install @modelcontextprotocol/server zod tsximport { McpServer } from '@modelcontextprotocol/server';import { serveStdio } from '@modelcontextprotocol/server/stdio';import * as z from 'zod/v4';const STOCK: Record<string, { name: string; qty: number; location: string }> = {'KB-100': { name: '무선 키보드', qty: 42, location: 'A-1' },'MS-200': { name: '무선 마우스', qty: 0, location: 'A-2' }};function createServer(): McpServer {const server = new McpServer({ name: 'inventory', version: '1.0.0' });server.registerTool('get_stock',{description: 'SKU 코드(예: KB-100)로 재고 수량과 보관 위치를 조회한다.',inputSchema: z.object({ sku: z.string().max(20).describe('SKU 코드') }),annotations: { readOnlyHint: true }},async ({ sku }) => {const item = STOCK[sku.trim().toUpperCase()];if (!item) {return { content: [{ type: 'text', text: `알 수 없는 SKU예요: ${sku}` }], isError: true };}return { content: [{ type: 'text', text: `${item.name}: ${item.qty}개 (위치 ${item.location})` }] };});return server;}void serveStdio(createServer);console.error('inventory MCP server running on stdio'); // 로그는 stderr로npx tsx server.ts로 실행하고, Claude Code에는claude mcp add inventory -- npx tsx /절대/경로/server.ts로 붙여요. 이 코드는tsx로 직접 띄워initialize와 도구 호출 응답을 확인했어요.
빠른 참조표
섹션 제목: “빠른 참조표”claude mcp 명령
섹션 제목: “claude mcp 명령”| 하고 싶은 일 | 명령 |
|---|---|
| 로컬 stdio 서버 추가 | claude mcp add <이름> -- <명령> [인자...] |
| 환경변수와 함께 추가 | claude mcp add --env KEY=값 --transport stdio <이름> -- <명령> |
| 원격 HTTP 서버 추가 | claude mcp add --transport http <이름> <URL> |
| 헤더(토큰) 붙이기 | --header "Authorization: Bearer ..." (토큰을 셸 기록에 남기지 않도록 주의) |
| 목록과 상태 | claude mcp list |
| 서버 상세 | claude mcp get <이름> |
| 제거 | claude mcp remove <이름> (저장된 OAuth 토큰도 삭제) |
| 세션 안에서 상태·인증·재연결 | /mcp |
| JSON으로 추가 | claude mcp add-json <이름> '<json>' |
SSE 전송은 deprecated예요. 새 서버는 HTTP를 쓰세요.
도구 결과 크기 한도 (Claude Code)
섹션 제목: “도구 결과 크기 한도 (Claude Code)”| 항목 | 동작 |
|---|---|
| 10,000토큰 초과 | 경고가 표시돼요 |
MAX_MCP_OUTPUT_TOKENS |
최대 출력 토큰, 기본 25,000 |
| 텍스트 결과 50,000자 초과 | 파일로 저장하고 파일 참조로 바뀌어요 |
| HTTP/SSE 응답 본문 16MB 초과 | 요청이 실패해요. 서버에서 나눠(페이지네이션) 주세요 |
서버 보안 체크리스트
섹션 제목: “서버 보안 체크리스트”| 항목 | 이렇게 해요 |
|---|---|
| 권한 최소화 | 읽기 전용 도구부터 시작해요. 쓰기·삭제 도구는 꼭 필요할 때만, 적은 범위로 넣어요. 서버가 쓰는 DB 계정·토큰도 읽기 전용으로 만들어요. |
| 입력 검증 | 타입·길이·범위를 코드에서 제한해요(예: max_results 상한). 모델이 만든 입력은 신뢰할 수 없는 입력이에요. 파일 경로나 SQL, 셸 명령을 만들 때 그대로 이어 붙이지 마세요. |
| 프롬프트 인젝션 | 도구가 돌려주는 글(메일, 웹, 티켓 본문)에 “이전 지시를 무시해” 같은 숨은 지시가 있을 수 있어요. 외부 글을 가져오는 도구는 쓰기 도구와 같은 서버에 두지 마세요. |
| 비밀 값 | 코드나 .mcp.json에 직접 쓰지 말고 환경변수로 받아요. 도구 결과와 로그에 토큰·개인정보를 싣지 마세요. |
| 출력 크기 | 한 번에 돌려주는 양을 제한해요. 큰 결과는 토큰(비용)과 컨텍스트를 같이 키워요. |
| 인증 | HTTP로 공개하는 서버는 인증을 붙여요. 공유 토큰은 모두에게 같은 권한을 줘요. MCP 공식 보안 문서(2026-10-12 기준)에 따르면 서버는 자기에게 발급된 토큰만 받아야 하고, 받은 토큰을 다른 API에 그대로 넘기는 token passthrough는 스펙이 금지해요. OAuth 스코프는 처음엔 읽기 같은 최소 범위로 시작해요. |
| 로컬 HTTP 서버 | 같은 PC의 다른 프로세스가 접근할 수 있어요. 공식 보안 문서는 로컬용 서버에 stdio를 쓰거나, HTTP라면 인증 토큰이나 접근이 제한된 IPC를 쓰라고 해요. |
| 힌트에 의존 금지 | readOnlyHint 같은 주석은 힌트예요. 실제 권한은 서버 코드와 자격 증명으로 막아요. |
| 로그 | stdio 서버는 stderr에만 써요. 민감한 값은 로그에 남기지 않아요. |
자주 막히는 지점
섹션 제목: “자주 막히는 지점”ImportError: cannot import name 'FastMCP' 또는 MCPServer를 찾을 수 없어요
섹션 제목: “ImportError: cannot import name 'FastMCP' 또는 MCPServer를 찾을 수 없어요”- 원인: 예전 튜토리얼은
FastMCP를 쓰고, 현재 SDK v2는MCPServer를 써요. 설치된mcp버전이 글과 달라서 생겨요. - 해결:
pip show mcp로 버전을 보고, 이 글의 코드는 v2 기준임을 기억하세요. 가상환경을 활성화한 상태에서pip install "mcp[cli]"로 다시 설치해 보세요.
claude mcp list에 Failed to connect가 떠요
섹션 제목: “claude mcp list에 Failed to connect가 떠요”- 원인: stdio는 서버 실행 명령이 틀렸거나(상대 경로, 가상환경 밖의
python) 서버가 시작 중에 죽은 경우가 많아요. HTTP는 서버가 안 떠 있거나 주소·포트가 달라요. - 해결: 같은 명령을 터미널에서 직접 실행해 오류를 보세요. 절대 경로로 다시
claude mcp remove후claude mcp add하세요. HTTP는ECONNREFUSED면 서버부터 켜세요.
stdio 서버가 연결 직후 멈추거나 깨진 응답이 와요
섹션 제목: “stdio 서버가 연결 직후 멈추거나 깨진 응답이 와요”- 원인: 서버가 표준 출력(stdout)에
print()로그를 찍으면 프로토콜 메시지가 섞여요. - 해결: 모든 로그를
sys.stderr(TypeScript는console.error)로 바꾸세요.
Claude가 내 도구를 안 불러요
섹션 제목: “Claude가 내 도구를 안 불러요”- 원인: 도구 이름과 설명이 모호하거나, 서버가 연결되어 있지 않거나, 승인 단계에서 거절했을 수 있어요.
- 해결:
/mcp에서 연결 상태를 확인하고, docstring을 “무엇을 하는지 + 입력 예시” 형태로 구체적으로 고치세요. 요청에 서버 이름을 넣어 보세요(예: “inventory 서버로 …”).
커스텀 커넥터를 추가했는데 연결이 안 돼요
섹션 제목: “커스텀 커넥터를 추가했는데 연결이 안 돼요”- 원인: 서버가 공용 인터넷에서 닿지 않거나(로컬·사내망·VPN), 인증 방식이 서버와 맞지 않아요. 연결은 내 기기가 아니라 Anthropic 클라우드에서 나가요.
- 해결: 외부 네트워크에서
https://도메인/mcp가 열리는지 확인하고, 사설 서버라면 Anthropic IP 대역을 허용하세요. 인증 설정은 서버가 요구하는 방식(OAuth, 헤더, 없음)과 맞추세요. 커넥터 수정은 제거 후 재추가예요.
.mcp.json 서버가 Pending approval로 멈춰 있어요
섹션 제목: “.mcp.json 서버가 Pending approval로 멈춰 있어요”- 원인: 프로젝트 범위 서버는 처음 쓸 때 승인이 필요해요. 신뢰하지 않은 폴더에서는 저장소가 스스로 승인할 수 없어요.
- 해결: 대화형
claude를 열어 승인하세요. 승인 선택을 초기화하려면claude mcp reset-project-choices를 써요.
실전 팁
섹션 제목: “실전 팁”- 도구는 적게, 설명은 구체적으로. 도구가 많으면 Claude가 고르기 어렵고 컨텍스트도 더 써요. 도구 하나가 한 가지 일을 하게 만드세요.
- 오류 메시지는 모델이 읽어요.
ToolError에 “무엇이 잘못됐고 어떻게 다시 시도할지”를 적으세요. 내부 경로나 SQL 같은 서버 내부 정보는 빼세요. - 먼저 로컬 stdio로 만들고, 필요할 때 HTTP로 옮기세요. 같은 도구 코드에서
mcp.run()의 전송 방식만 바꾸면 돼요. - 직접 만들기 전에 있는 것부터 보세요. 이미 공식 커넥터나 공개 서버가 있는 서비스라면 새로 만드는 비용이 더 클 수 있어요. 다만 서드파티 서버는 Anthropic이 검증하지 않으니 코드와 권한을 읽어 보고 쓰세요.
- 팀에 나눠 줄 때는 플러그인에 넣으세요. 플러그인은 스킬과 MCP 서버(
.mcp.json)를 한 묶음으로 배포해요. 6. 스킬·플러그인 패키지로 만들어 팀에 배포하기를 보세요.
다음에 볼 튜토리얼
섹션 제목: “다음에 볼 튜토리얼”- 6. 스킬·플러그인 패키지로 만들어 팀에 배포하기 — 서버와 스킬을 팀 배포용 플러그인으로 묶기
- 1. 도구 사용(Tool use)으로 업무 봇 만들기 — API에서 도구를 직접 정의해 부르는 방법
- C7. 커넥터 — 만들어진 커넥터를 쓰는 쪽의 사용법과 보안
- C14. Claude Code 핵심 기능 — MCP 범위, 권한 모드, 훅
출처
마지막 확인일: 2026년 10월 12일
- MCP Python SDK (README) github.com
- MCP Python SDK 문서 py.sdk.modelcontextprotocol.io
- MCP TypeScript SDK (README, v2) github.com
- MCP TypeScript SDK: Build your first server github.com
- Connect Claude Code to tools via MCP code.claude.com
- Get started with custom connectors using remote MCP support.claude.com
- MCP Security Best Practices modelcontextprotocol.io
가격·한도·메뉴 이름은 자주 바뀌어요. 중요한 결정 전에는 위 공식 페이지에서 최신 내용을 다시 확인하세요.