GrepBoard/매뉴얼
51 편 ·

MCP 서버의 print 와 Connection closed 구분하기

·Claude Code 2.1.302 · Python 3.12.3 · mcp 2.3.0 · @modelcontextprotocol/sdk 1.32.1 · Node.js 24 · Ubuntu 24.04·8 단계

파이썬 stdio MCP 서버에 print 를 넣으면 어떻게 되는지 세 가지로 나눠 재현하고, MCP error -32000 의 실제 원인과 로그를 stderr 로 돌리는 방법을 정리합니다.

MCP error -32000: Connection closed 를 보고 서버의 print() 를 의심하는 경우가 많지만, 직접 돌려 보면 print 는 이 오류의 원인이 아니라 증상 옆에 붙어 나오는 경우가 대부분입니다. 같은 서버를 클라이언트 두 종류로 붙여 차이를 확인합니다.

[1] 도구 하나짜리 서버를 만듭니다. MODE 환경 변수로 print 방식을 바꿀 수 있게 합니다.

console
$ python3 -m venv .venv && .venv/bin/pip install mcp
/srv/hello/server.py
import osfrom mcp.server.mcpserver import MCPServer mcp = MCPServer("hello")MODE = os.environ.get("MODE", "") @mcp.tool()def add(a: int, b: int) -> int:    """Add two numbers"""    if MODE == "flush":        print(f"add called: {a} {b}", flush=True)    if MODE == "noeol":        print(f"add called: {a} {b}", end="", flush=True)    return a + b if __name__ == "__main__":    if MODE == "flush":        print("starting hello server", flush=True)    if MODE == "noeol":        print("starting hello server", end="", flush=True)    mcp.run(transport="stdio")

[2] 규칙부터 봅니다. MCP 사양의 stdio 절은 메시지를 줄바꿈으로 구분하고, 서버에 대해 「MUST NOT write anything to its stdout that is not a valid MCP message」, 로그는 「MAY write UTF-8 strings to its standard error (stderr)」 라고 적습니다.

[3] Claude Code 에 붙여 봅니다. 프로젝트 범위로 등록한 뒤 claude -p 로 도구를 부릅니다(-p 는 승인 창 없이 --mcp-config 의 서버를 씁니다).

console
$ claude mcp add --scope project hello -- /srv/hello/.venv/bin/python /srv/hello/server.py$ claude -p "Use the add tool to compute 2+3 and answer with the number only" \    --mcp-config .mcp.json --strict-mcp-config --max-turns 3 --allowedTools mcp__hello__add5

MODE 없이 print 만 넣은 서버도 같은 결과였습니다. 파이프로 연결된 stdout 은 파이썬이 모아 두었다가 프로세스가 끝날 때 내보내므로, 서버가 도는 동안은 아무것도 섞이지 않습니다. flush=True 나 PYTHONUNBUFFERED=1 이면 그때부터 섞입니다.

[4] MODE=flush 로 줄바꿈 있는 print 를 즉시 내보냅니다. claude -p 는 그대로 5 이고 디버그 로그(--debug-file)에도 연결 실패가 없었습니다. TypeScript SDK 1.32.1 클라이언트로 붙이면 이렇게 나옵니다.

output
transport error: Unexpected token 's', "starting h"... is not valid JSONresult: 5

JSON 이 아닌 줄은 오류로 알리기만 하고 건너뜁니다. 파이썬 SDK 클라이언트(2.3.0)도 Invalid JSON 을 찍고 result: 5 를 돌려줍니다.

[5] MODE=noeol 은 줄바꿈 없는 print(..., end="") 입니다. 이 줄에 다음 JSON 메시지가 이어 붙어 한 줄이 통째로 깨집니다. 파이썬 SDK 가 본 줄은 이랬습니다.

output
Invalid JSON: expected value at line 1 column 1 [type=json_invalid, input_value='starting hello server{"j..."hello","version":""}}}', ...

initialize 응답이 이 줄에 붙어 사라진 것입니다. TypeScript SDK 는 MCP error -32001: Request timed out 으로 끝났고(-32000 이 아닙니다), 파이썬 SDK 는 ExceptionGroup 을 던졌습니다. Claude Code 2.1.302 는 첫 시도가 응답을 못 받자 한 번 더 시도해 연결했고(디버그 로그의 version negotiation probe timed out), 연결 시간이 1.3초에서 3.2초로 늘었습니다.

[6] -32000 Connection closed 는 서버 프로세스가 끝났다는 뜻입니다. 시작 도중 죽는 서버로 재현합니다. 이 경우 print 는 죽기 전에 한 줄을 남겼을 뿐입니다.

/srv/hello/server_exit.py
import sysprint("loading config...")import nonexistent_module
output
transport error: Unexpected token 'l', "loading config..." is not valid JSONERR: MCP error -32000: Connection closed

Claude Code 의 디버그 로그에는 이렇게 남았습니다.

output
[ERROR] MCP server "hello" Connection failed (CONNECTION_CLOSED): Connection closed

「is not valid JSON」 줄은 곁다리이고, 서버가 죽은 진짜 이유(ModuleNotFoundError)는 stderr 에만 있습니다. 클라이언트가 stderr 를 버리면 보이지 않습니다.

[7] 진짜 원인을 보려면 서버 명령을 터미널에서 직접 실행합니다. 입력을 주지 않으면 서버는 stdin 을 기다리기만 하므로, 시작 오류가 있으면 바로 트레이스백이 나옵니다.

console
$ /srv/hello/.venv/bin/python /srv/hello/server_exit.pyloading config...Traceback (most recent call last):  File "/srv/hello/server_exit.py", line 3, in <module>    import nonexistent_moduleModuleNotFoundError: No module named 'nonexistent_module'

[8] 로그는 stderr 로 보냅니다. print 마다 file=sys.stderr 를 붙이거나 logging 을 씁니다(logging.basicConfig 의 기본 출력이 stderr 입니다).

python
import sysprint("add called", file=sys.stderr)

stderr 판 서버를 TypeScript SDK 클라이언트(stderr 를 터미널로 연결)로 돌린 결과입니다. 오류 없이 도구가 성공하고, 로그는 따로 보입니다.

output
starting hello serveradd called: 2 3result: 5

시작 중 죽는 서버도 stderr 로그를 쓰면 Connection closed 와 함께 ModuleNotFoundError 트레이스백이 보입니다.

#mcp#python#stdio#troubleshooting