리소스 목록이 비었다면 템플릿 목록도 확인한다. resources/list가 빈 배열을 돌려줘도, 서버가 resources/templates/list로 매개변수형 리소스를 제공할 수 있다. 템플릿에서 실제 URI를 만든 다음 resources/read로 읽는 경로가 남아 있다. 두 목록과 읽기 요청을 확인하기 전에는 “서버에 자료가 없다”고 결론내리지 않는다. 공식 Python SDK 문서의 리소스 설명도 고정 URI와 템플릿을 별도 목록으로 구분한다.
적용 범위. 확인일은 2026년 10월 9일 KST, 기준은 MCP revision 2026-07-28이다. 아래 코드는 SDK 호출이 아닌 JSON-RPC 메시지의 가상 예제다. 특정 SDK 패키지 버전이나 ChatGPT·Claude·Cursor 화면에서의 지원 여부를 검증한 글은 아니다. 실제 서버에 요청을 보내거나 장애를 재현하지 않았다.
리소스가 안 보일 때 확인할 지점
같은 “자료가 안 보인다”에도 확인할 위치가 다르다. 먼저 서버가 resources capability를 제공하는지, 지금 보고 있는 것이 클라이언트 화면인지 프로토콜 응답인지 구분한다. 아래 표는 문서에 근거한 권장 진단 순서다.
| 관찰한 상태 | 먼저 확인할 것 | 이 단계에서 할 수 있는 판단 |
|---|---|---|
| 화면에 리소스 메뉴가 없다 | 호스트의 리소스 표시·첨부 기능과 서버 응답을 구분한다 | 화면만으로 서버의 리소스 유무를 판정할 수 없다 |
resources/list가 비었다 | resources/templates/list를 조회한다 | 매개변수형 리소스 제공 여부를 아직 확인하지 않았다 |
| 템플릿은 보이지만 읽기가 실패한다 | 템플릿 원문, 변수 값, 확장 후 URI, 호출한 서버를 대조한다 | 표시용 이름과 읽기 URI를 혼동했는지 좁힐 수 있다 |
| 계정을 바꾸면 목록이 다르다 | 현재 인증 범위와 캐시의 소유 범위를 확인한다 | 목록은 요청자의 권한에 따라 달라질 수 있다 |
| 읽기 응답은 왔지만 본문이 안 보인다 | resultType과 contents의 각 항목을 확인한다 | 추가 입력 대기 또는 바이너리 결과를 빈 텍스트로 오인했을 수 있다 |
리소스 UI는 호스트가 결정하며, 프로토콜이 특정 메뉴 모양을 정하지 않는다. 또한 권한에 따라 목록이 달라질 수 있다. 다른 계정의 토큰을 빌리거나 접근 제한을 풀어 확인하는 방식은 피한다. 리소스 상호작용 모델과 capability 규정이 이 구분의 근거다.
목록의 name이 아니라 URI를 읽는다
고정 리소스에는 uri, 템플릿에는 uriTemplate이 있다. name이나 title은 사람이 항목을 식별하는 데 쓰는 이름이다. resources/read의 params.uri에는 고정 URI 또는 템플릿을 확장한 URI를 넣는다. 템플릿 이름을 넣거나 중괄호를 남겨 두면 원하는 리소스를 지정하지 못한다. ResourceTemplate과 ReadResourceRequestParams를 대조하면 필드 차이가 분명해진다.
다음 예제는 서버가 가상의 안내 문서를 제공한다고 가정한다. URI는 구조를 설명하기 위한 식별자이며 실제 서비스 주소가 아니다. 먼저 템플릿 목록을 요청한다. protocolVersion과 clientCapabilities는 각 요청의 필수 메타데이터다. clientInfo는 권장 필드이며, 아래 이름과 버전은 가상 클라이언트 표기다. 요청 메타데이터 규격
{
"jsonrpc": "2.0",
"id": 21,
"method": "resources/templates/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "resource-example",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
서버가 다음처럼 응답했다고 가정한다. ttlMs와 cacheScope도 포함했다. 이 revision의 해당 완료 응답에는 캐시 힌트가 필요하다. 이 예제의 0은 즉시 오래된 것으로 취급하라는 뜻이고, private 결과는 다른 인증 범위와 공유하지 않는다. 캐싱 규격
{
"jsonrpc": "2.0",
"id": 21,
"result": {
"resultType": "complete",
"resourceTemplates": [
{
"uriTemplate": "guide://articles/{articleId}",
"name": "article-guide",
"description": "Read one example guide by its article ID",
"mimeType": "text/plain"
}
],
"ttlMs": 0,
"cacheScope": "private"
}
}
조회 권한이 있는 문서의 ID가 intro-42라고 확인됐다면, 다음 요청의 params.uri처럼 채운다. article-guide라는 이름이나 템플릿 원문 자체를 보내지 않는다. ID는 실제 업무에서 확인한 값이어야 하며, 임의로 번호를 바꾸며 다른 자료를 탐색하지 않는다.
{
"jsonrpc": "2.0",
"id": 22,
"method": "resources/read",
"params": {
"uri": "guide://articles/intro-42",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "resource-example",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
위 두 요청은 JSON-RPC 본문만 보여 준다. Streamable HTTP에서는 같은 MCP 엔드포인트로 POST하고, Content-Type: application/json과 JSON·SSE를 수용하는 Accept 헤더를 사용한다. MCP-Protocol-Version은 본문의 revision, Mcp-Method는 각 메서드와 일치해야 한다. 이 예제처럼 헤더에 안전한 ASCII URI라면 Mcp-Name에 params.uri를 그대로 넣는다. 안전하게 표현할 수 없는 값은 규격의 Base64 sentinel 형식으로 인코딩하며, 디코딩한 값이 본문과 같아야 한다. 인증은 승인된 기존 흐름을 유지한다. HTTP 헤더 규격
슬래시가 든 변수는 단순 치환하면 달라진다
실제 템플릿은 단순한 ID만 받지 않을 수 있다. RFC 6570에서 {name}과 {+name}은 같은 확장 방식이 아니다. 예를 들어 값 안의 슬래시는 전자에서 인코딩되고, 후자의 reserved expansion에서는 보존된다. 공백은 둘 다 퍼센트 인코딩된다. RFC 6570 단순 문자열 확장과 예약 문자 확장을 따른 가상 비교다.
value = "setup/first steps"
guide://articles/{name}
-> guide://articles/setup%2Ffirst%20steps
guide://articles/{+name}
-> guide://articles/setup/first%20steps
이 차이를 없애려고 서버가 돌려준 템플릿을 임의로 바꾸면 안 된다. 템플릿 원문과 원래 변수 값을 보존하고, RFC 6570을 지원하는 확장 기능의 결과를 대조한다. 모든 문자를 한 번 더 URL 인코딩하는 방식도 피한다. 예컨대 이미 만들어진 %2F를 다시 인코딩하면 %252F가 되어 다른 식별자를 만들 수 있다. 복잡한 템플릿은 단순 문자열 replace로 일반화하지 않는다.
URI는 서버가 해석하는 식별자다
위 예제의 사용자 정의 URI를 웹 브라우저나 HTTP fetch에 그대로 전달해도 MCP 읽기를 대신하지 못한다. resources/read에서 URI를 어떻게 해석할지는 그 서버의 몫이다. file scheme도 반드시 클라이언트 컴퓨터의 실제 파일 경로를 뜻하지 않는다. HTTPS 리소스는 직접 읽을 수 있는 경우가 있지만, scheme만 보고 인증 정보와 함께 임의의 URL을 가져오는 규칙으로 확장하지 않는다. 공통 URI scheme 규정
서버 측에서는 허용된 리소스 범위와 호출자의 권한을 읽기 직전에 확인해야 한다. 템플릿 형식에 맞는다는 이유만으로 모든 문서에 접근할 수 있게 만들지 않는다. 클라이언트 진단 기록에는 서버 구분, 메서드, 가린 URI, 변수의 인코딩 상태와 오류 코드를 남기는 편이 유용하다. 토큰과 실제 비공개 문서 내용은 공개 로그에 넣지 않는다.
읽기 성공 뒤에도 contents를 확인한다
resources/read의 완료 결과 객체에는 contents 배열이 들어 있다. 첫 항목의 text만 읽는 구현은 뒤의 항목이나 blob 형태의 바이너리를 놓칠 수 있다. 각 항목의 URI, MIME type, text 또는 blob을 구분해 처리한다. 다음은 텍스트 한 건이 돌아온 경우의 가상 응답이다. ReadResourceResult 스키마
{
"jsonrpc": "2.0",
"id": 22,
"result": {
"resultType": "complete",
"contents": [
{
"uri": "guide://articles/intro-42",
"mimeType": "text/plain",
"text": "Example guide content."
}
],
"ttlMs": 0,
"cacheScope": "private"
}
}
응답이 resultType: "input_required"라면 아직 최종 본문을 받은 상태가 아니다. 요청된 입력을 승인된 범위에서 처리한 뒤, 재요청에는 대응하는 inputResponses와 서버가 제공한 경우의 requestState를 반영한다. 재요청은 새 JSON-RPC ID를 사용하며, requestState를 해석하거나 수정하지 않는다. Multi Round-Trip 규격
리소스가 존재하지 않으면 2026-07-28 규격은 JSON-RPC -32602 오류를 요구한다. 다만 이 코드는 다른 잘못된 매개변수에도 쓰이므로 코드만 보고 “없는 URI”로 단정하지 않는다. 오류 설명과 대상 URI를 함께 본다. 구형 서버의 리소스 없음 코드 -32002도 호환 처리 대상이며, 빈 contents 배열을 “존재하지 않음”의 표준 응답처럼 쓰면 안 된다. 리소스 오류 처리 규정
수정 후에는 이 네 가지를 따로 기록한다
① 두 목록의 조회가 끝났는지, ② 확인된 템플릿으로 URI를 만들었는지, ③ 같은 서버·인증 범위에서 최종 읽기 결과를 받았는지, ④ 호스트가 필요한 내용을 실제 문맥에 넣었는지를 구분해 남긴다. 프로토콜 읽기 성공만으로 AI가 그 자료를 답변에 사용했다고 결론낼 수는 없다.
목록 응답에 nextCursor가 있으면 해당 목록의 다음 페이지도 확인해야 한다. 페이지와 캐시 문제가 남아 있다면 MCP 목록의 nextCursor 진단을 함께 읽는다. 초기화 방식이 서로 다르면 2026-07-28 discovery 모델, HTTP 단계에서 거절되면 헤더·본문 불일치 진단이 앞선 점검에 해당한다.
검증 범위. 공식 규격과 가상 메시지의 구조를 대조한 진단 안내다. 본문의 JSON 파싱과 예시 URI 인코딩은 로컬에서 확인했지만, 특정 MCP 서버·SDK·제품 UI에서의 재현과 수정 성공은 검증하지 않았다. 템플릿이 보인다는 사실도 해당 문서의 존재와 읽기 권한을 보증하지 않는다. 최종 판정에는 실제 허용된 대상의 읽기 응답이 필요하다.