이번 프로젝트에선 높아진 생산성을 얼마나 내가 사용 할 수 있고 관리 할 수 있는지를 테스트 해보기 위하여 최대한 AI를 활용해보려고 한다.
이에 요즘 아주 뜨거운 감자인 MCP에 대해 알아보고자 몇몇 개의 글들과 영상을 시청 한 후 공부하며 이번 게시글을 적는다.
게시글을 읽기 앞서 정말 끝내주게 멋있는 글들이 있어 서문에 미리 적어둔다.
2018 년 기존 Large Language Model
인 LLM 은 사전에 학습된 방대한 데이터를 기반으로 자연어를 인식하고 자연어로 된 응답을 반환하는 형태였다.
초기 LLM 모델은 사전에 학습된 데이터를 기반으로 하였기에 다음과 같은 한계점을 가지고 있었다.
이를 해결하기 위해 여러 사용자는 원하는 데이터를 LLM 모델에 입력하는 파인 튜닝을 거치거나 원하는 기능을 수행 할 수 있도록 서비스를 생성하여 사용하곤 했다.
2020~2022 년엔 Chain-of-Thought prompting
의 기법을 통해 파인 튜닝 없이도 추론을 가능하게 하여 복잡한 문제를 해결 할 수 있도록 했고 이후 LLM 자체를 문서 기반의 자연어 처리 모델에서 LLM을 두뇌로서 활용하는 프레임 워크들이 등장하며 에이전트의 개념 이 도입됐다.
대표적인 프레임워크인 랭체인에선 여러 자연어 처리 모델들의 결과값을 체인형태로 엮여진 다양한 자연어 처리모델들의 입력값으로 활용하며 그 과정 속에서 외부 데이터 소스와 연결하거나 자연어 처리의 추론 과정에 따라 Tools
에 해당하는 서비스를 이용해 실제 세계의 서비스를 처리하는 에이전트로서의 역할 을 할 수 있도록 하였다.
자연어 추론 과정에서 특정 서비스를 호출할지 말지 결정하여 서비스를 호출하는 과정을
Function calls
라고 한다.
이런 에이전트 서비스를 통해 LLM 모델이 단순 검색 엔진을 대체하는 역할 뿐 아니라 실제 서비스 제공자로서의 역할을 가능하게 되었다.
외부 데이터 소스와 연결 함으로서 특정 도메인에 특화된 정보를 쉽게 활용 할 수 있었을 뿐더러 연결된 Tools
를 활용해 실제 서비스 제공자로서의 역할을 가능하게 했다.
예를 들어 랭체인으로 생성된 여행 계획 챗봇 에이전트의 경우엔 연결된 Tools
에 해당하는 숙박 업소 API, 날씨 API , 맛집 API ... 등을 활용하여 계획을 생성하고 예약하는 등의 행위가 가능했다.
다만 문제는 에이전트 별로 Tools
를 사용하는 프로토콜이 달랐다는 점이다.
만약 숙박 업소 서비스를 제공하는 리소스 서버(이하 Tool 서버) 입장에서 여러 에이전트 프레임워크들과 연결하기 위해선 여러 에이전트에 맞춰 서비스를 생성해야 했으며 특정 에이전트가 업데이트되거나 양식이 수정되게 되면 해당 부분에 맞춰 수정해야했다.
이렇게 통합된 프로토콜이 존재하지 않았기에 에이전트에게 서비스를 제공하는Tool 서버입장에선 개발 복잡성의 증가와 중복 개발, 유지보수의 어려움 등을 가져왔으며 서로 다른 에이전트간의 협업이나 정보 교환의 어려움을 야기했다.
하지만 하나의 방식으로 통합된 프로토콜이 존재한다면 에이전트들과 툴 서버들이 소통하는 방식이 통일되어 위의 문제들을 해결 할 수 있게 되었으며 그러면서 등장한 것이 MCP 이다.
매우 자주 MCP에 대한 설명을 볼 때 USB 포트를 생각해보라는 예시들을 자주 보곤 한다.
USB 포트는 USB 를 통한 정보 교환을 허용하는 모든 기기들에 대해서 기기의 종류와 상관 없이 포트만 존재한다면 모든 데이터를 교환 할 수 있게 한다.
Model Context Protocol (이하 MCP) 는 2024년 11월 Anthropic 에서 소개된 프로토콜로 에이전트가 외부 서비스에 요청을 보낼 때 에이전트 별로 다르게 해석 될 수 있는 자연어 기반의 통신 대신, 명확하고 예측 가능하게 구조화된 형태의 데이터 로 통신 하도록 표준화 시킨 프로토콜을 의미한다.
MCP 또한 MCP 프로토콜을 준수하는 모든 Tools 서버와 에이전트가 있다면 통합적으로 사용 가능하도록 허용 한다. 즉 MCP는 에이전트와 Tool 서버 간의 통합되고 규격화 된 프로토콜 양식을 의미 한다.
MCP 는 크게 호스트, MCP 클라이언트 , MCP 서버로 나뉜다.
호스트는 여러 MCP 클라이언트 인스턴스를 생성하고 관리하는 컴포넌트이다.
사용자가 설정한 값에 맞춰 적합한 MCP 클라이언트 인스턴스를 생성하고 MCP 클라이언트들의 생명주기를 관리한다.
내가 사용하고자 하는 코파일럿의 예시로 들자면 코파일럿 자체가 하나의 에이전트가 된다.
MCP 클라이언트는 에이전트가 생성한 작은 컴포넌트로, 다른 클라이언트와 독립된 컴포넌트로서 MCP 서버와 1:1 로 연결 된다.
MCP 클라이언트는 호스트가 구동되는 위치에 존재하는 파일들을 읽거나 MCP 서버에게 보내거나 받은 데이터들을 샘플링 하는 역할을 한다.
예를 들어 코파일럿에게 내가 지금 적은 코드와 깃허브에 저장된 코드의 차이점을 분석하고 커밋으로 남겨줘 라고 했을 때 현재 코드베이스에 있는 코드를 읽고 깃허브 MCP 서버에게 요청을 보내기 위해 필요한 데이터를 구조화하고 직렬화하거나 받은 데이터를 에이전트에게 주기 위해 역직렬화 하는 등의 역할을 담당한다.
MCP 서버는 Tool 서버로서 내부 저장된 데이터들 혹은 로직들을 MCP 클라이언트에게 노출 시켜 MCP 클라이언트와 통신한다.
MCP 서버 , 즉 Tool 서버는 MCP 프로토콜에 맞춰 프롬프트를 처리하기도, 해당하는 리소스를 제공하기도 필요에 따른 실제 서비스 기능을 실행하기도 한다.
이 때 이름에서처럼 MCP 서버와 MCP 클라이언트 모두 MCP 에서 정의한 데이터 스킴인 JSON-RPC protocol을 따른다.
MCP 아키텍쳐는 client - host - server 컴포넌트들로 이뤄진 아키텍쳐를 따르며 각 컴포넌트간의 데이터 교환은 JSON-RPC 2.0
메시지 교환 방식을 이용해 이뤄진다.
매우 우리에게 익숙한 형태의 자료구조인 json
으로 리퀘스트와 리스폰스 , 알림등의 데이터의 형태는 다음과 같다.
// Request Message
{
"jsonrpc": "2.0",
"id": "string | number",
"method": "string",
"param?": {
"key": "value"
}
}
// Response Message
{
"jsonrpc": "2.0",
"id": "string | number",
"result?": {
"[key: string]": "unknown"
},
"error?": {
"code": "number",
"message": "string",
"data?": "unknown"
}
}
// Notify Message
{
"jsonrpc": "2.0",
"method": "string",
"params?": {
"[key: string]": "unknown"
}
}
MCP 클라이언트와 서버는 initialize -> operation -> close 단계를 거친다.
Initialize Stage 에선 각자 사용하는 프로토콜의 버전과 기타 메타 데이터들을 교환한다.
{
"jsonrpc": "2.0", // JSON-RPC 프로토콜 버전 (2.0)
"id": 1, // 요청의 고유 ID (응답과 매칭에 사용)
"method": "initialize", // 호출할 메서드 이름 (초기화)
"params": { // 메서드에 전달할 파라미터 (객체)
"protocolVersion": "2024-11-05", // 클라이언트가 지원/사용하려는 MCP 프로토콜 버전
"capabilities": { // 클라이언트가 지원하는 기능 (객체)
"roots": { // 루트 관련 기능 (객체)
"listChanged": true // 클라이언트가 루트 목록 변경 알림 수신 가능 여부 (true: 가능)
},
"sampling": {} // 샘플링 관련 기능 (객체, 현재 비어 있음)
},
"clientInfo": { // 클라이언트 정보 (객체)
"name": "ExampleClient", // 클라이언트 이름 또는 식별자
"version": "1.0.0" // 클라이언트 버전
}
}
}
{
"jsonrpc": "2.0", // JSON-RPC 프로토콜 버전 (2.0) - 요청과 동일해야 함
"id": 1, // 요청의 ID와 일치하여 응답이 어떤 요청에 대한 것인지 나타냄
"result": { // 요청 성공 시 결과 데이터를 담는 객체
"protocolVersion": "2024-11-05", // 서버가 합의한 MCP 프로토콜 버전 (클라이언트가 제시한 버전과 같거나 호환되는 버전)
"capabilities": { // 서버가 지원하는 기능 (객체)
"logging": {}, // 로깅 관련 기능 (객체, 현재 비어 있음 - 서버가 기본적인 로깅을 지원하거나 아직 특정 로깅 기능을 명시하지 않음)
"prompts": { // 프롬프트 관련 기능 (객체)
"listChanged": true // 서버가 프롬프트 목록 변경 알림 ($/prompts/listChanged)을 지원함 (true: 지원)
},
"resources": { // 리소스 관련 기능 (객체)
"subscribe": true, // 서버가 리소스 구독 기능을 지원함
"listChanged": true // 서버가 리소스 목록 변경 알림 ($/resources/listChanged)을 지원함
},
"tools": { // 툴 (외부 서비스) 관련 기능 (객체)
"listChanged": true // 서버가 사용 가능한 툴 목록 변경 알림 ($/tools/listChanged)을 지원함
}
},
"serverInfo": { // 서버 정보 (객체)
"name": "ExampleServer", // 서버 이름 또는 식별자
"version": "1.0.0" // 서버 버전
}
}
}
초기화 단계에서 클라이언트는 서버에게 사용 가능한 리소스와 툴들에 대한 요청을 보내고, 서버단에선 사용 가능 여부들을 전송한다.
이후 초기화 단계가 종료되고 나면 서버 단에선 초기화가 완료되었단 메시지를 보내며 초기화가 종료된다.
{
"jsonrpc": "2.0",
"method": "initialized"
}
이에 Operate Stage 단계에선 초기화 단계에서 교환한 정보를 토대로 MCP 클라이언트가 MCP 서버에게 요청을 보내며 데이터를 주고 받는다.
MCP 클라이언트 혹은 서버가 연결을 끊고자 할 땐 연결을 끊는 메시지를 전송하고 서로간의 연결을 끊는다.
이후 서로간의 저장되어있던 정보(LLM에 사용되었던)들을 모두 제거 하며 연결이 끝나게 된다.
MCP 에서 데이터의 형태는 JSON
형태로 주고 받는다는 것을 알았다.
이는 RestAPI 형태인 HTTP 와 비슷하지만 주고받는 방식이 조금 다르다.
두 가지 방식이 사용되는데 하나는 표준 입출력인 stdin , stdout
형태로 메시지를 주고 받거나 서버가 클라이언트에게 데이터를 전송하는 단방향 메시지 전송인 SSE
를 사용한다.
stdio
는 기본적인 입출력 스트림으로 MCP 클라이언트는 기본적으로 자식 프로세스로서 MCP 서버를 직접 구동 한다.
이건 일반적인 HTTP 에서 client - server
간의 원격 데이터 전송과 차이가 있다. 일반적인 HTTP
에선 원격으로 각자 client , server, repository
간 데이터 전송이 일어났다면 MCP
에선 서버 자체도 클라이언트가 구동되는 프로세스의 서브 프로세스로 실행되기 때문이다.
MCP 클라이언트는 구동된 자식 프로세스의 MCP 서버와 입출력 스트림인 stdin , stdout
을 이용하여 메시지를 주고 받는다.
기본적으로 최대한 MCP 클라이언트는 stdio
를 이용할 것을 권장한다. 참 신기하다. 이러한 방식을 택한 이유는 다음과 같다고 이야기 한다.
API key
와 같은 보안이 중요한 데이터를 직접 외부에 노출하지 않을 수 있다. SSE
또한 클라이언트의 서브 프로세스에서 구동 된다.
이 때의 MCP 서버는 두 개의 엔드포인트를 가져야 하는데 클라이언트 -> 서버 로 데이터를 받을 엔드포인트와 서버 -> 클라이언트로 데이터를 전송할 엔드포인트를 생성해야 한다.
MCP 클라이언트와 서버간 연결이 시작되면 클라이언트는 서버에게 데이터를 전송 할 엔드포인트 주소를 GET 요청으로 보내 받은 후, 엔드포인트를 통해 HTTP 형태로 데이터를 주고 받는다.
처음 찾아보기 전까진 MCP 에 대해서 무시무시하게 어렵게 생각했었는데, MCP 를 직접 개발하지 않고 사용 할 내 입장에서 개념만 가볍게 훑어봤을 때 MCP 자체는 단순히 이미 우리가 데이터를 주고 받을 때 사용하는 프로토콜인 JSON-RPC
을 이용해서 절차만 통합해둔 형태로 이해가 됐다.
그나저나 참 신기하다. 서버를 호스트 단에서 직접 구동한다는 아이디어가
어떤식으로 동작하는지는 감 잡았으니, 아무것도 모른채로 사용한다는 죄책감에서 벗어나 조금은 가벼운 마음으로 MCP 를 에이전트와 함께 사용해봐야겠다 호호