AI 해결 노트 · 2026-09-17 · 실측 2026-09-17

MCP 서버가 ✘ Failed to connect일 때 — 잘못된 실행 경로(CONNECTION_CLOSED)와 stdout 오염(connection timed out) 구분하기

한 줄로

저희 재현(Claude Code 2.1.274, 윈도우)에서 없는 파일을 등록하면 claude mcp list✘ Failed to connect — CONNECTION_CLOSED: Connection closed가 나왔습니다. 없는 실행 파일을 등록했을 때도 claude mcp get의 문구가 같았습니다. 서버가 stdout에 줄바꿈 없이 글자를 찍어 initialize 응답 줄을 망가뜨리면 connection timed out after 30000ms가 나왔습니다. 한 줄짜리 console.log를 넣은 서버는 두 경우 모두(시작 시, 도구 호출 때) ✔ Connected로 나와서, 이 목록만으로는 드러나지 않았습니다.

증상

실측 환경

항목
OSWindows 11 Home (10.0.26200)
Git Bash (프로세스 확인만 PowerShell 7)
Node.jsv24.19.0
Claude Code2.1.274 (Claude Code)
서버@modelcontextprotocol/sdk 1.30.0(npm ls로 확인)으로 만든 더하기 도구 서버. 코드 전문은 최소 MCP 서버 글에 실었고, 이 글의 원자료에도 cat server.mjs 출력으로 남겼습니다
프로젝트 폴더C:\Users\User\AppData\Local\Temp\longtail_D2\proj2, 모든 서버를 --scope project로 추가
승인claude --settings '{"enableAllProjectMcpServers": true}' mcp …

재현에 쓴 서버 변형

정상 server.mjs에 로그 줄을 넣은 변형과, 등록 인자나 명령만 틀리게 한 경우입니다.

등록 이름무엇을 바꿨나
d2ok바꾸지 않음
d2missing인자를 없는 파일 no_such_server.mjs
d2nocmd명령을 없는 실행 파일 C:/nope/node.exe
d2logstart연결 전에 console.log('add-server starting');
d2logtool도구 안에서 console.log('add called', a, b);
d2stderr위 두 줄을 console.error
d2nonlstart연결 전에 process.stdout.write('add-server starting '); (줄바꿈 없음)
d2nonltool도구 안에서 process.stdout.write('add called '); (줄바꿈 없음)

도구 안에 로그를 넣은 변형은 이런 모양입니다(diff server.mjs server_log_tool.mjs).

14,16c14,17
<   async ({ a, b }) => ({
<     content: [{ type: 'text', text: String(a + b) }]
<   })
---
>   async ({ a, b }) => {
>     console.log('add called', a, b);
>     return { content: [{ type: 'text', text: String(a + b) }] };
>   }

경우 1 — 실행 경로가 틀렸을 때

등록 명령은 경로를 확인하지 않고 성공했습니다.

$ claude mcp add d2missing --scope project -- node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/no_such_server.mjs
Added stdio MCP server d2missing with command: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/no_such_server.mjs to project config

승인을 켜고 상태를 보면 이렇습니다.

d2missing:
  Scope: Project config (shared via .mcp.json)
  Status: ✘ Failed to connect
  Issue: CONNECTION_CLOSED: Connection closed
  Type: stdio
  Command: node
  Args: C:/Users/User/AppData/Local/Temp/longtail_D2/srv/no_such_server.mjs

실행 파일 자체를 틀린 d2nocmd(Command: C:/nope/node.exe)도 Issue: CONNECTION_CLOSED: Connection closed로 문구가 같았습니다. 이 문구만으로는 파일이 없는지 명령이 없는지 가릴 수 없었습니다.

원인은 등록된 명령을 직접 돌려 보면 보입니다. 없는 파일을 테스트 스크립트로 띄웠을 때 node는 stderr에 이렇게 남기고 곧바로(92ms) 종료 코드 1로 끝났습니다.

Error: Cannot find module 'C:\Users\User\AppData\Local\Temp\longtail_D2\srv\no_such_server.mjs'
    at Module._resolveFilename (node:internal/modules/cjs/loader:1517:15)
    …
  code: 'MODULE_NOT_FOUND',
  requireStack: []
}

Node.js v24.19.0
[child exit 92ms] code=1 signal=null

공식 SDK 클라이언트(Client + StdioClientTransport)로 붙였을 때는 McpError: MCP error -32000: Connection closed가 났습니다. claude mcp get d2missing은 약 4.2초 만에 끝났습니다.

없는 실행 파일은 모습이 달랐습니다. Git Bash에서 직접 치면 bash: line 1: C:/nope/node.exe: No such file or directory(종료 코드 127)였고, node의 spawn으로 띄우면 프로세스가 시작되지 않은 채 error 이벤트로 ENOENT spawn C:/nope/node.exe ENOENT가 왔습니다. 그런데도 Claude Code의 Issue: 문구는 없는 파일과 같았습니다.

경우 2 — 서버가 stdout에 일반 글을 찍을 때

MCP 사양의 stdio 절(modelcontextprotocol.io/specification/2025-06-18/basic/transports, 2026-09-17 확인)은 "The server MUST NOT write anything to its stdout that is not a valid MCP message."라고 적고, 로그는 stderr에 쓸 수 있다고 합니다. 서버 만들기 문서(modelcontextprotocol.io/docs/develop/build-server, 같은 날 확인)의 TypeScript 부분도 "Never use console.log()"라고 하고, 대신 console.error()를 쓰라고 합니다.

실제로 무엇이 깨지는지 세 가지 방법으로 봤습니다.

직접 JSON-RPC(`rpc_test.mjs`, JSON이 아닌 줄은 표시만 하고 넘김)

$ node rpc_test.mjs server_log_start.mjs
[send 12ms] initialize
[stdout 1] (NOT-JSON) add-server starting
[stdout 2] (JSON) {"result":{"protocolVersion":"2025-06-18",…},"jsonrpc":"2.0","id":1}
…
[stdout 4] (JSON) {"result":{"content":[{"type":"text","text":"5"}]},"jsonrpc":"2.0","id":3}

$ node rpc_test.mjs server_write_nonl_start.mjs
[send 11ms] initialize
[stdout 1] (NOT-JSON) add-server starting {"result":{"protocolVersion":"2025-06-18",…},"jsonrpc":"2.0","id":1}
[timeout 10021ms] id=1 응답 없음(10초)

console.log는 줄바꿈까지 찍으므로 로그가 따로 한 줄을 차지했고, 뒤의 JSON 응답은 온전했습니다. 줄바꿈 없는 process.stdout.write는 로그 뒤에 응답이 같은 줄로 붙어, 그 줄 전체가 JSON이 아니게 됐습니다. 이 스크립트는 id=1 응답을 끝내 받지 못했습니다.

공식 SDK 클라이언트(설치된 1.30.0)

# server_log_start.mjs
[client onerror] SyntaxError: Unexpected token 'a', "add-server starting" is not valid JSON
[connect] ok
[listTools] add
[callTool] [{"type":"text","text":"5"}]

# server_write_nonl_start.mjs
[client onerror] SyntaxError: Unexpected token 'a', "add-server"... is not valid JSON
[catch] Error: connect 10초 초과

SDK 클라이언트는 JSON이 아닌 줄마다 onerrorSyntaxError를 알렸습니다. 한 줄짜리 로그일 때는 연결과 호출이 그대로 이어졌습니다. 응답이 붙어 버린 경우에는 연결 단계에서 멈췄습니다(10초는 저희가 건 제한). 도구 안에서 줄바꿈 없이 찍은 server_write_nonl.mjs는 연결과 listTools까지 되고 callTool에서 10초 제한에 걸렸습니다.

Claude Code 상태 표시

$ claude --settings '{"enableAllProjectMcpServers": true}' mcp list
Checking MCP server health…
d2ok: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server.mjs - ✔ Connected
d2missing: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/no_such_server.mjs - ✘ Failed to connect — CONNECTION_CLOSED: Connection closed
d2logstart: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server_log_start.mjs - ✔ Connected
d2logtool: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server_log_tool.mjs - ✔ Connected
d2stderr: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server_log_stderr.mjs - ✔ Connected
d2nonlstart: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server_write_nonl_start.mjs - ✘ Failed to connect — MCP server "d2nonlstart" connection timed out after 30000ms
d2nonltool: node C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server_write_nonl.mjs - ✔ Connected

(다른 범위 서버 줄은 걸러 냈습니다.) 일곱 개를 한 번에 확인한 이 명령은 57.8초 걸렸고, claude mcp get d2nonlstart 한 건은 33.6초 걸렸습니다. 나머지 get은 4~6초였습니다.

도구 호출 때만 stdout을 더럽히는 d2logtoold2nonltool✔ Connected였습니다. 상태 확인 때 Claude Code가 어떤 요청까지 보내는지는 확인하지 않았습니다. 이 목록이 ✔ Connected여도 도구 호출에서 깨지는 서버가 있을 수 있다는 것까지가 이번 결과입니다.

고쳐서 복구되는지

19번째 줄이 process.stdout.write('add-server starting ');server_fix.mjsd2fix로 등록했습니다. 고치기 전에는 Issue: MCP server "d2fix" connection timed out after 30000ms였습니다. 19번째 줄 한 줄만 바꿨습니다.

19:process.stdout.write('add-server starting ');   ← 고치기 전
19:console.error('add-server starting');           ← 고친 뒤

고친 뒤 직접 호출에서는 로그가 [stderr] add-server starting으로 따로 나오고 세 응답이 모두 JSON이었습니다. claude mcp get d2fix도 바로 이어서 돌렸고 결과는 이렇습니다.

d2fix:
  Scope: Project config (shared via .mcp.json)
  Status: ✔ Connected
  Type: stdio
  Command: node
  Args: C:/Users/User/AppData/Local/Temp/longtail_D2/srv/server_fix.mjs

결과

서버직접 JSON-RPCSDK 클라이언트claude mcp list/get
d2ok세 응답 정상정상✔ Connected
d2missing응답 없음, node 종료 코드 1(MODULE_NOT_FOUND)MCP error -32000: Connection closed✘ Failed to connect / CONNECTION_CLOSED: Connection closed
d2nocmd(node spawn) 프로세스 시작 안 됨, ENOENT돌리지 않음get: ✘ Failed to connect / CONNECTION_CLOSED: Connection closed
d2logstartJSON 아닌 줄 1개 + 응답 정상onerror 1회, 호출 성공✔ Connected
d2logtool호출 때 JSON 아닌 줄 1개 + 응답 정상onerror 1회, 호출 성공✔ Connected
d2stderr로그는 stderr로, 응답 정상정상✔ Connected
d2nonlstartinitialize 응답 줄이 깨져 id=1 못 받음연결 10초 초과✘ Failed to connect / connection timed out after 30000ms
d2nonltooltools/call 응답 줄이 깨져 id=3 못 받음callTool 10초 초과✔ Connected
d2fix(고친 뒤)세 응답 정상돌리지 않음✔ Connected

이번 재현에서 본 구분은 이렇습니다. CONNECTION_CLOSED는 없는 파일(직접 실행하면 node가 곧바로 종료 코드 1)과 없는 실행 파일(직접 실행하면 시작 자체가 ENOENT) 두 경우에서 나왔습니다. connection timed out after 30000ms는 시작 로그가 initialize 응답과 같은 줄에 붙는 서버에서 나왔고, 직접 호출에서도 그 줄은 JSON으로 읽히지 않았습니다. CONNECTION_CLOSED를 보면 등록된 명령을 그대로 셸에 쳐 보고, 시간 초과를 보면 서버를 직접 JSON-RPC로 불러 stdout 줄을 확인하는 순서가 이번 결과와 맞습니다. 공식 문서(code.claude.com/docs/en/mcp, 2026-09-17 확인)는 ✘ Failed to connect일 때 claude mcp get <name>이 실패 상세를 Issue: 줄에 보여 준다고 설명합니다.

실패한 것

확인하지 않은 것

함께 보기