[AI] MCP 프로토콜(모델에 도구를 노출하는 표준 인터페이스)

개요

LLM을 단순한 챗봇이 아니라 외부 시스템을 조작하는 에이전트(Agent)로 쓰려면 모델이 외부 도구를 표준화된 방식으로 인식하고 호출할 수 있어야 한다. 이 표준이 바로 MCP(Model Context Protocol)다.

MCP는 도구를 모델에 어떻게 노출할지를 정의하는 프로토콜이다. 누가 어떤 절차로 그 도구를 쓸지는 별도 단위인 Claude Agent, Skill, Hook이 책임진다.

MCP의 통신 구조, Tool Calling과의 관계, Spring AI 로 직접 MCP 서버를 띄우는 절차를 차례대로 다룬다.

MCP(모델에 도구를 노출하는 표준 프로토콜)

MCP 가 풀려는 문제

MCP 이전에는 AI앱과 외부 시스템의 연결이 N x M 문제였다. 앱이 N개, 외부 시스템이 M개 있을 때 통합 코드는 N x M개 필요했다. Slack을 각 LLM 클라이언트에게 제공하기 위해 각 LLM 클라이언트에 맞게 Slack 시스템을 만들어야 했다.

MCP 는 이를 N+M 으로 줄인다. 외부 시스템은 MCP Server 를 한 번만 구현하고, AI 앱은 MCP Client 를 한 번만 구현한다. 그 뒤로는 어떤 호스트에 어떤 서버를 꽂아도 동작한다.

핵심은 도구의 명세를 llm 클라이언트가 런타임에 가져온다는 점이다. OpenAPI처럼 사람이 문서를 보고 클라이언트 코드를 작성하는 흐름이 아니라, llm 클라이언트가 시작 시점에 서버에 어떤 도구가 있는지 물어 도구 목록을 받는다. 그 목록을 그대로 LLM 의 시스템 프롬프트나 도구 영역에 끼워 넣으면, LLM 은 그 도구들을 자기 도구로 인식하고 적절한 시점에 호출 결정을 내린다.

MCP 의 3-Layer 구조

MCP는 Host, Client, Server 세 층으로 동작한다.

구성요소역할위치
HostAI 가 동작하는 앱(Claude Desktop, Claude Code, Cursor 등)사용자 PC 또는 IDE
MCP ClientHost 안에 내장되어 Server 와 통신Host 프로세스 내부
MCP Server외부 시스템(GitHub, DB, FS 등)을 MCP 프로토콜로 노출로컬 프로세스 또는 원격 서버

각 MCP Server는 세 종류의 자원을 LLM에게 제공할 수 있다.

  • Tools: LLM이 호출하는 함수(메시지 전송, 파일 생성 등)
  • Resources: LLM이 읽는 데이터(파일, DB 레코드)
  • Prompts: 재사용 프롬프트 템플릿

Tool

Tool은 단순한 함수 정의가 아니라 LLM이 읽을 수 있게 표준에 맞게 정의되고, input을 전달받으면, 그에 맞는 요청을 실행해준다.

{
  "name": "create_pull_request",
  "description": "GitHub 저장소에 새 PR을 생성한다. base와 head 브랜치 사이의 diff로 PR을 만든다.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "title":  { "type": "string", "description": "PR 제목" },
      "body":   { "type": "string", "description": "PR 본문 (마크다운)" },
      "branch": { "type": "string", "description": "PR을 만들 head 브랜치 이름" }
    },
    "required": ["title", "branch"]
  }
}

LLM은 description을 읽고 어떤 Tool을 쓸지 결정하고, inputSchema 로 인자의 모양을 결정한다.즉, Tool은 기존 REST API의 래퍼다.

REST SDKMCP Tool
호출 주체개발자가 코드로 직접 호출LLM 이 스스로 판단해 호출
발견 방식사람이 문서 읽고 파악LLM 이 런타임에 목록 조회
인터페이스사람이 이해하는 엔드포인트LLM 이 이해하는 description 과 JSON Schema
표준화없음(서비스마다 다름)MCP 스펙으로 통일

REST SDK는 사람이 설계하고 사람이 호출하는 인터페이스이고, MCP Tool은 LLM이 동적으로 발견하고 LLM이 호출하는 인터페이스다.

로컬 서버와 원격 서버

로컬 MCP Server원격 MCP Server
실행 위치사용자 PC 에서 직접 실행외부 서버에서 실행
Transportstdio(표준 입출력)HTTP/SSE 또는 Streamable HTTP
예시파일 시스템, 로컬 DB, PlaywrightSlack, GitHub, Notion
인증보통 불필요API Key, Bearer JWT, OAuth

로컬 서버는 stdio로 동작한다. Host가 자식 프로세스로 서버를 띄우고 그 프로세스의 stdin/stdout 으로 JSON-RPC 메시지를 주고받는다. 네트워크가 필요 없고 인증도 거의 필요 없다.

원격 서버는 HTTP/SSE 로 동작한다. 서버가 SSE 스트림을 유지하고 호스트가 클라이언트로 붙는다. SSE를 쓰는 이유는 도구 실행이 길어질 수 있어서다. 빌드 실행이나 대용량 파일 처리는 응답을 한 번에 못 돌려준다. SSE는 서버가 준비되는 즉시 스트리밍으로 결과를 응답한다.

JSON-RPC 메시지 포맷

MCP의 모든 통신은 JSON-RPC 2.0 메시지다. 클라이언트와 서버는 다음 형태의 단일 메시지 구조만 주고받는다.

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "get_current_weather",
    "arguments": { "city": "서울" }
  }
}

응답은 같은 id를 그대로 돌려준다.

{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "content": [
      { "type": "text", "text": "{\"city\":\"서울\",\"temperatureC\":23}" }
    ]
  }
}

Tool 이름 짓기와 인자 설계

LLM이 도구를 정확히 선택하느냐는 description과 이름 설계에 크게 좌우된다. 같은 기능이라도 표현을 어떻게 하느냐에 따라 선택 정확도가 달라진다.

  • 동사로 시작한다. create_pull_request는 좋고, pull_request 는 모호하다.
  • 한 도구는 한 일만 한다. update_or_create_user 처럼 분기가 들어가면 LLM이 인자를 잘못 채우기 쉽다.
  • description에 사용 시점을 명시한다. 이 도구는 PR 본문 작성이 끝난 뒤 호출한다 같은 한 줄이 description 매칭 정확도를 끌어올린다.
  • 중첩보다 플랫한 필드 다섯 개가 낫다.
{
  "name": "send_slack_message",
  "description": "지정한 Slack 채널에 텍스트 메시지를 보낸다. 사용자에게 결과를 통보하거나 알림을 띄울 때 호출한다.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "channel": { "type": "string", "description": "채널 ID 또는 이름. 예: #ops" },
      "text":    { "type": "string", "description": "메시지 본문. 1000자 이하" }
    },
    "required": ["channel", "text"]
  }
}

LLM은 사용자에게 Slack 으로 결과를 알려달라는 요청에 send_slack_message를 호출하고 channel과 text를 채운다.

MCP 클라이언트와 서버 통신 다이어그램

원격 SSE 서버를 기준으로 한 통신 흐름이다.

sequenceDiagram
    autonumber
    actor User as 사용자
    participant Host as Host (Claude Code)
    participant LLM
    participant MCli as MCP Client
    participant MSrv as MCP Server
    participant API as 외부 API (GitHub)

    Host->>MCli: 시작 시 connect
    MCli->>MSrv: 1. initialize (handshake)
    MSrv-->>MCli: 2. 서버 메타데이터 (이름, 버전, instructions)
    MCli->>MSrv: 3. notifications/initialized
    MCli->>MSrv: 4. tools/list
    MSrv-->>MCli: 5. 도구 목록 JSON Schema
    MCli->>Host: 6. 도구 목록을 LLM 컨텍스트에 주입

    User->>Host: biuea 브랜치로 PR 만들어줘
    Host->>LLM: 7. user message + tools
    LLM-->>Host: 8. tool_use 응답<br/>(create_pull_request, title, branch)
    Host->>MCli: 9. 도구 호출 위임
    MCli->>MSrv: 10. tools/call create_pull_request
    MSrv->>API: 11. POST /repos/.../pulls
    API-->>MSrv: 12. 201 Created + PR URL
    MSrv-->>MCli: 13. JSON-RPC 결과
    MCli-->>Host: 14. tool_result
    Host->>LLM: 15. tool_result 메시지
    LLM-->>Host: 16. 최종 자연어 응답
    Host-->>User: PR 을 생성했습니다 https://...

Spring AI 로 MCP Server 띄우기(Kotlin)

// build.gradle.kts
extra["springAiVersion"] = "1.1.6"
 
dependencyManagement {
    imports {
        mavenBom("org.springframework.ai:spring-ai-bom:${property("springAiVersion")}")
    }
}
 
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc")
}
# application.yml
spring:
  ai:
    mcp:
      server:
        enabled: true
        name: clothing-ecommerce-mcp
        version: 0.0.1
        type: SYNC
        instructions: 의류 이커머스 상품 검색, 재고 조회, 주문 상태 조회 도구를 제공한다.

@Tool 메서드 빈만 정의하면 starter 가 자동으로 MCP tools/list 에 끼워 넣는다.

@Component
class CatalogTools(
    private val productRepository: ProductRepository,
    private val toolGuard: ToolGuard,
) {
    @Tool(description = "의류 상품을 키워드, 카테고리, 색상, 최대 가격으로 검색해 목록을 반환한다.")
    fun searchProducts(
        @ToolParam(description = "검색 키워드", required = false) keyword: String?,
        @ToolParam(description = "카테고리", required = false) category: String?,
        @ToolParam(description = "색상", required = false) color: String?,
        @ToolParam(description = "최대 가격(원)", required = false) maxPrice: Int?,
    ): List<ProductSummary> = toolGuard.invoke(
        tool = "searchProducts",
        scope = "catalog:read",
        args = mapOf("keyword" to keyword, "category" to category),
    ) {
        productRepository.search(keyword, category, color, maxPrice).map { it.toSummary() }
    }
}
 
@Configuration
class McpServerConfig {
    @Bean
    fun toolCallbackProvider(catalogTools: CatalogTools): ToolCallbackProvider =
        MethodToolCallbackProvider.builder()
            .toolObjects(catalogTools)
            .build()
}

starter 는 GET /ssePOST /mcp/message 엔드포인트를 자동으로 띄운다. Claude Code 에서는 다음 명령 하나로 연결된다.

TOKEN=$(curl -s -X POST localhost:8080/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"clientId":"shopper-llm","clientSecret":"dev-secret-1"}' | jq -r .accessToken)
 
claude mcp add --transport sse clothing-ecommerce http://localhost:8080/sse \
  --header "Authorization: Bearer $TOKEN"

같은 @Tool 빈을 인앱 ChatClient(.tools(catalogTools))와 MCP Server 양쪽에서 동시에 쓸 수 있다는 점이 가장 큰 장점이다. 도구 정의는 한 벌이지만 노출 채널은 두 갈래다.

MCP 의 보안

MCP Server를 외부에 노출하면 LLM이 임의의 인자로 도구를 호출할 수 있다는 뜻이다. 환각, 오해, 악의적 사용자에 의해 LLM이 권한 밖 도구를 호출하거나 인자를 잘못 채울 수 있다.

  • 모델 쪽 방어: 사용자 의도 재확인(중요한 작업은 confirm), description에 이 도구는 어떤 경우에만 쓴다는 가드 문구.
  • 호스트 쪽 방어: JWT 인증, 스코프 기반 인가, 멱등키 검증.
@Tool(description = "재고를 입출고한다.")
fun restockProduct(
    @ToolParam(description = "상품 ID") productId: Long,
    @ToolParam(description = "변경량(+/-)") delta: Int,
): RestockResult = toolGuard.invoke(
    tool = "restockProduct",
    scope = "catalog:write",     // 부족하면 AccessDeniedException
    args = mapOf("productId" to productId, "delta" to delta),
) {
    productRepository.adjustStock(productId, delta)
}

MCP와 REST API 차이점

Tool은 REST API의 래퍼이다. 그러면 이미 REST API가 있는데 왜 MCP가 필요한가.REST API 는 LLM이 자기 도구로 인식하지 못한다. LLM에게 엔드포인트 URL과 OpenAPI 문서는 그냥 텍스트일 뿐이다. 누군가 그 문서를 읽고 코드를 짜야 호출이 일어난다.

MCP는 REST API를 대체하지 않는다. MCP Server 안에서 실제로 일어나는 일은 REST API 호출이다. 차이는 그 API를 누가, 언제, 어떻게 발견하고 호출하느냐이다.

관점REST APIMCP
호출 주체개발자가 작성한 코드LLM 이 런타임에 스스로
결합 시점빌드타임(정적). 문서 보고 코드 작성런타임(동적). tools/list 로 발견
명세 소비자사람(OpenAPI 문서)LLM(description + JSON Schema)
통신 형식서비스마다 제각각(REST·GraphQL·gRPC)JSON-RPC 2.0 으로 통일
세션보통 무상태initialize 핸드셰이크 + capability 협상(유상태)
통신 방향단방향 요청-응답양방향(서버→클라이언트 notification, sampling 요청)
인증서비스별 제각각MCP 인증 스펙(OAuth 2.1 기반)으로 표준화

REST API는 사람이 코드로 호출하는 인터페이스이고, MCP는 LLM이 스스로 발견하고, 호출하게 하는 인터페이스다. 즉, REST를 없애는 게 아니라 REST 위에 LLM 이 읽을 수 있게 래핑한 것이다.

MCP와 CLI 도구: 왜 CLI 가 다시 떠오르는가

에이전트에게 터미널(bash)을 주고 CLI를 쓰게 하자는 흐름이 커졌다. Claude Code가 별도 MCP Server 없이 gh, git, curl, jq 같은 CLI를 그대로 사용하여 작업하는 게 대표적이다.

  • 토큰 효율: MCP는 연결하는 순간 모든 도구의 JSON Schema를 매 요청 컨텍스트로 읽는다. CLI 는 필요할 때만 --help 나 man 으로 사용법을 조회하므로 평상시 컨텍스트가 가볍다.
  • 합성성: CLI 는 파이프(|)·grep·jq 로 조합한다. 중간 결과가 LLM 컨텍스트를 거치지 않고 쉘 안에서 가공돼 최종 결과만 모델로 돌아온다. MCP Tool은 호출할 때마다 결과 전체가 컨텍스트로 올라온다.
  • 생태계 재사용: gh, aws, kubectl, git 등 수십 년 검증된 도구를 새 서버 구현 없이 즉시 쓴다.

그렇다고 CLI가 MCP를 대체하지는 않는다. 표준화·인증·구조화가 필요한 지점에서는 MCP가 여전히 앞선다.

관점CLI 도구MCP
발견--help 텍스트 파싱(비정형)tools/list 구조화 스키마
출력비정형 텍스트(파싱이 깨지기 쉬움)JSON 구조화 결과
인증·인가로컬 자격증명에 의존OAuth·스코프로 표준화
권한 제어bash 풀 권한은 위험도구 단위 스코프
원격·크로스플랫폼도구가 설치된 환경 필요원격 서버로 어디서든