에이전트가 내 DB 를 지웠다 · 표준 라이브러리로 짜는 stdio 서버 · 실습
표준 라이브러리만으로 MCP stdio 서버를 만든다
목표
json·sys·sqlite3 만으로 MCP 서버를 한 편 씁니다. 클라이언트가 보내는initialize → notifications/initialized → tools/list → tools/call 을 순서대로
받아 내고, 마지막에는 그 서버를 자식 프로세스로 띄우는 클라이언트까지 씁니다.
왜 중요한가
SDK 를 쓰면 다섯 줄로 끝나는 일입니다. 그런데 사고는 SDK 가 감춰 준 자리에서
납니다 — stdout 에 찍은 디버그 한 줄이 클라이언트의 JSON 파서를 죽이고, 알림에
답을 보내서 짝이 안 맞는 응답이 생기고, 도구가 실패했는데 프로토콜 오류로
돌려보내서 모델이 "서버가 고장났다" 고 판단합니다. 프레이밍(줄 하나 = 메시지
하나), id 로 짝 맞추기, 알림 무시, 오류 두 종류의 구분 — 이 네 가지가 MCP 의
실체이고, 손으로 한 번 짜 보면 SDK 의 로그가 무슨 말을 하는지 읽힙니다.
단계
1. /root/mcp/seed.sql 을 저장하고 /root/mcp/shop.db 로 적재하세요. customers 5행, orders 8행이어야 합니다.
2. /root/mcp/server.py 를 만드세요. stdin 에서 한 줄씩 JSON-RPC 요청을 읽고, initialize 에 protocolVersion 이 2025-06-18 이고 capabilities.tools 와 serverInfo.name 이 있는 result 로 답해야 합니다.
3. 같은 파일에서 알림(id 없는 메시지)에는 답하지 않고, 모르는 메서드는 -32601, 깨진 JSON 은 -32700 오류(id 는 null)로 답하게 하세요. 서버가 죽으면 안 됩니다.
4. tools/list 에 도구 두 개를 돌려주세요 — list_customers 와 count_orders(inputSchema.properties.status, 문자열, required). 각 도구에 description 과 type: object 인 inputSchema 가 있어야 합니다.
5. tools/call 을 구현하세요. count_orders 에 {"status":"paid"} 를 주면 DB 의 paid 건수가 content[0].text 에 들어 있어야 하고, list_customers 는 다섯 고객 이름을 모두 담아야 합니다. DB 경로는 환경변수 MCP_DB 가 있으면 그것을, 없으면 /root/mcp/shop.db 를 씁니다.
6. 오류를 두 종류로 나누세요. 없는 도구 이름은 JSON-RPC 오류 -32602, count_orders 에 모르는 상태값(예: banana)을 주면 result 에 isError: true 와 설명 텍스트를 돌려주어야 합니다.
7. /root/mcp/client.py 를 만드세요. 서버를 subprocess 로 띄워 initialize → notifications/initialized → tools/list → tools/call(count_orders, paid) 을 보내고, 결과를 /root/mcp/session.json 에 tools(이름 목록)와 paid_orders(응답 텍스트)로 적습니다. 환경변수 MCP_SESSION_OUT 이 있으면 그 경로에 씁니다.
8. 서버의 모든 로그를 stderr 로 보내세요. 요청마다 메서드 이름이 stderr 에 한 줄씩 찍히고, stdout 에는 JSON 응답 외에 아무것도 나오면 안 됩니다.
참고
- 한 줄씩 읽는 루프는
for line in sys.stdin:이면 됩니다. 응답은sys.stdout.write(json.dumps(...) + "\n")뒤에 반드시flush()하세요 — 파이프는 줄 버퍼링이 아니라서 flush 없이는 클라이언트가 영원히 기다립니다. - 알림인지는
"id" in msg로 판단합니다.msg.get("id")가 None 인지로 보면, id 가 실제로 null 인 (잘못된) 요청과 구분되지 않습니다. json.loads가 던지는 예외를 잡지 않으면 깨진 줄 하나에 서버가 통째로 죽습니다. 클라이언트는 그것을 "서버가 사라졌다" 로 봅니다.- 손으로 시험하려면:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | python3 /root/mcp/server.py - 흔한 실수 1:
print()로 디버그 문장을 stdout 에 찍는 것. 8단계 채점이 그것을 잡습니다. - 흔한 실수 2: 없는 도구를
isError: true로 돌려주는 것. 그것은 도구가 실행되다 실패한 것이 아니라 요청 자체가 잘못된 것이라 프로토콜 오류입니다.
단계 8개
- 가게 DB 를 만든다
- initialize 에 답한다
- 알림에는 답하지 않는다
- 도구 목록을 낸다
- 도구를 실제로 실행한다
- 오류는 두 종류다
- 클라이언트로 한 세션을 돈다
- stdout 은 프로토콜 전용이다