다중 왕복 요청 패턴
다중 왕복 요청 패턴 (Multi Round-Trip Requests, MRTR)
MRTR(Multi Round-Trip Requests) 는 MCP에서 서버가 클라이언트 요청을 처리하는 동안 사용자에게 추가 정보를 요청하는 표준화된 방식을 제공합니다. 서버가 요청을 완료하는 데 필요한 정보(예: roots/list, sampling/createMessage, elicitation/create)를 요청할 때 쓰여요. 이 패턴은 서버 인스턴스 간 공유 저장 계층이나 상태 저장 로드 밸런싱 없이도 그런 서버 요청을 처리할 수 있게 해줍니다.
전체 흐름은 이렇게 동작합니다:
- 클라이언트가 작업 수행에 필요한 파라미터와 함께 초기 요청을 서버로 보낸다.
- 서버는 요청을 처리하는 데 추가 정보가 필요하다고 판단하고, 더 많은 정보를 요청하며 응답한다.
- 클라이언트는 사용자나 다른 소스에서 요청된 정보를 모은 뒤, 추가 정보를 포함해 원래 요청을 재시도한다.
- 서버는 작업 완료에 충분한 정보가 있다고 판단하고 최종 결과로 응답한다.
sequenceDiagram
participant C as Client
participant S as Server
C->>S: client request (id: 1, request params)
note over S: Server needs more info <br/> to process request.
S-->>C: Request for additional input.
note over C: Client gathers input and <br/> retries initial request.
C->>S: client request (id: 2, request params, requested input)
note over S: Server has enough information <br/> to complete the request.
S-->>C: Result (id: 2, result)
참고 (도입 배경): MRTR은 이번 MCP 명세 버전에서 도입됐어요. 서버 시작(server-initiated) 요청을 보내는 이전 방식을 대체합니다. 서버는
roots/list,sampling/createMessage,elicitation/create같은 서버→클라이언트 요청을 반드시(MUST) MRTR 패턴으로 보내야 합니다. 이전의 서버 시작 요청 패턴은 더 이상 지원되지 않으며, 이것은 파괴적 변경(breaking change) 입니다.
참고 (예시 축약): 이 페이지의 요청 예시는 가독성을 위해
_meta요청 메타데이터(io.modelcontextprotocol/protocolVersion,io.modelcontextprotocol/clientInfo,io.modelcontextprotocol/clientCapabilities)를 생략했어요. 모든 요청은 필수_meta필드를 포함해야 합니다(MUST);_meta를 참고하세요.
핵심 타입 (Core Types)
이 흐름은 MCP에서 다음 타입들로 구현됩니다.
InputRequests
InputRequests 객체는 서버-클라이언트 요청들의 맵(map)입니다. 키는 서버가 부여한 문자열 식별자이고, 값은 요청 객체(예: ElicitRequest, CreateMessageRequest, ListRootsRequest)입니다.
{
"github_login": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Please provide your GitHub username",
"requestedSchema": {
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}
}
},
"capital_of_france": {
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "What is the capital of France?"
}
}
],
"systemPrompt": "You are a helpful assistant.",
"maxTokens": 100
}
}
}
InputResponses
InputResponses 객체는 서버 요청에 대한 클라이언트 응답들의 맵입니다. 키는 InputRequests 맵의 키와 대응하고, 값은 각 요청에 대한 클라이언트 결과(예: ElicitResult, CreateMessageResult, ListRootsResult)입니다.
{
"github_login": {
"action": "accept",
"content": {
"name": "octocat"
}
},
"capital_of_france": {
"role": "assistant",
"content": {
"type": "text",
"text": "The capital of France is Paris."
},
"model": "claude-3-sonnet-20240307",
"stopReason": "endTurn"
}
}
InputRequiredResult
InputRequiredResult는 Result의 한 타입으로, 요청을 완료하기 전에 추가 입력이 필요함을 나타냅니다.
inputRequests(선택): 클라이언트가 이행해야 하는 서버 시작 요청들의InputRequests맵.requestState(선택): 서버에게만 의미 있는 불투명 문자열. 클라이언트는 그 내용을 검사, 해석, 수정하거나 어떤 가정도 해선 안 됩니다(MUST NOT).
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
// Elicitation request.
"github_login": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Please provide your GitHub username",
"requestedSchema": {
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}
}
},
// Sampling request.
"capital_of_france": {
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "What is the capital of France?"
}
}
],
"modelPreferences": {
"hints": [{ "name": "claude-3-sonnet" }],
"intelligencePriority": 0.8,
"speedPriority": 0.5
},
"systemPrompt": "You are a helpful assistant.",
"maxTokens": 100
}
}
},
"requestState": "AEAD-protected blob"
}
}
지원되는 요청 (Supported Requests)
서버는 다음 클라이언트 요청에 InputRequiredResult 응답을 보낼 수 있습니다(MAY):
| 클라이언트 요청 | InputRequiredResult 지원 |
|---|---|
prompts/get |
예 |
resources/read |
예 |
tools/call |
예 |
서버는 다른 어떤 클라이언트 요청에도 InputRequiredResult 응답을 보내면 안 됩니다(MUST NOT).
기본 워크플로우 (Basic Workflow)
기본 워크플로우는 서버가 클라이언트-서버 요청의 일부로 클라이언트에서 추가 입력을 요청하는 방법을 설명합니다. 예시로 tools/call을 클라이언트 요청으로 쓰지만, 위에 나열된 지원 요청 어디에도 같은 패턴이 적용돼요.
특히 이 패턴은 서버가 서버 측 상태를 유지하지 않고도 추가 정보를 요청하게 해줍니다. 서버는 필요한 문맥을 requestState 필드에 인코딩하고, 클라이언트는 재시도 때 그 값을 그대로 되돌려 보냅니다.
sequenceDiagram
participant U as User
participant C as Client
participant S as Server
C->>S: tools/call (id: 1)
note over S: Server needs more info via Elicitation
S-->>C: InputRequiredResult (id: 1, ElicitRequest, requestState)
note over C,S: Initial Request Terminated
C->>U: Prompts user for input
U-->>C: Provides responses
note over C: Client retries tool call <br/> with inputResponses and requestState
C->>S: tools/call (id: 2, ElicitResult, requestState)
note over S: Server reconstitutes state<br/>Completes execution
S-->>C: Result (id: 2, ToolCallResult)
각 단계의 요청은 완전히 독립적이라는 점을 기억하세요. 재시도를 처리하는 서버는 재시도 요청에 직접 있는 것 외의 어떤 정보도 필요로 하지 않아요.
서버 요건 (기본 워크플로우)
- 서버는 지원되는 클라이언트 요청 어디에든
InputRequiredResult로 응답할 수 있습니다(MAY). InputRequiredResult는inputRequests필드를 포함할 수 있어요(MAY).inputRequests키는 서버가 부여한 식별자이며 요청 범위 안에서 유일해야 합니다(MUST).inputRequests값은ElicitRequest,CreateMessageRequest,ListRootsRequest중 하나여야 합니다(MUST)
InputRequiredResult는requestState필드를 포함할 수 있어요(MAY). 지정되면 이 필드는 서버에게만 의미 있는 불투명 문자열입니다. 서버는 상태를 어떤 형식으로든(base64 인코딩 JSON, 암호화 JWT, 직렬화된 바이너리) 인코딩할 수 있어요.- 클라이언트 요청에
requestState필드가 있으면 서버는requestState를 공격자 제어 입력으로 취급해야 합니다(MUST).requestState가 인가, 리소스 접근, 비즈니스 로직에 영향을 주면 서버는 그것의 무결성을 보호해야 하며(MUST)(예: HMAC 또는 AEAD), 검증에 실패하는 상태는 거부해야 합니다(MUST). 변조가 요청 실패보다 더 나쁜 것을 일으킬 수 없을 때만 무결성 보호를 생략할 수 있어요(MAY). - 재생을 막기 위해 서버는 무결성 보호된
requestState페이로드 안에 다음을 포함하고, 수신 시 각각 검증해야 합니다(SHOULD):- 인증된 주체(principal) — 다른 주체가 제시한 상태는 거부.
- 짧은 만료(TTL) — 만료 후 제시된 상태는 거부.
- 발원 요청의 식별자 — 예: 메서드 이름과 핵심 파라미터의 다이제스트. 일치하지 않는 요청에 제시된 상태는 거부.
경고: 이런 조치는 재생 창을 제한하고 교차 사용자·교차 요청 재사용을 막지만, 그 자체로 일회 사용을 보장하진 않아요. 주어진
requestState가 최대 한 번만 소비되어야 하는 서버(예: 일회성 상환)는 그 불변식을 서버 측에서 강제해야 합니다(MUST).
- 서버는 모든
InputRequiredResult응답에inputRequests또는requestState중 적어도 하나는 포함해야 합니다(MUST). - 서버는 클라이언트가 역량(capabilities)에서 지원을 선언하지 않은
inputRequests를 보내면 안 됩니다(MUST NOT). 예를 들어 클라이언트가elicitation지원을 선언하지 않았으면 서버는inputRequests필드에 어떤elicitation/create요청도 포함해선 안 됩니다. - 서버는 클라이언트가
inputRequests를 이행하거나 원래 요청을 재시도할 것이라고 가정해서는 안 됩니다(MUST NOT). 서버는 같은 요청에 여러 번InputRequiredResult를 돌려주어, 요청을 완료하는 데 필요한 정보를 얻을 때까지 사용자에게 반복적으로 물어볼 수 있어요(MAY).
클라이언트 요건 (기본 워크플로우)
- 클라이언트가
inputRequests필드를 담은InputRequiredResult를 받으면, 원래 요청을 재시도하기 전에 요청된 입력을 구성해야 합니다(MUST).InputRequiredResult에inputRequests필드가 없으면 클라이언트는 원래 요청을 즉시 재시도할 수 있어요(MAY). InputRequiredResult에requestState필드가 있으면 클라이언트는 원래 요청을 재시도할 때 그 필드의 정확한 값을 그대로 되돌려 보내야 합니다(MUST). 클라이언트는requestState내용을 검사, 해석, 수정하거나 어떤 가정도 해선 안 됩니다(MUST NOT).InputRequiredResult에requestState필드가 없으면 클라이언트는 재시도에 그것을 포함해선 안 됩니다(MUST NOT).- 초기 요청과 재시도는 독립적인 요청이므로 JSON-RPC
id가 서로 달라야 합니다(MUST). inputRequests와requestState두 필드 모두 오직 클라이언트의 원래 요청 재시도에만 영향을 줍니다. 클라이언트가 병렬로 보내는 다른 어떤 요청에도 사용하면 안 됩니다(MUST NOT).
오류 처리 (Error Handling)
서버는 클라이언트가 제공한 데이터가 유효한 InputResponses 객체이고 안의 정보가 올바르게 해석되는지 검증해야 합니다(SHOULD). 프로토콜 오류(잘못된 JSON, 유효하지 않은 스키마, 내부 서버 오류)는 적절한 오류 코드와 메시지로 JSON-RPC 오류 응답을 반환해야 합니다(SHOULD).
InputResponses 객체에 추가적이고 예상치 못한 파라미터가 제공되면, 서버는 인식하지 못하거나 필요 없는 정보를 무시해야 합니다(SHOULD).
이전 InputRequests에서 요청한 정보를 모두 보내지 않았는데, 그 빠진 정보가 서버가 요청을 처리하는 데 필요하면, 서버는 오류를 돌려주기보다 빠진 정보를 다시 요청하는 새 InputRequiredResult로 응답해야 합니다(SHOULD).
보안 고려 (Security Considerations)
requestState는 클라이언트를 통과하므로, 악의적이거나 손상된 클라이언트가 서버 동작을 바꾸거나, 인가 검사를 우회하거나, 서버 로직을 손상시키려고 그것을 수정할 수 있어요. 서버는 위 서버 요건에 설명된 대로 요청 상태를 검증해야 합니다(MUST).
더 알아보기 (Learn more)
- 메시지 패턴 개요 — MRTR이 얹히는 프레임
- 전송 계층 개요 — InputRequiredResult를 실어 나르는 전송
- Streamable HTTP 전송 — HTTP 위에서의 MRTR 왕복
- 구독·알림 패턴 — 변경 알림 스트림