ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • MCP Resources 완전 분해: URI로 AI에게 데이터를 공급하는 7가지 메서드
    IT 2026. 6. 12. 23:00
    MCP Resources 완전 분해: URI로 AI에게 데이터를 공급하는 7가지 메서드

    지난 글에서 MCP의 세 기본 단위(Tools·Resources·Prompts)를 큰 그림으로 훑었다. 이번 글은 그 중 Resources를 뼈대까지 분해한다. Resources는 5가지 요청 메서드 + 2가지 알림으로 구성되며, "AI가 외부 데이터를 어떻게 읽는가"라는 문제를 URI(Uniform Resource Identifier) 기반 접근 모델로 푼다.

    먼저 가장 흔한 오해부터 짚는다. Tools로도 파일을 읽을 수 있는데 왜 Resources가 따로 필요한가?

    Tools vs Resources — 같은 파일, 다른 목적

    Tools의 read_file 도구와 Resources의 resources/read는 둘 다 파일 내용을 돌려주지만, MCP 명세가 둘을 분리한 이유가 있다. 핵심 차이는 누가 트리거하느냐어떻게 쓰이느냐다.

    diagram

    다이어그램 설명: Tool 경로에서는 모델이 "read_file 써줘"라고 먼저 결정하고, 결과를 도구 응답으로 받는다. 모델이 실행을 트리거한다. Resource 경로에서는 클라이언트가 모델이 추론을 시작하기 전에 데이터를 미리 읽어서 context에 삽입한다 — 모델은 파일이 이미 거기 있는 상태에서 대화를 시작한다. 이 차이가 두 경로를 분리한 이유다.

    정리하면 이렇다. Tool은 "모델이 실행을 결정하는 동적 행동"이고, Resource는 "클라이언트가 context를 채우는 정적 데이터 공급"이다. 어떤 데이터를 쓸지 모델이 스스로 판단해야 한다면 Tool, 대화 시작 전에 미리 알려줄 수 있다면 Resource가 맞다. 실무에서 둘을 혼용하는 경우가 많지만, Resource를 올바르게 쓰면 모델의 불필요한 "도구 호출 → 읽기" 왕복을 줄여 응답이 빨라진다.

    diagram
    그림: Resources의 7가지 메서드 — 왼쪽 5개는 요청(응답 있음), 오른쪽 2개는 알림(응답 없음)

    다이어그램 설명: Resources는 요청 5개와 알림 2개로 구성된다. 요청은 클라이언트가 서버에 보내고 응답을 받는다. 알림은 서버가 클라이언트에게 일방향으로 보내며 id가 없어 응답을 기대하지 않는다. 이 7가지가 Resources의 전부다. 각각을 순서대로 분해해 보자.

    resources/list — "이 서버에 무엇이 있나"

    Resources를 사용하려면 먼저 무엇이 있는지 알아야 한다. resources/list는 서버가 현재 노출하는 모든 정적 리소스의 목록을 돌려준다. "정적"이란 URI가 고정되어 있는 리소스를 뜻한다 — 특정 파일, 특정 DB 테이블처럼.

    // 요청 (params 없음 — 단순 목록 조회)
    {
      "jsonrpc": "2.0",
      "id": 1,
      "method": "resources/list"
    }
    
    // 응답
    {
      "jsonrpc": "2.0",
      "id": 1,
      "result": {
        "resources": [
          {
            "uri":         "file:///workspace/src/main.c",  // 리소스를 가리키는 고유 식별자
            "name":        "main.c",                        // 사람이 읽기 좋은 이름
            "description": "네트워크 연결 모듈 진입점",
            "mimeType":    "text/x-csrc"                    // 콘텐츠 형식 힌트 (선택)
          },
          {
            "uri":         "sqlite:///build.db?table=errors",
            "name":        "build_errors",
            "description": "최근 빌드 오류 로그",
            "mimeType":    "application/json"
          },
          {
            "uri":         "https://docs.internal/api-spec",
            "name":        "API 명세",
            "description": "REST API OpenAPI 스펙",
            "mimeType":    "application/yaml"
          }
        ]
      }
    }

    코드 설명: uri가 핵심 필드다. 클라이언트는 이 값을 resources/read에 그대로 넘긴다. URI 스키마(file://·sqlite://·https://)는 MCP 명세가 강제하지 않는다 — 서버가 자유롭게 정의한다. 클라이언트는 URI를 opaque string(불투명 문자열)으로 취급하면 되고, 스키마를 해석할 필요 없다. mimeType은 클라이언트가 내용을 어떻게 렌더링할지 결정하는 힌트다 — 없어도 동작하지만 있으면 IDE가 구문 강조를 미리 준비할 수 있다.

    응답에 nextCursor 필드가 있으면 목록이 더 있다는 신호다. 서버가 대량의 리소스를 노출하면 한 번에 다 돌려주지 않고 페이지로 나눈다. resources/list?cursor=로 다음 페이지를 요청한다. 전형적인 파일시스템 기반 MCP 서버라면 수백 개의 파일이 있을 수 있어 페이지네이션이 실용적이다.

    resources/templates/list — 파라미터로 결정되는 동적 리소스

    모든 리소스의 URI가 미리 고정되어 있는 건 아니다. "빌드 작업 번호 42의 로그"는 번호가 바뀌는 동적 리소스다. 이런 리소스를 위해 MCP는 URI 템플릿(RFC 6570 — URL 패턴을 변수로 표현하는 표준)을 별도로 노출한다.

    diagram

    다이어그램 설명: resources/templates/list로 패턴을 먼저 받고, 실제 값(job_id=42)을 채워서 resources/read를 호출하는 2단계 흐름이다. 클라이언트가 {job_id}를 채울 값을 모른다면 completion/complete로 서버에게 후보를 물어볼 수 있다 — 이것이 Resources·Prompts와 자동완성 메서드의 연결 지점이다.

    // resources/templates/list 응답
    {
      "jsonrpc": "2.0",
      "id": 2,
      "result": {
        "resourceTemplates": [
          {
            "uriTemplate":  "log://build/{job_id}",           // {job_id}가 파라미터
            "name":         "build_log",
            "description":  "빌드 작업별 실시간 로그",
            "mimeType":     "text/plain"
          },
          {
            "uriTemplate":  "db://users/{user_id}/profile",
            "name":         "user_profile",
            "description":  "사용자 프로필 레코드",
            "mimeType":     "application/json"
          },
          {
            "uriTemplate":  "git://commits/{branch}/{sha}",   // 복수 파라미터도 가능
            "name":         "git_commit",
            "description":  "특정 커밋의 diff",
            "mimeType":     "text/x-diff"
          }
        ]
      }
    }

    코드 설명: uriTemplate{변수명}은 RFC 6570 레벨 1 변수다. 클라이언트는 실제 값으로 대입해서 완성된 URI를 만들고 resources/read에 넘긴다. 예를 들어 log://build/{job_id}job_id=42를 넣으면 log://build/42가 된다. 서버는 이 패턴을 파싱해서 실제 데이터를 가져오는 로직을 갖고 있다 — 클라이언트는 URI 스키마의 의미를 몰라도 된다.

    resources/read — URI를 넘기면 내용이 온다

    resources/read는 Resources의 본론이다. URI 하나를 보내면 그 리소스의 실제 내용이 contents 배열로 온다. 텍스트와 바이너리 둘 다 처리한다.

    // 텍스트 리소스 읽기
    {
      "jsonrpc": "2.0",
      "id": 3,
      "method": "resources/read",
      "params": { "uri": "file:///workspace/src/main.c" }
    }
    
    // 텍스트 응답
    {
      "jsonrpc": "2.0",
      "id": 3,
      "result": {
        "contents": [
          {
            "uri":      "file:///workspace/src/main.c",
            "mimeType": "text/x-csrc",
            "text":     "#include \n\nint main(int argc, char **argv) {\n    ..."
            // text 필드: UTF-8 텍스트. 바이너리라면 이 자리에 blob 필드가 온다
          }
        ]
      }
    }
    
    // 바이너리 리소스 읽기 (예: 이미지, PDF)
    {
      "jsonrpc": "2.0",
      "id": 4,
      "result": {
        "contents": [
          {
            "uri":      "file:///workspace/docs/architecture.png",
            "mimeType": "image/png",
            "blob":     "iVBORw0KGgoAAAANSUhEUgAAA..."  // base64 인코딩
            // text와 blob은 배타적 — 둘 중 하나만 온다
          }
        ]
      }
    }

    코드 설명: contents가 배열인 이유가 있다. 하나의 URI가 논리적으로 여러 내용 조각을 가리킬 수 있다 — 예를 들어 DB 테이블 URI가 여러 행을 각각 별도 항목으로 돌려줄 수 있다. textblob은 배타적이다. text는 UTF-8로 인코딩된 일반 텍스트, blob은 base64로 인코딩된 바이너리다. 클라이언트는 mimeType을 보고 어느 쪽을 기대할지 판단한다. 이미지·PDF 같은 바이너리도 이 경로로 모델에게 공급할 수 있다는 점이 Resources의 범용성이다.

    resources/subscribe — 변경을 기다리지 않고 통보받는다

    파일을 한 번 읽고 끝내는 게 아니라, 그 파일이 바뀌면 자동으로 알고 싶을 때가 있다. 빌드 로그가 실시간으로 쌓이거나, 설정 파일이 외부 프로세스에 의해 갱신되는 상황이 그렇다. resources/subscribe가 이 문제를 푼다.

    diagram

    다이어그램 설명: subscribe → (외부 변경 발생) → 알림 수신 → read → unsubscribe의 전형적인 흐름이다. 핵심은 "push 알림 + pull 데이터" 패턴이다. notifications/resources/updated는 "바뀌었다"는 신호만 보내고, 실제 내용은 클라이언트가 resources/read로 다시 가져간다. 알림에 바뀐 내용을 싣지 않는 이유 — 클라이언트가 원할 때만 읽어가게 해서 불필요한 데이터 전송을 막는다. 구독 지원 여부는 초기화 핸드셰이크의 capabilities.resources.subscribe: true로 확인한다.

    // resources/subscribe 요청
    {
      "jsonrpc": "2.0",
      "id": 5,
      "method": "resources/subscribe",
      "params": { "uri": "file:///config/app.yaml" }
    }
    
    // 응답 — 빈 객체, "구독 시작됐어" 신호만
    { "jsonrpc": "2.0", "id": 5, "result": {} }
    
    // (나중에) 서버 → 클라이언트: 변경 알림
    {
      "jsonrpc": "2.0",
      "method": "notifications/resources/updated",   // id 없음 = 알림
      "params": { "uri": "file:///config/app.yaml" } // 뭐가 바뀌었는지만 알림
    }
    
    // resources/unsubscribe 요청
    {
      "jsonrpc": "2.0",
      "id": 6,
      "method": "resources/unsubscribe",
      "params": { "uri": "file:///config/app.yaml" }
    }
    
    // 응답
    { "jsonrpc": "2.0", "id": 6, "result": {} }

    코드 설명: notifications/resources/updated에는 변경된 URI만 들어 있다 — 바뀐 내용은 없다. 서버가 내용을 알림에 싣지 않는 건 설계상 의도다. 변경 내용이 크면 클라이언트가 필요 없을 수도 있고, 클라이언트가 원할 때 pull하는 편이 흐름 제어에 유리하다. resources/unsubscribe를 빠뜨리면 세션 내내 서버가 알림을 보내므로, 더 이상 필요 없을 때 반드시 해제해야 한다. 실제 서버 구현에서 구독 상태는 클라이언트 연결 범위와 함께 관리되는 게 일반적이다 — 연결이 끊어지면 모든 구독이 자동 해제된다.

    notifications/resources/list_changed — 목록 자체가 바뀌었다

    notifications/resources/updated가 특정 리소스의 내용 변경이라면, notifications/resources/list_changed는 리소스 목록의 변경이다. 새 파일이 추가되거나 기존 리소스가 사라질 때 서버가 클라이언트에게 보내는 힌트다.

    // 서버 → 클라이언트: 리소스 목록 변경 알림
    {
      "jsonrpc": "2.0",
      "method": "notifications/resources/list_changed"
      // params 없음 — "목록이 바뀌었으니 다시 list 해줘"라는 신호만
    }
    
    // 클라이언트는 이 알림을 받으면 resources/list를 다시 호출한다
    {
      "jsonrpc": "2.0",
      "id": 7,
      "method": "resources/list"
    }
    // 새로 추가된/제거된 리소스가 반영된 목록이 온다

    코드 설명: params가 없다 — 어떤 리소스가 추가/제거됐는지 알려주지 않는다. "뭔가 바뀌었으니 다시 조회해"라는 invalidation 신호만이다. 이 패턴은 "push invalidation + pull data"의 전형이다. 구체적인 변경 내용을 알림에 싣지 않는 이유는 두 가지다. 첫째, 변경이 연속으로 여러 번 오면 알림을 여러 번 보내지 않고 한 번만 보낼 수 있다. 둘째, 클라이언트가 관심 없는 리소스도 있으므로 필요한 것만 pull해 가게 한다.

    중요한 점은 notifications/resources/list_changedresources/subscribe와 완전히 무관하다는 것이다. 클라이언트가 구독 요청을 따로 보낼 필요가 없다. 서버가 초기 핸드셰이크에서 capabilities.resources.listChanged: true를 선언하면, 이후 목록 변경이 발생할 때마다 서버가 자동으로 이 알림을 전송한다. 지원 여부는 서버 capabilities 하나로만 결정된다.

    실전 시나리오 — 소스코드 분석 MCP 서버

    지금까지의 7가지 메서드가 실제로 어떻게 조합되는지 보자. 소스코드 저장소를 Resources로 노출하는 MCP 서버 시나리오다.

    diagram

    다이어그램 설명: 세션 시작 시 resources/listresources/templates/list로 무엇이 있는지 파악한다. 실제 분석 작업에서는 resources/read로 내용을 가져온다. 빌드 로그 같은 실시간 데이터는 subscribe로 변경 알림을 받고 그때그때 read한다. 파일이 새로 추가되면 list_changed 알림 → 재조회로 최신 상태를 유지한다. 7가지 메서드가 각자 역할을 맡아 하나의 자연스러운 워크플로우를 이룬다.

    Resources가 주는 것 — Tools 대비 얻는 것 3가지

    Resources를 Tool 대신 쓸 때 실제로 무엇이 좋아지는지 구체적으로 정리한다.

    1. 모델의 추론 왕복 제거. Tool로 파일을 읽으면 모델이 "read_file을 호출해야겠다"고 결정하고, 호출하고, 결과를 받고, 그 다음 추론을 이어간다. Resource를 context에 미리 삽입하면 이 왕복이 사라진다. 대화 응답 시간이 빨라진다.

    2. context 공유. 클라이언트가 여러 Resource를 한꺼번에 context에 넣으면, 모델은 처음부터 전체 그림을 보고 추론한다. Tool은 모델이 하나씩 호출하면서 점진적으로 파악한다. 파일 3개를 비교해야 할 때 Resource가 더 유리하다.

    3. subscribe로 실시간 데이터 연동. Tool은 호출 시점의 스냅샷을 준다. Resource의 subscribe는 데이터가 바뀔 때마다 클라이언트가 알 수 있다. 빌드 로그, 센서 데이터, 실시간 DB 같은 변화하는 데이터를 모델 context에 반영할 수 있다 — Tool만으로는 구현하기 까다로운 시나리오다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.