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로 나와서, 이 목록만으로는 드러나지 않았습니다.
증상
claude mcp add는Added stdio MCP server …를 내고 끝납니다. 경로가 틀려도 똑같았습니다.claude mcp list또는claude mcp get <이름>에서✘ Failed to connect가 보이고,get에는Issue:줄이 붙습니다.Issue:문구는 이번 재현에서 두 가지였습니다.CONNECTION_CLOSED: Connection closed와MCP server "<이름>" connection timed out after 30000ms입니다.
실측 환경
| 항목 | 값 |
|---|---|
| OS | Windows 11 Home (10.0.26200) |
| 셸 | Git Bash (프로세스 확인만 PowerShell 7) |
| Node.js | v24.19.0 |
| Claude Code | 2.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이 아닌 줄마다 onerror로 SyntaxError를 알렸습니다. 한 줄짜리 로그일 때는 연결과 호출이 그대로 이어졌습니다. 응답이 붙어 버린 경우에는 연결 단계에서 멈췄습니다(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을 더럽히는 d2logtool과 d2nonltool은 ✔ Connected였습니다. 상태 확인 때 Claude Code가 어떤 요청까지 보내는지는 확인하지 않았습니다. 이 목록이 ✔ Connected여도 도구 호출에서 깨지는 서버가 있을 수 있다는 것까지가 이번 결과입니다.
고쳐서 복구되는지
19번째 줄이 process.stdout.write('add-server starting ');인 server_fix.mjs를 d2fix로 등록했습니다. 고치기 전에는 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-RPC | SDK 클라이언트 | 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 |
d2logstart | JSON 아닌 줄 1개 + 응답 정상 | onerror 1회, 호출 성공 | ✔ Connected |
d2logtool | 호출 때 JSON 아닌 줄 1개 + 응답 정상 | onerror 1회, 호출 성공 | ✔ Connected |
d2stderr | 로그는 stderr로, 응답 정상 | 정상 | ✔ Connected |
d2nonlstart | initialize 응답 줄이 깨져 id=1 못 받음 | 연결 10초 초과 | ✘ Failed to connect / connection timed out after 30000ms |
d2nonltool | tools/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: 줄에 보여 준다고 설명합니다.
실패한 것
- 처음 일곱 개를 등록할 때 셸 인용을 잘못 써서 인자가 모두
C:\Users\User\AppData\Local\Temp\longtail_D2\srv$f로 들어갔습니다. 이때도claude mcp add는 일곱 번 모두Added …로 끝났습니다. 전부 지우고 경로를 풀어 다시 등록했습니다. - 줄바꿈이 있는
console.log한 줄로는claude mcp list에서 실패를 재현하지 못했습니다. stdout 로그 변형 중에서 연결 실패가 난 것은 시작 시 줄바꿈 없이process.stdout.write를 넣은 경우뿐이었습니다. - 정리 단계에서 남은 node 프로세스를 찾는 첫 명령은 인용이 깨져 제대로 돌지 않았습니다. 스크립트 파일로 다시 돌려
longtail_D2가 들어간 node 프로세스가 0개임을 확인했습니다.
확인하지 않은 것
- 대화형 Claude Code 세션에서
d2logtool·d2nonltool의 도구를 Claude가 실제로 불렀을 때의 동작과 오류 문구. /mcp화면의 표시, 서버 stderr가 Claude Code 어디에 기록되는지.MCP_TIMEOUT값을 바꿨을 때 30000ms 문구가 달라지는지.- 설치된 SDK 1.30.0 외 다른 버전의 클라이언트가 JSON이 아닌 줄을 어떻게 다루는지. Python 등 다른 언어 서버.
- 여러 줄에 걸친 로그(객체를 펼쳐 찍는 경우)나 JSON처럼 생긴 로그.
- 명령 프롬프트(cmd.exe)·PowerShell에서의 등록, 맥·리눅스.