stdio 전송 가이드

stdio 전송 가이드

stdio 전송에서는 클라이언트가 MCP 서버를 서브프로세스로 실행해요. 양쪽 끝은 서브프로세스의 표준 스트림을 통해 통신합니다.

  • 서버는 stdin에서 JSON-RPC 메시지를 읽고 stdout으로 JSON-RPC 메시지를 씁니다.
  • 각 메시지는 하나의 JSON-RPC 요청, 알림, 또는 응답입니다.
  • 메시지는 줄바꿈(newline)으로 구분되며, 임베디드 줄바꿈을 포함해선 안 됩니다(MUST NOT).
  • 서버는 정보성·디버그·오류 메시지 등 어떤 로깅 목적으로든 stderr에 UTF-8 문자열을 쓸 수 있습니다(MAY).
  • 클라이언트는 서버의 stderr 출력을 캡처하거나, 전달하거나, 무시할 수 있으며, stderr 출력이 오류 조건을 뜻한다고 가정해서는 안 됩니다(SHOULD NOT).
  • 서버는 유효한 MCP 메시지가 아닌 어떤 것도 stdout에 쓰면 안 됩니다(MUST NOT).
  • 클라이언트는 유효한 MCP 메시지가 아닌 어떤 것도 서버의 stdin에 쓰면 안 됩니다(MUST NOT).

표준 스트림이 정석 채널이지만, 이 바인딩에서 표준 스트림에 의존하는 부분은 프로세스 수명주기뿐이에요. 와이어 형식(신뢰할 수 있는 양방향 바이트 스트림 위에서 줄 하나당 하나의 줄바꿈 구분 JSON-RPC 메시지)은 Unix 도메인 소켓, TCP 연결, 또는 이와 유사한 어떤 채널에서도 그대로 동작합니다. 이런 스트림 위에 만드는 커스텀 전송은 이 페이지의 프레이밍과 메시지 규칙을 재사용하는 것이 좋아요(SHOULD). 서브프로세스 특화 부분(실행, stderr, 스트림을 닫아 종료, 프로세스 재시작)만 채널별로 대응하는 방식이 필요합니다.

출처: MCP 공식 문서 — stdio Transport

메시지 보내기 (Sending Messages)

클라이언트는 서버의 stdin에 JSON-RPC 요청알림을 줄 하나씩 써서 메시지를 보냅니다. 클라이언트는 JSON-RPC 응답을 써선 안 됩니다(MUST NOT).

메시지 받기 (Receiving Messages)

클라이언트는 stdout에서 서버 메시지를 줄 하나씩 읽어요. 모든 메시지가 이 단일 채널을 공유하며, 요청별 스트림(per-request stream)은 존재하지 않습니다.

서버는 세 종류의 메시지를 씁니다:

  1. 클라이언트 요청에 대한 응답 — JSON-RPC id로 상호 대응합니다.
  2. 진행 중인 요청과 관련된 알림 — 예: notifications/progress, notifications/message.
  3. 활성화된 subscriptions/listen 요청을 위해 전달되는 알림 — 클라이언트는 _metaio.modelcontextprotocol/subscriptionId 필드로 이들을 대응해야 합니다(MUST); SubscriptionsListenRequest를 참고하세요.

서버는 stdout에 JSON-RPC 요청을 쓰면 안 됩니다(MUST NOT). 서버→클라이언트 상호작용은 InputRequiredResult 응답으로 전달되며, Multi Round-Trip Requests를 참고하세요.

요청 메타데이터 (Request Metadata)

stdio 전송의 모든 요청 메타데이터는 JSON-RPC 메시지 본문에 인라인으로 실립니다. 프로토콜 버전, 요청별 역량, 선택적 클라이언트 정체성은 _meta.io.modelcontextprotocol/*에 있고, 메서드 이름과 인자는 JSON-RPC가 정해 둔 자리에 있어요. 헤더 레이어는 없습니다.

취소 (Cancellation)

진행 중인 요청을 취소하려면 클라이언트가 해당 요청의 ID를 참조하는 notifications/cancelled 알림을 보내야 합니다(MUST). stdio는 단일 공유 양방향 채널이라 닫을 요청별 스트림이 없기 때문이에요. 서버는 취소된 요청에 대한 작업을 가능한 빨리 멈추고(SHOULD), 그 요청에 대해 더 이상 메시지를 보내지 않아야 합니다(MUST NOT). 전체 규칙은 Cancellation을 참고하세요.

종료 (Shutdown)

클라이언트는 다음과 같이 종료를 시작하는 것이 좋습니다(SHOULD):

  1. 하위 프로세스(서버)로 가는 입력 스트림을 닫는다.
  2. 서버가 종료될 때까지 기다린다.
  3. 합리적인 시간 안에 서버가 종료되지 않으면, 운영체제에 적절한 메커니즘으로 프로세스를 강제 종료한다.

POSIX 시스템에서 강제 종료는 보통 SIGTERM에서 SIGKILL로 확대됩니다. POSIX 신호를 쓸 수 없는 Windows에서는 클라이언트가 TerminateProcessJob Objects를 사용할 수 있어요.

서버는 표준 입력이 닫히거나 읽기가 파일 끝(end-of-file)을 돌려주면 신속하게 종료하는 것이 좋습니다(SHOULD). 이것이 기본 우아한 종료 신호이자 유일한 이식 가능한 신호라서, 이 신호를 존중하면 강제 종료의 필요성이 줄어듭니다.

서버는 클라이언트로 가는 출력 스트림을 닫고 종료함으로써 종료를 시작할 수도 있습니다(MAY).

예상치 못한 종료 (Unexpected Termination)

서버 프로세스가 예상치 못하게 종료되면 클라이언트는 이를 재시작해야 합니다(SHOULD). 프로토콜이 무상태(stateless)라 진행 중이던 요청은 그저 유실되고, 클라이언트는 새 프로세스를 상대로 재시도하면 돼요. 활성 subscriptions/listen 스트림도 재시작 후 다시 수립해야 합니다.

하위 호환성 (Backward Compatibility)

현대(요청별 메타데이터) MCP 버전과 initialize 핸드셰이크가 필요한 레거시 버전을 모두 지원하는 클라이언트는, 다른 요청을 보내기 전에 server/discover로 프로브(probe)하고 _meta에 선호하는 현대 버전을 설정하는 것이 좋습니다(SHOULD). 프로브는 세 가지 결과가 가능해요:

  • 서버가 DiscoverResult를 돌려주면: 현대 서버입니다. supportedVersions에서 서로 지원하는 버전을 골라 계속 진행하세요.
  • 서버가 UnsupportedProtocolVersionError 같은 인지된 현대 JSON-RPC 오류를 돌려주면: 현대 서버지만 요청한 버전을 지원하지 않는 경우입니다. 서버가 광고한 supported 목록에서 버전을 하나 사용하세요. initialize로 폴백하면 안 됩니다.
  • 서버가 다른 오류를 돌려주거나 합리적인 타임아웃 안에 응답하지 않으면: 레거시 서버입니다. initialize 핸드셰이크로 폴백하세요.

폴백을 특정 오류 코드 하나에 키우면 안 됩니다(MUST NOT). 레거시 서버는 initialize 이전에 오는 미지의 요청에 구현 정의 오류(보통 -32601 또는 -32602)로 응답하거나 아예 응답하지 않기 때문이에요.

현대 버전만 지원하는 클라이언트는 프로브할 필요가 없지만, 프로브는 여전히 권장(RECOMMENDED) 됩니다. 일부 레거시 서버는 initialize 이후에 요청이 온다는 것을 검증하지 않아, 시대가 모호한 메서드(예: tools/call)를 레거시 의미로 처리할 수 있기 때문이에요. 프로브는 대신 결정적인 실패를 만들어 냅니다.

시대 모델과 구현자용 호환성 매트릭스는 Versioning: Backward Compatibility를 참고하세요.

더 알아보기 (Learn more)