포스트

[MCP&A2A] 02. MCP 프로토콜 이해

[MCP&A2A] 02. MCP 프로토콜 이해

Model Context Protocol (MCP) 개요

MCP는 AI 어시스턴트가 외부 데이터 소스와 도구에 접근하기 위한 보편적이고 개방적인 표준입니다. 2024년 11월 Anthropic이 발표했으며, 현재 Linux Foundation의 Agentic AI Foundation에 기부되었습니다.

MCP의 핵심 가치 제안

문제:

1
2
각 AI 애플리케이션이 데이터 소스마다 커스텀 통합 구축
→ M × N 통합 폭발

해결:

1
2
AI 애플리케이션 → MCP 클라이언트 ← 단일 프로토콜 → MCP 서버 → 데이터 소스
→ M + N 통합으로 단순화

주요 특징

  1. 표준화된 인터페이스: JSON-RPC 2.0 기반
  2. 언어 중립적: 모든 프로그래밍 언어 지원
  3. 동기식 실행: 빠른 요청-응답 패턴
  4. 무상태: 수평 확장 가능
  5. 보안: 인증 및 권한 부여 지원

MCP 아키텍처

컴포넌트 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌─────────────────────────────────────┐
│      MCP Client (Host)              │
│  ┌───────────────────────────────┐  │
│  │  Claude Desktop               │  │
│  │  Zed                          │  │
│  │  Replit                       │  │
│  │  Custom AI Application        │  │
│  └───────────────────────────────┘  │
└────────────┬────────────────────────┘
             │ MCP Protocol
             │ (JSON-RPC 2.0)
             ▼
┌─────────────────────────────────────┐
│      MCP Server                     │
│  ┌───────────────────────────────┐  │
│  │  Tools                        │  │
│  │  Resources                    │  │
│  │  Prompts                      │  │
│  └───────────────────────────────┘  │
└────────────┬────────────────────────┘
             │
             ▼
┌─────────────────────────────────────┐
│   Data Sources / External Systems   │
│  • PostgreSQL                       │
│  • Google Drive                     │
│  • Slack                            │
│  • GitHub                           │
└─────────────────────────────────────┘

MCP 서버의 세 가지 핵심 기능

1. Tools (도구)

정의: AI 모델이 실행할 수 있는 함수

특징:

  • 구조화된 입력/출력
  • 잘 정의된 동작
  • 동기식 실행
  • JSON 스키마로 매개변수 정의

예제 - 하이브리드 검색 도구:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
  "name": "hybrid_search",
  "description": "BM25와 벡터 유사도를 결합한 하이브리드 문서 검색",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "검색 쿼리"
      },
      "limit": {
        "type": "integer",
        "description": "반환할 최대 결과 수",
        "default": 10,
        "minimum": 1,
        "maximum": 50
      },
      "bm25_weight": {
        "type": "number",
        "description": "BM25 점수 가중치 (0.0-1.0)",
        "default": 0.5,
        "minimum": 0.0,
        "maximum": 1.0
      },
      "vector_weight": {
        "type": "number",
        "description": "벡터 점수 가중치 (0.0-1.0)",
        "default": 0.5,
        "minimum": 0.0,
        "maximum": 1.0
      }
    },
    "required": ["query"]
  }
}

Go 구현 예제:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// MCP 도구 인터페이스
type Tool interface {
    Definition() protocol.ToolDefinition
    Execute(ctx context.Context, args map[string]interface{}) (protocol.ToolCallResult, error)
}

// 하이브리드 검색 도구 구현
type HybridSearchTool struct {
    db database.Store
}

func (t *HybridSearchTool) Definition() protocol.ToolDefinition {
    return protocol.ToolDefinition{
        Name:        "hybrid_search",
        Description: "BM25와 벡터 유사도를 결합한 하이브리드 문서 검색",
        InputSchema: map[string]interface{}{
            "type": "object",
            "properties": map[string]interface{}{
                "query": map[string]interface{}{
                    "type":        "string",
                    "description": "검색 쿼리",
                },
                "limit": map[string]interface{}{
                    "type":        "integer",
                    "description": "반환할 최대 결과 수",
                    "default":     10,
                    "minimum":     1,
                    "maximum":     50,
                },
                // ... 나머지 속성
            },
            "required": []string{"query"},
        },
    }
}

func (t *HybridSearchTool) Execute(ctx context.Context, args map[string]interface{}) (protocol.ToolCallResult, error) {
    // 1. 매개변수 추출
    query, _ := args["query"].(string)
    limit, _ := args["limit"].(float64)
    bm25Weight, _ := args["bm25_weight"].(float64)
    vectorWeight, _ := args["vector_weight"].(float64)
    
    // 2. 기본값 설정
    if limit <= 0 {
        limit = 10
    }
    if bm25Weight == 0 && vectorWeight == 0 {
        bm25Weight = 0.5
        vectorWeight = 0.5
    }
    
    // 3. 임베딩 생성 (벡터 검색용)
    var embedding []float32
    if vectorWeight > 0 {
        embedding = generateEmbedding(query)
    }
    
    // 4. 하이브리드 검색 실행
    params := database.HybridSearchParams{
        Query:        query,
        Embedding:    embedding,
        Limit:        int(limit),
        BM25Weight:   bm25Weight,
        VectorWeight: vectorWeight,
    }
    
    results, err := t.db.HybridSearch(ctx, params)
    if err != nil {
        return protocol.ToolCallResult{IsError: true}, err
    }
    
    // 5. 결과 반환
    jsonData, _ := json.Marshal(results)
    return protocol.ToolCallResult{
        Content: []protocol.ContentBlock{
            {Type: "text", Text: string(jsonData)},
        },
        IsError: false,
    }, nil
}

2. Resources (리소스)

정의: AI 모델이 읽을 수 있는 데이터

특징:

  • URI 기반 식별
  • 다양한 MIME 타입 지원
  • 읽기 전용 또는 읽기/쓰기
  • 템플릿 지원

예제 - 문서 리소스:

1
2
3
4
5
6
{
  "uri": "document://contracts/contract-123",
  "name": "계약서 #123",
  "description": "Acme Corp와의 서비스 계약서",
  "mimeType": "application/pdf"
}

사용 시나리오:

1
2
3
4
5
6
7
사용자: "계약서 #123의 데이터 유출 조항을 요약해줘"

1. Claude가 리소스 목록 조회
2. document://contracts/contract-123 발견
3. 리소스 읽기 요청
4. 문서 내용 수신
5. AI가 관련 조항 분석 및 요약

3. Prompts (프롬프트)

정의: 재사용 가능한 프롬프트 템플릿

특징:

  • 매개변수화된 템플릿
  • 다단계 상호작용
  • 일관된 AI 동작
  • 베스트 프랙티스 캡슐화

예제 - 코드 리뷰 프롬프트:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "name": "code_review",
  "description": "코드 리뷰 수행 및 개선 제안",
  "arguments": [
    {
      "name": "language",
      "description": "프로그래밍 언어",
      "required": true
    },
    {
      "name": "code",
      "description": "리뷰할 코드",
      "required": true
    }
  ]
}

JSON-RPC 2.0 프로토콜

왜 JSON-RPC 2.0인가?

MCP가 JSON-RPC 2.0을 선택한 이유:

  1. 언어 중립적: 모든 언어가 JSON과 HTTP를 지원
  2. 배치 처리 가능: 여러 요청을 하나의 HTTP 호출로
  3. 표준화된 에러: 일관된 에러 코드 체계
  4. 검증된 기술: 20년 이상의 프로덕션 사용 경험

요청 구조

1
2
3
4
5
6
7
8
9
10
11
12
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "hybrid_search",
    "arguments": {
      "query": "machine learning",
      "limit": 10
    }
  }
}

필드 설명:

  • jsonrpc: 항상 “2.0”
  • id: 요청 식별자 (응답 매칭용)
  • method: 호출할 RPC 메서드
  • params: 메서드 매개변수

성공 응답

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[{\"doc_id\": \"123\", \"title\": \"ML Guide\", \"score\": 0.95}]"
      }
    ],
    "isError": false
  }
}

에러 응답

1
2
3
4
5
6
7
8
9
10
11
12
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "Invalid params",
    "data": {
      "field": "query",
      "reason": "required field missing"
    }
  }
}

표준 에러 코드: | 코드 | 의미 | 설명 | |——|——|——| | -32700 | Parse error | 잘못된 JSON | | -32600 | Invalid Request | 필수 필드 누락 | | -32601 | Method not found | 존재하지 않는 메서드 | | -32602 | Invalid params | 잘못된 매개변수 | | -32603 | Internal error | 서버 내부 오류 |

커스텀 MCP 에러 코드: | 코드 | 의미 | 사용 예 | |——|——|———| | -32001 | Authentication required | JWT 토큰 누락 | | -32002 | Authorization failed | 권한 부족 | | -32003 | Rate limit exceeded | 할당량 초과 | | -32004 | Resource not found | 리소스 없음 | | -32005 | Validation error | 입력 검증 실패 |

MCP 메서드

초기화 (Initialization)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 클라이언트  서버: initialize
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "roots": {
        "listChanged": true
      }
    },
    "clientInfo": {
      "name": "my-client",
      "version": "1.0.0"
    }
  }
}

// 서버  클라이언트: 응답
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "tools": {},
      "resources": {},
      "prompts": {}
    },
    "serverInfo": {
      "name": "mcp-server",
      "version": "1.0.0"
    }
  }
}

도구 목록 조회 (tools/list)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 요청
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list"
}

// 응답
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "hybrid_search",
        "description": "하이브리드 문서 검색",
        "inputSchema": { /* ... */ }
      },
      {
        "name": "get_document",
        "description": "ID로 문서 조회",
        "inputSchema": { /* ... */ }
      }
    ]
  }
}

도구 호출 (tools/call)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 요청
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "hybrid_search",
    "arguments": {
      "query": "AI ethics",
      "limit": 5
    }
  }
}

// 응답
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[{\"id\":\"doc1\",\"title\":\"AI Ethics Guide\",\"score\":0.92}]"
      }
    ],
    "isError": false
  }
}

Transport 메커니즘

MCP는 다양한 전송 메커니즘을 지원합니다.

1. STDIO (Standard Input/Output)

용도: 로컬 프로세스 간 통신

1
2
3
4
5
6
7
8
9
10
11
12
# MCP 서버를 STDIO 모드로 실행
./mcp-server --transport stdio

# Claude Desktop 설정
{
  "mcpServers": {
    "documents": {
      "command": "/path/to/mcp-server",
      "args": ["--transport", "stdio"]
    }
  }
}

장점:

  • ✅ 간단한 설정
  • ✅ 프로세스 격리
  • ✅ 로컬 개발에 적합

단점:

  • ❌ 네트워크 통신 불가
  • ❌ 원격 배포 어려움

2. HTTP + SSE (Server-Sent Events)

용도: 원격 서버 배포 (레거시, 2024-11-05 스펙)

1
2
클라이언트 → HTTP POST → 서버 (요청)
서버 → SSE → 클라이언트 (진행 상황 스트리밍)

단점:

  • ❌ 지속적 연결 필요 (서버리스 불가)
  • ❌ 유휴 시 비용 효율성 낮음

3. Streamable HTTP (권장, 2025-03-26 스펙)

용도: 프로덕션 원격 배포

1
클라이언트 ←→ HTTP(S) ←→ 서버

장점:

  • ✅ 무상태 설계
  • ✅ 서버리스 플랫폼 지원
  • ✅ 수평 확장 용이
  • ✅ 유휴 시 비용 절감

예제 엔드포인트:

1
2
3
4
5
6
7
8
9
10
11
POST /mcp
Host: mcp.example.com
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": { /* ... */ }
}

MCP 생명주기

1. 연결 및 초기화

1
2
3
4
1. 클라이언트가 서버에 연결
2. initialize 메서드 호출
3. 서버가 기능(capabilities) 반환
4. initialized 알림 전송

2. 도구 디스커버리

1
2
3
1. tools/list로 사용 가능한 도구 조회
2. 각 도구의 스키마 검토
3. AI가 적절한 도구 선택

3. 도구 실행

1
2
3
1. tools/call로 도구 실행
2. 서버가 처리 후 결과 반환
3. AI가 결과 해석 및 다음 단계 결정

4. 종료

1
2
1. 클라이언트 연결 종료
2. 서버 리소스 정리

MCP 보안 고려사항

1. 인증 (Authentication)

JWT 기반 인증:

1
2
POST /mcp
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

OAuth 2.1 지원 (2025년 추가):

1
2
3
4
5
6
7
8
{
  "/.well-known/oauth-protected-resource": {
    "resource": "https://mcp.example.com",
    "authorization_servers": [
      "https://auth.example.com"
    ]
  }
}

2. 권한 부여 (Authorization)

도구별 권한:

1
2
3
4
5
6
7
8
9
10
11
type Permission struct {
    UserID    string
    ToolName  string
    Allowed   bool
}

func (a *Authorizer) CanExecute(userID, toolName string) bool {
    // 사용자별 도구 실행 권한 확인
    perm := a.db.GetPermission(userID, toolName)
    return perm.Allowed
}

3. Rate Limiting

테넌트별 할당량:

1
2
3
4
5
6
7
8
type RateLimiter struct {
    limits map[string]*rate.Limiter
}

func (r *RateLimiter) Allow(tenantID string) bool {
    limiter := r.getLimiter(tenantID)
    return limiter.Allow()
}

MCP 모범 사례

1. 도구 설계

DO:

  • 단일 책임 원칙 준수
  • 명확한 입력/출력 스키마
  • 상세한 설명 제공
  • 에러 케이스 처리

DON’T:

  • 과도하게 복잡한 도구
  • 애매한 매개변수 이름
  • 부작용이 많은 도구
  • 긴 실행 시간 (>30초)

2. 에러 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (t *Tool) Execute(ctx context.Context, args map[string]interface{}) (protocol.ToolCallResult, error) {
    // 입력 검증
    if err := validateArgs(args); err != nil {
        return protocol.ToolCallResult{IsError: true}, 
            fmt.Errorf("invalid arguments: %w", err)
    }
    
    // 타임아웃 설정
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
    
    // 실행
    result, err := t.executeInternal(ctx, args)
    if err != nil {
        // 에러 로깅
        log.Error("tool execution failed", 
            "tool", t.Name(),
            "error", err)
        
        // 사용자 친화적 에러 메시지
        return protocol.ToolCallResult{IsError: true}, 
            fmt.Errorf("도구 실행 실패: %w", err)
    }
    
    return result, nil
}

3. 성능 최적화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 커넥션 풀 사용
var dbPool *pgxpool.Pool

func init() {
    dbPool, _ = pgxpool.New(context.Background(), dbURL)
}

// 캐싱
var cache = make(map[string]interface{})

func (t *Tool) Execute(ctx context.Context, args map[string]interface{}) (protocol.ToolCallResult, error) {
    cacheKey := generateCacheKey(args)
    
    // 캐시 확인
    if cached, ok := cache[cacheKey]; ok {
        return cached.(protocol.ToolCallResult), nil
    }
    
    // 실행 및 캐싱
    result, err := t.executeInternal(ctx, args)
    if err == nil {
        cache[cacheKey] = result
    }
    
    return result, err
}

실전 예제: 완전한 MCP 서버

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    
    "github.com/yourorg/mcp-server/protocol"
)

type MCPServer struct {
    tools map[string]Tool
}

func (s *MCPServer) handleMCP(w http.ResponseWriter, r *http.Request) {
    var req protocol.Request
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        respondError(w, -32700, "Parse error")
        return
    }
    
    ctx := r.Context()
    
    switch req.Method {
    case "initialize":
        s.handleInitialize(ctx, w, &req)
    case "tools/list":
        s.handleToolsList(ctx, w, &req)
    case "tools/call":
        s.handleToolsCall(ctx, w, &req)
    default:
        respondError(w, -32601, "Method not found")
    }
}

func (s *MCPServer) handleToolsCall(ctx context.Context, w http.ResponseWriter, req *protocol.Request) {
    var params struct {
        Name      string                 `json:"name"`
        Arguments map[string]interface{} `json:"arguments"`
    }
    
    if err := json.Unmarshal(req.Params, &params); err != nil {
        respondError(w, -32602, "Invalid params")
        return
    }
    
    tool, ok := s.tools[params.Name]
    if !ok {
        respondError(w, -32004, "Tool not found")
        return
    }
    
    result, err := tool.Execute(ctx, params.Arguments)
    if err != nil {
        respondError(w, -32603, err.Error())
        return
    }
    
    respondSuccess(w, req.ID, result)
}

func main() {
    server := &MCPServer{
        tools: map[string]Tool{
            "hybrid_search": &HybridSearchTool{},
            "get_document":  &GetDocumentTool{},
        },
    }
    
    http.HandleFunc("/mcp", server.handleMCP)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

핵심 요약

MCP의 강점

  • 표준화: 언어 중립적 프로토콜
  • 단순성: JSON-RPC 2.0 기반
  • 확장성: 무상태 설계
  • 보안: 인증/권한 지원
  • 성능: 동기식 빠른 실행

MCP의 한계

  • 상태 관리: 멀티스텝 워크플로우 어려움
  • 진행 상황: 장기 실행 태스크 추적 불가
  • 에이전트 협업: 에이전트 간 통신 미지원

이러한 한계를 A2A 프로토콜이 보완합니다 (다음 장에서 설명).

다음 장: A2A 프로토콜의 핵심 개념과 MCP와의 통합 방법


참고 자료:

  • MCP 공식 사양: https://spec.modelcontextprotocol.io/
  • MCP SDK (Go): https://github.com/mark3labs/mcp-go
  • MCP SDK (Python): https://github.com/modelcontextprotocol/python-sdk

작성일: 2024년 12월 13일

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.