MCP(Model Context Protocol)
없이 손으로 만든 작은 와 로 메시지를 하나씩 따라가고, 에 연결해 설정이 어디에 저장되는지 봅니다.
MCP 란
는 앱을 바깥의 와 에 연결하는 이에요. MCP 가 없던 때는 AI 앱마다, 도구마다 연결을 따로 만들어야 했어요. MCP 로는 를 한 번 만들면 MCP 를 아는 앱 — , , , 내가 만든 — 어디서나 쓸 수 있어요.
MCP 는 를 만드는 회사 이 만들었어요. 공식 문서: https://claude.com/docs/connectors/building/mcp
MCP 클라이언트와 MCP 서버
MCP 는 두 쪽이 이야기해요.
- MCP = : 도구를 골라 쓰는 쪽이에요. 클로드 코드, Codex 같은 AI 에이전트가 MCP 클라이언트가 되어 서버에 연결해요.
- = 자기 기능을 내놓는 쪽: 자기가 할 수 있는 일(기능)을 에이전트가 쓸 수 있게 MCP 로 내놓는 프로그램이에요.
- : 에서 쓰는 이름으로, 에이전트가 들어 있는 AI 앱이에요. 그 앱 안에서 연결한 서버마다 MCP 클라이언트 연결이 하나씩 생겨요.
예: WebMCP
WebMCP 는 웹사이트가 자기 자신을 MCP 처럼 내놓는 방법이에요. 웹브라우저가 제공하는 로, 웹사이트가 자기 기능을 인공지능 에이전트가 쓸 수 있는 도구로 내놓게 해요. 사이트 개발자가 자기 사이트 에 registerTool() 을 넣어 "상품 검색", "장바구니 담기" 같은 기능을 등록하면, 에이전트는 화면을 보고 단추를 하나씩 누르는 대신 그 도구를 바로 불러요.
- 에이전트(MCP 클라이언트): "이 쇼핑몰에서 우산을 찾아 장바구니에 담아 줘"라는 부탁을 받은 AI 예요.
- 웹사이트(MCP 서버 역할): 자기 기능을 도구로 내놓고, 에이전트가 부르면 실행해 결과를 돌려줘요. 웹브라우저는 둘을 잇는 창구예요.
즉 MCP 서버가 된다는 것은 "나를 에이전트가 쓸 수 있게 내놓는다"는 뜻이에요. 다른 MCP 와의 차이는 MCP 활용 탭에서 봐요.
MCP 서버가 내놓는 세 가지
서버가 내놓는 것은 세 가지예요.
- 도구(Tools): AI 이 부를 수 있는 함수예요. "메모 찾기", "줄 하나 더하기" 같은 것이에요.
- : 앱이 읽을 수 있는 데이터예요. 파일이나 기록 같은 것이고, 저마다 주소()가 있어요.
- (Prompts): 사용자가 골라 쓰는, 미리 만든 메시지 틀이에요.
쉽게 이해하기
- 이렇게 생각해 보세요
- MCP 는 AI 앱의 USB 단자와 같아요. USB 가 없던 때는 프린터와 키보드마다 플러그가 달랐지만, 이제는 USB 플러그만 있으면 어느 컴퓨터에나 꽂혀요. MCP 서버는 기기, AI 앱은 컴퓨터이고, 플러그 모양이 같으니까 서버를 한 번 만들면 MCP 를 아는 모든 앱이 쓸 수 있어요.
- 예시
- strange-add 서버는 도구 하나를 내놓아요. 두 수를 더하고 1~100 사이의 무작위 수를 더 얹어요.
- 한 줄로 클로드 코드에 연결해요.
- "strange-add 로 2 와 3 을 더해 줘"라고 하면 AI 가 도구 설명을 읽고, 내 허락을 묻고, 클라이언트가 서버의 strange-add 를 불러요. 답이 5 가 아니에요 — 도구가 정말 돌았다는 증거예요.
- 자주 하는 실수
- 찾은 MCP 서버를 아무거나 설치하는 것. 서버는 에서 내 으로 코드를 돌려요. 직접 읽어 보거나 믿을 수 있는 것을 고르고, 꼭 필요한 폴더나 키만 주세요.
에이전트에 MCP 서버 연결하기
는 에 연결해야 쓸 수 있어요. 연결 ( · codex )과 범위·확인 방법은 툴마다 달라서 각 툴 화면의 MCP 단원에 한 번씩 적어 두었어요.
WebMCP vs Chrome DevTools MCP vs Playwright MCP
셋 다 에이전트가 웹브라우저에서 일하게 해요. 다른 점은 누가 준비하느냐와 에이전트가 어떻게 일하느냐예요. WebMCP 는 웹사이트 쪽에서 "내 사이트에서는 이런 기능을 쓸 수 있다"고 선언하는 이고, 나머지 둘은 에이전트 쪽에 설치해 아무 사이트에서나 브라우저를 직접 다루는 MCP 서버예요.
| 항목 | WebMCP | Chrome DevTools MCP | Playwright MCP |
|---|---|---|---|
| 무엇인가 | 웹사이트가 쓰는 브라우저 API — 아직 제안 단계의 웹 표준(Web 커뮤니티 그룹 초안) | 에이전트 쪽에 설치하는 MCP 서버(Google 팀) | 에이전트 쪽에 설치하는 MCP 서버(Microsoft) |
| 누가 준비하나 | 웹사이트 개발자 — 자기 사이트 에 registerTool() 을 넣어 를 등록해요 | 에이전트를 쓰는 사람 — 내 에이전트에 를 더해요 | 에이전트를 쓰는 사람 — 내 에이전트에 서버를 더해요 |
| 에이전트가 일하는 방법 | 사이트가 등록한 도구를 이름으로 불러요. 화면을 읽고 추측할 필요가 없어요. | 실제 Chrome 을 로 조종하고 들여다봐요 — 클릭 · 입력 · . | 페이지의 접근성 트리(화면 구조)를 읽고 클릭 · 입력해요 — 그림이 아니라 구조를 봐요. |
| 잘하는 일 | 그 사이트의 기능을 정확하고 빠르게, 인된 내 그대로 써요 | — 성능 기록, 요청, | · — · · WebKit 여러 브라우저 |
| 한계 | 사이트가 도구를 등록한 곳에서만 돼요. 새 표준이라 지원하는 브라우저 · 사이트가 아직 적어요. | Chrome(과 Chrome for Testing)에 맞춰져 있어요. 화면이 바뀌면 다시 찾아야 해요. | 화면 구조가 바뀌면 다시 찾아야 해요. 사이트가 정한 기능이 아니라 단추 하나하나로 일해요. |
시작하는 코드
// WebMCP: the website registers its own feature as a tool
// (first explainer / Chrome preview: navigator.modelContext,
// spec draft 2026-10-27: document.modelContext)
const modelContext = document.modelContext ?? navigator.modelContext;
modelContext.registerTool({
name: "search-products",
description: "Search this shop's products by keyword",
inputSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
async execute({ query }) {
const response = await fetch("/api/products?q=" + encodeURIComponent(query));
return await response.json(); // the agent gets this result
},
});
# Chrome DevTools MCP: add the server to your agent (Claude Code)
claude mcp add chrome-devtools -- npx -y chrome-devtools-mcp@latest
# Playwright MCP: add the server to your agent (Claude Code)
claude mcp add playwright -- npx @playwright/mcp@latest
WebMCP 는 사이트의 에, 나머지 둘은 의 에 적어요. WebMCP 의 처음 안내와 Chrome 미리보기는 navigator.modelContext 를, 2026-10-27 초안은 document.modelContext 를 써요 — 규격이 아직 바뀌는 중이에요.
비유로 보기 — 메뉴판을 내건 가게와 심부름 로봇
WebMCP = 가게가 내건 주문 창구와 메뉴판
- 가게(웹사이트)가 문 앞에 "주문은 이 창구로 하세요"라는 메뉴판을 미리 내걸어요.
- 메뉴판에는 주문할 수 있는 것(도구 이름)과 주문서 쓰는 법(입력 )이 적혀 있어요.
- 로봇(에이전트)은 메뉴판을 읽고 창구에 주문서만 넣으면 돼요 — 가게 안을 돌아다니지 않아요.
- 다만 창구를 만들어 둔 가게에서만 이렇게 주문할 수 있어요.
Chrome DevTools MCP · Playwright MCP = 손님이 보낸 심부름 로봇
- 손님(에이전트를 쓰는 사람)이 심부름 로봇을 가게에 보내요. 가게는 아무것도 준비하지 않아도 돼요.
- 로봇은 가게 안을 직접 둘러보며 진열대를 읽고, 물건을 집고, 계산대 단추를 눌러요.
- 어느 가게든 들어갈 수 있지만, 진열이 바뀌면 다시 찾아야 해서 느리고 헤맬 수 있어요.
- Chrome DevTools MCP 로봇은 가게의 배선 · 계량기까지 보는 점검 기사예요(성능 · 네트워크 · 콘솔).
- Playwright MCP 로봇은 정해 준 장보기 순서를 여러 가게(브라우저)에서 똑같이 되풀이하는 장보기 로봇이에요.
정리하면, 창구가 있는 가게에서는 창구로 주문하는 것이 빠르고 정확해요. 창구가 없는 가게에서는 로봇이 직접 둘러봐야 해요. 그래서 셋은 서로를 대신하지 않고 함께 써요.
MCP 안전 수칙
안전: 도구는 내 으로 돌아가는 코드이고, 믿을 수 있는 서버가 아니면 도구 설명도 믿으면 안 돼요. 규격은 가 도구를 부르기 전에 사용자의 허락을 받으라고 해요. 직접 읽어 봤거나 믿을 수 있는 서버만 설치하고, 서버에는 꼭 필요한 만큼만 접근을 주고, 키는 코드가 아니라 환경 변수에 두고, 을 조심하세요 — 도구가 읽은 웹 페이지나 파일에 를 노린 지시가 숨어 있을 수 있어요.
메시지와 전송 방식
메시지는 2.0 이에요. ("무엇을 할 수 있어?"), ("이 값으로 이 일을 해 줘") 같은 작은 이에요. 메시지는 두 가지 표준 중 하나로 오가요. stdio 는 가 같은 컴퓨터에서 을 직접 켜고 표준 입력·출력으로 이야기하는 방식이고, 는 서버가 에서 돌아서 여러 사용자가 접속하는 방식이에요.
손으로 만든 서버와 클라이언트
서버(mcp-server.py)는 만 써서 자체가 그대로 보여요. 표준 입력에서 JSON-RPC 메시지를 한 줄에 하나씩 읽고, 표준 출력에 답을 한 줄에 하나씩 써요.
- : 클라이언트가 프로토콜 을 들고 인사해요. 서버는 같은 버전으로 답하고, 가 있다고 알리고, 자기 이름을 밝혀요.
- tools/list: 서버가 하나뿐인 도구 strange-add 를 설명과 입력 (a 와 b 는 숫자)와 함께 돌려줘요.
- tools/call: 서버가 strange-add 를 실행하고 결과를 글자로 돌려줘요.
- id 가 없는 메시지는 (notifications/initialized 같은 것)이라 서버가 답하지 않아요. 그 밖의 요청(ping 같은 것)에는 빈 결과로 답해요.
strange-add 는 a + b 에 1~100 사이의 무작위 수를 더해요. 은 이 값을 스스로 알아낼 수 없으니, 2 + 3 의 답이 5 가 아니면 도구가 정말 실행됐다는 증거예요. stdout 으로는 프로토콜 메시지만 오가요 — () 한 줄만 잘못 들어가도 대화가 깨지니 기록은 stderr 에 쓰세요. print(..., =True) 는 답을 바로 내보내요.
클라이언트(mcp-client.sh)는 없이 역할을 대신하는 스크립트예요. 서버를 켜고 전체(initialize, initialized 알림, tools/list, tools/call, ping)를 보낸 뒤 답을 하나씩 검사해 PASS 나 FAIL 을 찍어요. 종료 0 은 모두 통과했다는 뜻이에요. 에 연결하기 전에 서버를 이렇게 시험하세요. 실제 클라이언트에서는 모델이 이렇게 함께 돌아요.
- 사용자의 질문과 도구 목록을 모델에 보내요.
- 모델이 도구를 부르겠다고 하면 클라이언트가 tools/call 로 서버에서 그 도구를 불러요.
- 결과를 모델에 돌려주면 모델이 마지막 답을 써요.
이 손으로 만든 서버는 initialize 로 시작하는 방식(프로토콜 버전 2025-06-18)으로 말하고, 같은 클라이언트는 이 방식도 받아들여요. 최신 (2026-07-28)은 initialize 대신 요청마다 버전을 실어 보내요. 실제 서비스를 만들 때는 규격을 알아서 맞춰 주는 공식 (Python 의 MCPServer, )를 쓰세요.
"""
mcp-server.py: the smallest MCP server.
One tool, plain Python (standard library only), no SDK.
Who is who
MCP server This program. It offers tools (functions the AI
may use).
Host The app the user talks to (Claude Code, Codex).
It runs one MCP client for each connected server.
MCP client The part of the host that talks to this server
with JSON-RPC messages (here over stdio: one JSON
message per line).
Model The LLM (Claude Opus, GPT, ...). It never talks to
the server directly. It only talks to the host.
Flow
1. Discover tools
The client asks for the tools (tools/list). The server replies
with each tool's name, description and input schema.
2. Offer tools
The host sends the user's message to the model, together
with the tool list.
3. Model decides
The model answers directly, or asks for a tool call
(tool name + arguments). It never runs the tool itself.
4. Call the tool
The client sends the request to the server (tools/call).
5. Run the tool
The server runs the function and returns the result.
6. Model continues
The host gives the result back to the model. The model may
call more tools (3-6 again) or write the final answer.
Model Host (MCP client) MCP server
| | 1. tools/list ------> |
| | <------ tool list |
| <-- 2. message + tool list |
| 3. tool call --> | |
| | 4. tools/call ------> |
| | <------ 5. result |
| <-- 6. result | |
Rule for stdio servers: stdout carries protocol messages only.
Never print() anything else there. Write logs to stderr.
"""
import json
import random
import sys
SERVER_INFO = {"name": "strange-add-mcp", "version": "1.0.0"}
# The tool. The model reads the description to decide when to use it.
STRANGE_ADD = {
"name": "strange-add",
"description": "Add two numbers with a strange number.",
"inputSchema": {
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"},
},
"required": ["a", "b"],
},
}
def strange_add(a, b):
"""Add a + b and a random number from 1 to 100 (always > a + b).
The model cannot guess the answer, so a right answer proves
that the tool was really called.
"""
return a + b + random.randint(1, 100)
def handle(method, params):
"""Return the result for one request. Others (like ping) get {}."""
if method == "initialize":
# Use the client's protocol version and say "I have tools".
return {
"protocolVersion": params["protocolVersion"],
"capabilities": {"tools": {}},
"serverInfo": SERVER_INFO,
}
if method == "tools/list":
return {"tools": [STRANGE_ADD]}
if method == "tools/call":
args = params["arguments"]
total = strange_add(args["a"], args["b"])
return {"content": [{"type": "text", "text": str(total)}]}
return {}
def main():
# One JSON-RPC message per line comes in, one reply per line goes out.
for line in sys.stdin:
request = json.loads(line)
# No id means a notification: send no reply.
if "id" not in request:
continue
result = handle(request["method"], request.get("params", {}))
reply = {"jsonrpc": "2.0", "id": request["id"], "result": result}
# flush=True sends the reply at once.
print(json.dumps(reply), flush=True)
if __name__ == "__main__":
main()
#!/bin/sh
# mcp-client.sh: the simplest MCP client, a shell script.
# It starts mcp-server.py, talks to it over stdio (one JSON-RPC
# message per line) and checks every reply.
# Exit code 0 means every check passed.
SERVER="python3 $(dirname "$0")/mcp-server.py"
# 1. Send the whole session at once and keep the replies.
# id 1 initialize agree on a version
# (no id) notifications/initialized "ready" (no reply)
# id 2 tools/list which tools are there?
# id 3 tools/call run strange-add(2, 3)
# id 4 ping is the server alive?
replies=$(
$SERVER <<'EOF'
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "shell-mcp-client", "version": "1.0.0"}}}
{"jsonrpc": "2.0", "method": "notifications/initialized"}
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "strange-add", "arguments": {"a": 2, "b": 3}}}
{"jsonrpc": "2.0", "id": 4, "method": "ping"}
EOF
)
echo "--- Server replies ---"
echo "$replies"
echo "--- Checks ---"
failed=0
# 2. check <label> <reply id> <text the reply must contain>
check() {
reply=$(echo "$replies" | grep -F "\"id\": $2,")
if echo "$reply" | grep -qF "$3"; then
echo "PASS $1"
else
echo "FAIL $1 (expected: $3)"
failed=1
fi
}
check "initialize: protocol version" 1 '"protocolVersion": "2025-06-18"'
check "initialize: server info" 1 '"serverInfo": {'
check "initialize: has tools" 1 '"capabilities": {"tools": {}}'
check "tools/list: strange-add" 2 '"name": "strange-add"'
check "ping: empty result" 4 '"result": {}'
# 3. strange-add adds a random 0 to 100,
# so the answer only has to be >= 2 + 3.
total=$(echo "$replies" | grep -F '"id": 3,' |
sed 's/.*"text": "\([0-9]*\)".*/\1/')
if [ "$total" -ge 5 ] 2>/dev/null; then
echo "PASS tools/call: strange-add(2, 3) >= 5 (got $total)"
else
echo "FAIL tools/call: strange-add(2, 3) >= 5 (got $total)"
failed=1
fi
exit $failed
# put the two files above into one folder
mkdir -p ~/tmp/mcp && cd ~/tmp/mcp
# mcp-server.py mcp-client.sh
# 1) test the server without any AI
sh mcp-client.sh # -> 6 x PASS, exit code 0
# 2) connect it to Claude Code (no --scope: the "local" scope)
claude mcp add strange-add -- python3 ~/tmp/mcp/mcp-server.py
claude mcp list # strange-add: ... - Connected
claude mcp get strange-add # Scope: Local config
# 3) where it was saved: NOT in this folder (no new file here)
# ~/.claude.json -> projects -> "<this folder>" -> mcpServers
jq '.projects["'"$PWD"'"].mcpServers' ~/.claude.json
# 4) use it: start Claude Code in THIS folder
claude # "Use strange-add to add 2 and 3"
# -> not 5 (e.g. 61): the tool ran
claude mcp remove strange-add -s local
이 은 이 페이지가 아니라 나 서버의 에서 실행해요. 실행하기 전에 한 줄씩 읽어 보세요.