LabHub
배우기 러닝패스 코스

エージェントが私のDBを消した

標準ライブラリだけでMCP stdioサーバーを作る

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

json·sys·sqlite3 만으로 MCP 서버를 한 편 씁니다. 클라이언트가 보내는 initializenotifications/initializedtools/listtools/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 요청을 읽고, initializeprotocolVersion2025-06-18 이고 capabilities.toolsserverInfo.name 이 있는 result 로 답해야 합니다.
  3. 같은 파일에서 알림(id 없는 메시지)에는 답하지 않고, 모르는 메서드는 -32601, 깨진 JSON 은 -32700 오류(id 는 null)로 답하게 하세요. 서버가 죽으면 안 됩니다.
  4. tools/list 에 도구 두 개를 돌려주세요 — list_customerscount_orders(inputSchema.properties.status, 문자열, required). 각 도구에 descriptiontype: objectinputSchema 가 있어야 합니다.
  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.jsontools(이름 목록)와 paid_orders(응답 텍스트)로 적습니다. 환경변수 MCP_SESSION_OUT 이 있으면 그 경로에 씁니다.
  8. 서버의 모든 로그를 stderr 로 보내세요. 요청마다 메서드 이름이 stderr 에 한 줄씩 찍히고, stdout 에는 JSON 응답 외에 아무것도 나오면 안 됩니다.

참고

가게 DB 를 만든다

/root/mcp/seed.sql 을 저장하고 /root/mcp/shop.db 로 적재하세요. customers 5행, orders 8행이어야 합니다.

sqlite3 는 sqlite3 shop.db < seed.sql 로 파일을 통째로 실행합니다. 파이썬으로 하려면 sqlite3.connect(...).executescript(open(...).read()) 입니다. 이미 있는 DB 에 다시 적재하면 테이블이 있다는 오류가 나니 먼저 지우세요.

initialize 에 답한다

/root/mcp/server.py 를 만드세요. stdin 에서 한 줄씩 JSON-RPC 요청을 읽고, initializeprotocolVersion2025-06-18 이고 capabilities.toolsserverInfo.name 이 있는 result 로 답해야 합니다.

뼈대 예시의 루프에서 method == "initialize" 일 때 result 딕셔너리를 만들어 reply(msg["id"], result=...) 로 보내면 됩니다. result 에는 protocolVersion, capabilities(tools 키를 가진 객체), serverInfo(name·version) 세 가지가 들어갑니다.

알림에는 답하지 않는다

같은 파일에서 알림(id 없는 메시지)에는 답하지 않고, 모르는 메서드는 -32601, 깨진 JSON 은 -32700 오류(id 는 null)로 답하게 하세요. 서버가 죽으면 안 됩니다.

JSON-RPC 에서 알림은 id 멤버가 없는 요청이고 서버는 답하면 안 됩니다. json.loads 를 try 로 감싸 JSONDecodeError 면 id null 로 -32700 을 보내고 continue 하세요. 나머지 예외도 잡아 -32603 으로 답하면 서버가 살아남습니다.

도구 목록을 낸다

tools/list 에 도구 두 개를 돌려주세요 — list_customerscount_orders(inputSchema.properties.status, 문자열, required). 각 도구에 descriptiontype: objectinputSchema 가 있어야 합니다.

result 는 {"tools": [...]} 이고 도구 하나는 name·description·inputSchema 세 키입니다. inputSchema 는 JSON Schema 객체라 "type": "object"properties 를 갖습니다. 인자가 없는 도구도 properties: {} 는 둡니다.

도구를 실제로 실행한다

tools/call 을 구현하세요. count_orders{"status":"paid"} 를 주면 DB 의 paid 건수가 content[0].text 에 들어 있어야 하고, list_customers 는 다섯 고객 이름을 모두 담아야 합니다. DB 경로는 환경변수 MCP_DB 가 있으면 그것을, 없으면 /root/mcp/shop.db 를 씁니다.

params 는 {"name": 도구이름, "arguments": {...}} 입니다. 결과는 {"content": [{"type": "text", "text": "..."}]} 모양이어야 합니다. os.environ.get("MCP_DB", "/root/mcp/shop.db") 로 경로를 잡고 요청마다 sqlite3.connect 해서 닫으세요.

오류는 두 종류다

오류를 두 종류로 나누세요. 없는 도구 이름은 JSON-RPC 오류 -32602, count_orders 에 모르는 상태값(예: banana)을 주면 result 에 isError: true 와 설명 텍스트를 돌려주어야 합니다.

스펙은 '모르는 도구·잘못된 인자' 를 프로토콜 오류(error 멤버, 예시 코드 -32602)로, '도구가 돌다 실패한 것' 을 도구 실행 오류(result 안의 isError: true)로 구분합니다. 후자는 모델이 읽고 다시 시도할 수 있게 텍스트로 이유를 적습니다.

클라이언트로 한 세션을 돈다

/root/mcp/client.py 를 만드세요. 서버를 subprocess 로 띄워 initialize → notifications/initialized → tools/list → tools/call(count_orders, paid) 을 보내고, 결과를 /root/mcp/session.jsontools(이름 목록)와 paid_orders(응답 텍스트)로 적습니다. 환경변수 MCP_SESSION_OUT 이 있으면 그 경로에 씁니다.

subprocess.Popen([...], stdin=PIPE, stdout=PIPE, text=True) 로 띄우고, 보낼 때마다 stdin.flush(), 받을 때는 stdout.readline() 한 줄을 json.loads 합니다. 알림을 보낸 뒤에는 readline 하지 마세요 — 답이 오지 않으니 영원히 기다립니다. 끝낼 때는 stdin 을 닫고 wait() 합니다.

stdout 은 프로토콜 전용이다

서버의 모든 로그를 stderr 로 보내세요. 요청마다 메서드 이름이 stderr 에 한 줄씩 찍히고, stdout 에는 JSON 응답 외에 아무것도 나오면 안 됩니다.

sys.stderr.write(...) 또는 print(..., file=sys.stderr) 입니다. 스펙(stdio 전송)은 서버가 stdout 에 유효한 MCP 메시지 외의 것을 써서는 안 되고, stderr 는 로그 용도로 써도 된다고 정합니다. 채점기는 stdout 의 모든 줄을 JSON 으로 파싱해 봅니다.