MCP 아키텍처: 설계 철학 및 엔지니어링 원칙

MCP 아키텍처: 설계 철학 및 엔지니어링 원칙

MCP 아키텍처: 설계 철학 및 엔지니어링 원칙

MCP의 아키텍처를 이해하려면 단순한 클라이언트-서버 패턴을 넘어서 생각해야 합니다. 이것은 AI 우선 컴퓨팅을 위해 설계된 프로토콜로, 전통적인 요청-응답 모델이 대규모 언어 모델의 동적이고 컨텍스트가 풍부한 세계를 만나는 곳입니다.

🏗️
아키텍처 관점: MCP는 “AI 통합 패러독스"를 해결합니다 - 즉, 보안 악몽이나 통합 복잡성 없이 AI 시스템에 외부 리소스에 대한 풍부하고 안전한 액세스를 제공하는 방법입니다.

🎯 설계 철학: 이러한 선택들이 중요한 이유

AI 통합의 도전 과제

전통적인 API는 예측 가능한 인간 설계 워크플로우를 위해 설계되었습니다. AI 시스템에는 다음이 필요합니다:

  • 동적 리소스 발견 (AI는 필요할 때까지 무엇이 필요한지 모릅니다)
  • 풍부한 컨텍스트 교환 (데이터뿐만 아니라 메타데이터, 관계, 기능)
  • 안전한 샌드박싱 (AI는 직접 시스템 액세스를 신뢰할 수 없습니다)
  • 양방향 통신 (AI는 데이터를 소비하는 것뿐만 아니라 질문도 해야 합니다)

MCP의 아키텍처 대응

  flowchart TB
    subgraph "🧠 AI 우선 설계 원칙"
        A["🔍 동적 발견<br/>AI가 필요한 것을 찾습니다"] 
        B["🛡️ 안전한 샌드박싱<br/>제어된 리소스 액세스"]
        C["💬 풍부한 컨텍스트<br/>메타데이터 + 관계"]
        D["🔄 양방향 흐름<br/>AI가 질문할 수 있습니다"]
    end
    
    subgraph "🏗️ MCP 아키텍처"
        E["📡 프로토콜 레이어<br/>메시지 라우팅 및 수명주기"]
        F["🚚 전송 레이어<br/>통신 메커니즘"]
        G["🎭 기능 시스템<br/>기능 협상"]
        H["🔐 보안 모델<br/>액세스 제어 및 검증"]
    end
    
    A --> E
    B --> H
    C --> G
    D --> F

🏛️ 핵심 아키텍처: 클라이언트-서버를 넘어서

MCP는 **“중개된 액세스 패턴”**을 구현합니다 - 호스트는 AI와 외부 리소스 간의 보안 중개자 역할을 합니다:

  flowchart TB
    subgraph "🧠 AI 시스템 (LLM)"
        AI["대규모 언어 모델<br/>필요: 컨텍스트, 도구, 데이터"]
    end
    
    subgraph "🏠 호스트 애플리케이션 (보안 중개자)"
        direction TB
        H["호스트 프로세스<br/>(Claude Desktop, IDE 등)"]
        C1["MCP 클라이언트 A<br/>🔗 데이터베이스 액세스"]
        C2["MCP 클라이언트 B<br/>🔗 파일 시스템"]
        C3["MCP 클라이언트 C<br/>🔗 웹 API"]
        
        H --> C1
        H --> C2  
        H --> C3
    end
    
    subgraph "🌐 외부 리소스"
        S1["MCP 서버 A<br/>📊 PostgreSQL"]
        S2["MCP 서버 B<br/>📁 파일 시스템"]
        S3["MCP 서버 C<br/>🌍 REST API"]
    end
    
    AI -.->|"컨텍스트/도구 요청"| H
    C1 <-->|"보안 프로토콜"| S1
    C2 <-->|"보안 프로토콜"| S2
    C3 <-->|"보안 프로토콜"| S3

🔑 핵심 아키텍처 통찰

  1. 보안 중개자로서의 호스트: 호스트는 모든 AI-리소스 상호작용을 중개합니다
  2. 1:1 클라이언트-서버 매핑: 각 리소스 타입은 전용 격리 통신을 갖습니다
  3. 기능 기반 보안: 서버는 할 수 있는 것을 선언하고, 호스트는 허용할 것을 결정합니다
  4. 전송 불가지성: 프로토콜은 stdio, HTTP, WebSockets 등에서 작동합니다

🏗️ 계층형 아키텍처: 관심사 분리

프로토콜 레이어

프로토콜 레이어는 메시지 프레이밍, 요청/응답 연결 및 고수준 통신 패턴을 처리합니다.

    class Protocol<Request, Notification, Result> {
        // Handle incoming requests
        setRequestHandler<T>(schema: T, handler: (request: T, extra: RequestHandlerExtra) => Promise<Result>): void

        // Handle incoming notifications
        setNotificationHandler<T>(schema: T, handler: (notification: T) => Promise<void>): void

        // Send requests and await responses
        request<T>(request: Request, schema: T, options?: RequestOptions): Promise<T>

        // Send one-way notifications
        notification(notification: Notification): Promise<void>
    }
    class Session(BaseSession[RequestT, NotificationT, ResultT]):
        async def send_request(
            self,
            request: RequestT,
            result_type: type[Result]
        ) -> Result:
            """
            Send request and wait for response. Raises McpError if response contains error.
            """
            # Request handling implementation

        async def send_notification(
            self,
            notification: NotificationT
        ) -> None:
            """Send one-way notification that doesn't expect response."""
            # Notification handling implementation

        async def _received_request(
            self,
            responder: RequestResponder[ReceiveRequestT, ResultT]
        ) -> None:
            """Handle incoming request from other side."""
            # Request handling implementation

        async def _received_notification(
            self,
            notification: ReceiveNotificationT
        ) -> None:
            """Handle incoming notification from other side."""
            # Notification handling implementation

핵심 클래스는 다음을 포함합니다:

  • Protocol
  • Client
  • Server

전송 레이어

전송 레이어는 클라이언트와 서버 간의 실제 통신을 처리합니다. MCP는 여러 전송 메커니즘을 지원합니다:

  1. Stdio 전송

    • 통신을 위해 표준 입출력을 사용합니다
    • 로컬 프로세스에 이상적입니다
  2. SSE와 함께하는 HTTP 전송

    • 서버-클라이언트 메시지를 위해 Server-Sent Events를 사용합니다
    • 클라이언트-서버 메시지를 위해 HTTP POST를 사용합니다

모든 전송은 메시지 교환을 위해 JSON-RPC 2.0을 사용합니다. Model Context Protocol 메시지 형식에 대한 자세한 정보는 명세를 참조하세요.

메시지 타입

MCP에는 다음과 같은 주요 메시지 타입이 있습니다:

  1. **요청(Requests)**은 다른 쪽의 응답을 기대합니다:

    interface Request {
      method: string;
      params?: { ... };
    }
  2. **결과(Results)**는 요청에 대한 성공적인 응답입니다:

    interface Result {
      [key: string]: unknown;
    }
  3. **오류(Errors)**는 요청이 실패했음을 나타냅니다:

    interface Error {
      code: number;
      message: string;
      data?: unknown;
    }
  4. **알림(Notifications)**은 응답을 기대하지 않는 일방향 메시지입니다:

    interface Notification {
      method: string;
      params?: { ... };
    }

연결 수명주기

1. 초기화

  sequenceDiagram
    participant Client
    participant Server

    Client->>Server: initialize 요청
    Server->>Client: initialize 응답
    Client->>Server: initialized 알림

    Note over Client,Server: 연결 사용 준비 완료
  1. 클라이언트는 프로토콜 버전과 기능으로 initialize 요청을 보냅니다
  2. 서버는 프로토콜 버전과 기능으로 응답합니다
  3. 클라이언트는 확인으로 initialized 알림을 보냅니다
  4. 정상적인 메시지 교환이 시작됩니다

2. 메시지 교환

초기화 후 다음 패턴이 지원됩니다:

  • 요청-응답: 클라이언트나 서버가 요청을 보내면 다른 쪽이 응답합니다
  • 알림: 양쪽 모두 일방향 메시지를 보낼 수 있습니다

3. 종료

양쪽 모두 연결을 종료할 수 있습니다:

  • close()를 통한 정상 종료
  • 전송 연결 끊김
  • 오류 상황

오류 처리

MCP는 다음과 같은 표준 오류 코드를 정의합니다:

enum ErrorCode {
  // Standard JSON-RPC error codes
  ParseError = -32700,
  InvalidRequest = -32600,
  MethodNotFound = -32601,
  InvalidParams = -32602,
  InternalError = -32603
}

SDK와 애플리케이션은 -32000 이상의 자체 오류 코드를 정의할 수 있습니다.

오류는 다음을 통해 전파됩니다:

  • 요청에 대한 오류 응답
  • 전송에서의 오류 이벤트
  • 프로토콜 수준 오류 핸들러

구현 예제

MCP 서버를 구현하는 기본 예제입니다:

    import { Server } from "@modelcontextprotocol/sdk/server/index.js";
    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

    const server = new Server({
      name: "example-server",
      version: "1.0.0"
    }, {
      capabilities: {
        resources: {}
      }
    });

    // Handle requests
    server.setRequestHandler(ListResourcesRequestSchema, async () => {
      return {
        resources: [
          {
            uri: "example://resource",
            name: "Example Resource"
          }
        ]
      };
    });

    // Connect transport
    const transport = new StdioServerTransport();
    await server.connect(transport);
    import asyncio
    import mcp.types as types
    from mcp.server import Server
    from mcp.server.stdio import stdio_server

    app = Server("example-server")

    @app.list_resources()
    async def list_resources() -> list[types.Resource]:
        return [
            types.Resource(
                uri="example://resource",
                name="Example Resource"
            )
        ]

    async def main():
        async with stdio_server() as streams:
            await app.run(
                streams[0],
                streams[1],
                app.create_initialization_options()
            )

    if __name__ == "__main__":
        asyncio.run(main)

모범 사례

전송 선택

  1. 로컬 통신

    • 로컬 프로세스에는 stdio 전송을 사용하세요
    • 동일 머신 통신에 효율적입니다
    • 간단한 프로세스 관리
  2. 원격 통신

    • HTTP 호환성이 필요한 시나리오에는 SSE를 사용하세요
    • 인증 및 권한 부여를 포함한 보안 영향을 고려하세요

메시지 처리

  1. 요청 처리

    • 입력을 철저히 검증하세요
    • 타입 안전 스키마를 사용하세요
    • 오류를 우아하게 처리하세요
    • 타임아웃을 구현하세요
  2. 진행률 보고

    • 긴 작업에는 진행률 토큰을 사용하세요
    • 점진적으로 진행률을 보고하세요
    • 총 진행률을 알 때 포함하세요
  3. 오류 관리

    • 적절한 오류 코드를 사용하세요
    • 유용한 오류 메시지를 포함하세요
    • 오류 시 리소스를 정리하세요

보안 고려사항

  1. 전송 보안

    • 원격 연결에는 TLS를 사용하세요
    • 연결 출처를 검증하세요
    • 필요시 인증을 구현하세요
  2. 메시지 검증

    • 모든 수신 메시지를 검증하세요
    • 입력을 정화하세요
    • 메시지 크기 제한을 확인하세요
    • JSON-RPC 형식을 검증하세요
  3. 리소스 보호

    • 액세스 제어를 구현하세요
    • 리소스 경로를 검증하세요
    • 리소스 사용량을 모니터링하세요
    • 요청 속도를 제한하세요
  4. 오류 처리

    • 민감한 정보를 유출하지 마세요
    • 보안 관련 오류를 기록하세요
    • 적절한 정리를 구현하세요
    • DoS 시나리오를 처리하세요

디버깅 및 모니터링

  1. 로깅

    • 프로토콜 이벤트를 기록하세요
    • 메시지 흐름을 추적하세요
    • 성능을 모니터링하세요
    • 오류를 기록하세요
  2. 진단

    • 상태 확인을 구현하세요
    • 연결 상태를 모니터링하세요
    • 리소스 사용량을 추적하세요
    • 성능을 프로파일링하세요
  3. 테스팅

    • 다른 전송을 테스트하세요
    • 오류 처리를 검증하세요
    • 엣지 케이스를 확인하세요
    • 서버 부하 테스트를 하세요