여러분은 이런 광고 문구를 본 적 있을 겁니다. "이 제품 하나면 다 됩니다!", "단 한 번의 클릭으로 해결!", "더 이상 고민하지 마세요!"... 그리고 대부분의 경우, 실제로는 그렇게 마법같은 일이 일어나지 않죠. 하지만 이번만큼은 예외인거 같습니다. 그 정체는 바로 MCP(Model Context Protocol)입니다.
"이제 MCP로 LLM에 전달할 외부 API를 규격화하세요!"
Anthropic 주도로 개발되고 오픈소스로 공개된 MCP는 마치 AI계의 USB-C 포트처럼 소개되고 있습니다. 다양한 기기를 표준화된 방식으로 연결하는 USB-C처럼, MCP는 LLM에 전달하는 외부 API의 규격화된 프로토콜을 제공합니다. 이 프로토콜은 https://github.com/modelcontextprotocol 에서 SDK 형태로 제공되어, 누구나 MCP 서버나 클라이언트를 쉽게 구현할 수 있습니다.
온 세상이 MCP 서버를 오픈하고 있습니다. Cursor와 같은 코딩 툴에서는 MCP를 통해 로컬 파일 시스템에 접근하여 코드를 분석하고 자동으로 코드를 작성해주는 기능을 선보이고 있습니다. Claude 데스크탑앱도 MCP 연동을 통해 외부 API를 활용하도록 지원하고 있으며, Microsoft는 Playwright MCP 서버 코드를 직접 오픈하여 웹 테스트 자동화를 지원하고 있습니다. 점점 더 많은 도구들이 이 프로토콜을 채택하며 대세가 되어가고 있습니다.
"에이, 그게 무슨 대수라고. 기존에도 API 연동 많이 했잖아요?"
맞습니다. LLM과 외부 도구를 연결하는 방법은 이전에도 있었습니다. 하지만 각 서비스마다 API 구조가 다르고, 인증 방식이 달라 통합하는 과정이 복잡했죠. MCP의 진가는 바로 이 지점에서 빛납니다. 규격화된 프로토콜을 통해 누구나 동일한 방식으로 API를 구현하고 연결할 수 있게 된 것입니다. 개발자라면 이런 표준화의 가치를 아실 겁니다. '모든 것이 연결된' AI 에코시스템의 꿈이 MCP를 통해 한 걸음 더 가까워진 것입니다.
예를 들어보죠. Cursor에서는 MCP를 통해 다음과 같은 대화가 가능합니다:
"내 프로젝트의 모든 Python 파일을 분석해서 중복 코드를 찾아줘"
"이 기능을 테스트하는 유닛 테스트를 작성해줘"
"이 API와 호환되는 클라이언트 코드를 자동으로 생성해줘"
MCP 서버를 통해 AI가 로컬 파일 시스템에 안전하게 접근할 수 있게 되면서, 이제 AI는 단순히 대화 상자에 붙여넣은 코드 조각이 아니라 전체 프로젝트 구조를 이해하고 맥락에 맞는 코드를 생성할 수 있게 되었습니다. 이것이 바로 개발자들이 "와, 이거 진짜 마법 같은데?"라고 반응하는 이유죠.
하지만 잠깐, MCP 서버만 구현하면 LLM이 촥 하고 달라붙어서 모든 기능을 자동으로 수행할까요? 이 역할도 MCP의 몫일까요?
물론 그렇지 않습니다. MCP는 결국 '프로토콜'일 뿐입니다. USB-C 포트가 있다고 해서 모든 기기가 자동으로 모든 기능을 수행하는 것은 아니죠. 케이블은 연결만 해줄 뿐, 각 기기가 어떤 기능을 수행할지는 또 다른 문제입니다.
그렇다면 MCP의 진짜 가치는 무엇일까요? 왜 이것이 중요한 걸까요? 더 나아가, MCP는 앞으로의 개발 패러다임을 어떻게 바꿀까요? 모든 사람이 AI 개발자가 되는 세상이 올까요? 전통적인 프로그래밍 기술 없이도 자신만의 AI 도구를 만들고 연결할 수 있는 미래가 도래할지도 모릅니다.
이제부터 Model Context Protocol의 오해와 진실, 그리고 어떤 가치가 있는지 MCP의 모든 것을 하나씩 파헤쳐보며, AI의 미래가 어떻게 변화할지 함께 살펴보겠습니다.
AI가 단순히 텍스트만 생성하던 시대는 지났습니다. 현대 LLM(Large Language Model)은 다양한 도구를 사용할 수 있는데, 이것이 바로 'Function Call'입니다. OpenAI가 선보인 이 개념은 AI가 특정 함수를 호출하여 외부 세계와 상호작용할 수 있게 해주었죠.
Function Call은 본질적으로 특별한 형태의 시스템 프롬프트입니다. 사용자의 질문이 들어오면 LLM은 이 질문이 Function Call을 호출해야 하는지 판단합니다. 만약 호출이 필요하다고 판단되면, LLM은 규격화된 인자(arguments)를 생성하고, 그렇지 않으면 일반적인 대화를 계속 진행합니다.
Function Call의 작동 방식을 도식화하면 다음과 같습니다:
간단한 예를 들어볼까요? OpenAI의 TypeScript SDK를 사용한 Function Call 예시입니다:
import OpenAI from 'openai';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
// 함수 정의
const functions = [
{
name: 'get_weather',
description: '특정 위치의 현재 날씨 정보를 가져옵니다',
parameters: {
type: 'object',
properties: {
location: {
type: 'string',
description: '도시 이름, 예: 서울, 뉴욕, 런던',
},
},
required: ['location'],
},
},
];
// 채팅 완성 요청
async function main() {
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{ role: 'user', content: '서울의 오늘 날씨는 어때?' }
],
tools: functions,
tool_choice: 'auto',
});
// Function Call 결과 확인
const message = completion.choices[0].message;
if (message.tool_calls) {
// AI가 함수를 호출하려고 한다면
const functionCall = message.tool_calls[0];
console.log(`함수 호출: ${functionCall.function.name}`);
console.log(`매개변수: ${functionCall.function.arguments}`);
// 여기서 실제 날씨 API를 호출하고 결과를 AI에게 돌려줄 수 있습니다
} else {
// 일반 텍스트 응답
console.log(message.content);
}
}
main();
이 코드에서 일어나는 일을 단계별로 살펴보면:
get_weather
라는 함수와 그 매개변수를 정의합니다.get_weather
함수를 호출하기로 결정하고, location: "서울"
이라는 인자를 생성합니다.이것이 바로 Function Call의 핵심입니다. LLM은 사용자의 의도를 파악하여 적절한 함수를 호출하고, 개발자는 실제로 그 함수를 구현하여 외부 세계와의 상호작용을 가능하게 합니다.
Function Call에 익숙해지셨다면, 이제 MCP(Model Context Protocol)를 만나볼 시간입니다. MCP는 LLM과 외부 도구 간의 통신을 표준화하는 프로토콜입니다. 그런데 이 추상적인 설명만으로는 MCP의 진짜 모습을 이해하기 어렵겠죠? 직접 코드를 살펴보며 MCP의 정체를 파헤쳐 봅시다.
MCP는 JSON-RPC 2.0 프로토콜을 기반으로 합니다. JSON-RPC란 간단히 말해 JSON 형식으로 원격 프로시저 호출(RPC)을 인코딩하는 경량 프로토콜입니다. 복잡한 말처럼 들리지만, 실제로는 매우 단순합니다:
각 요청과 응답은 다음과 같은 형태를 가집니다:
요청:
{
"jsonrpc": "2.0",
"id": "unique-request-id",
"method": "메서드 이름",
"params": { /* 매개변수 */ }
}
응답:
{
"jsonrpc": "2.0",
"id": "unique-request-id",
"result": { /* 결과 데이터 */ }
}
MCP는 이 JSON-RPC 형식을 사용하여 tools/list
, tools/call
등의 표준화된 메서드를 정의합니다. 공식 SDK(https://modelcontextprotocol.io/introduction)를 사용하면 쉽게 구현할 수 있습니다. 이를 통해 클라이언트와 서버는 일관된 방식으로 통신할 수 있죠.
tools/list는 MCP 서버에서 사용 가능한 도구 목록을 조회할 때 사용하는 메서드입니다. 요청과 응답 형식은 다음과 같습니다:
요청:
{
"jsonrpc": "2.0",
"id": "request-123",
"method": "tools/list"
}
응답:
{
"jsonrpc": "2.0",
"id": "request-123",
"result": {
"tools": [
{
"name": "readFile",
"description": "파일 내용을 읽어옵니다",
"inputSchema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "파일 경로"
}
},
"required": ["path"]
}
},
{
"name": "searchWeb",
"description": "웹에서 정보를 검색합니다",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "검색 쿼리"
},
"limit": {
"type": "number",
"description": "최대 결과 수",
"default": 5
}
},
"required": ["query"]
}
}
]
}
}
tools/call은 특정 도구를 호출할 때 사용하는 메서드입니다. 요청과 응답 형식은 다음과 같습니다:
요청:
{
"jsonrpc": "2.0",
"id": "request-456",
"method": "tools/call",
"params": {
"name": "searchWeb",
"arguments": {
"query": "최신 AI 기술 동향",
"limit": 3
}
}
}
응답:
{
"jsonrpc": "2.0",
"id": "request-456",
"result": {
"content": [
{
"type": "text",
"text": "최신 AI 기술 동향에 관한 검색 결과:\n1. 생성형 AI의 발전과 윤리적 고려사항\n2. 자율주행 기술의 최신 동향\n3. 의료 분야에서의 AI 활용 사례"
}
]
}
}
MCP 서버를 직접 구현한다면 어떻게 할까요? 가장 간단한 방법은 단일 엔드포인트(예: /messages
)를 노출하는 API를 만드는 것입니다. 이 엔드포인트는 요청 본문(body)을 해석하여 method
필드의 값에 따라 다른 응답을 반환합니다:
그런데 MCP 설계의 진정한 유연함은 기존 API를 새로 만들지 않고도 MCP 프로토콜에 맞게 사용할 수 있다는 점입니다. 이를 가능하게 하는 것이 바로 Transport 인터페이스입니다:
interface Transport {
// 통신 시작
start(): Promise<void>;
// JSON-RPC 메시지 전송 (요청 또는 응답)
send(message: JSONRPCMessage): Promise<void>;
// 연결 종료
close(): Promise<void>;
// 연결 종료 시 콜백
onclose?: () => void;
// 오류 발생 시 콜백
onerror?: (error: Error) => void;
// 메시지 수신 시 콜백
onmessage?: (message: JSONRPCMessage) => void;
// 세션 ID
sessionId?: string;
}
이 인터페이스를 구현하면 기존 API를 MCP 프로토콜에 맞게 변환할 수 있습니다. 즉, 클라이언트는 Transport를 통해 request를 보내고, 이 Transport가 해당 요청을 적절한 API 호출로 변환하는 것입니다:
이렇게 하면 서버를 새로 구현할 필요 없이, 클라이언트 측의 Transport만으로 MCP 프로토콜을 구현할 수 있습니다.
MCP SDK를 사용하여 클라이언트를 초기화하고 서버와 통신하는 방법을 살펴봅시다:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
// 클라이언트 초기화
const client = new Client(
{ name: "my-client", version: "1.0.0" },
);
// 기존 엔드포인트를 MCP로 변환하는 Transport
class ExistingAPITransport implements Transport {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
onmessage?: (message: JSONRPCMessage) => void;
onclose?: () => void;
onerror?: (error: Error) => void;
async start(): Promise<void> {
console.log("API Transport started");
}
async send(message: JSONRPCMessage): Promise<void> {
// 요청이 아니면 무시
if (!('method' in message)) return;
const request = message as JSONRPCRequest;
try {
let response;
// 기존 API 엔드포인트로 라우팅
if (request.method === 'tools/list') {
// /api/tools 엔드포인트로 변환
response = await fetch(`${this.baseUrl}/api/tools`);
const tools = await response.json();
// MCP 형식으로 변환
if (this.onmessage) {
this.onmessage({
jsonrpc: '2.0',
id: request.id,
result: { tools }
});
}
}
else if (request.method === 'tools/call') {
const { name, arguments: args } = request.params;
// /api/execute/[도구명] 엔드포인트로 변환
response = await fetch(`${this.baseUrl}/api/execute/${name}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(args)
});
const result = await response.json();
// MCP 형식으로 변환
if (this.onmessage) {
this.onmessage({
jsonrpc: '2.0',
id: request.id,
result: {
content: [{ type: 'text', text: JSON.stringify(result) }]
}
});
}
}
} catch (error) {
if (this.onerror) this.onerror(error);
if (this.onmessage) {
this.onmessage({
jsonrpc: '2.0',
id: request.id,
error: {
code: -32603,
message: error.message
}
});
}
}
}
async close(): Promise<void> {
if (this.onclose) this.onclose();
}
}
// 기존 API를 가리키는 Transport 연결
const transport = new ExistingAPITransport("https://api.example.com");
await client.connect(transport);
console.log("Connected to existing API through MCP");
// tools/list 요청 예시
const toolsListResult = await client.request(
{ method: "tools/list" },
ListToolsResultSchema
);
console.log("Available tools:", toolsListResult.tools);
// tools/call 요청 예시
const toolCallResult = await client.request(
{
method: "tools/call",
params: {
name: "searchWeb",
arguments: {
query: "최신 AI 기술 동향",
limit: 3
}
}
},
CallToolResultSchema
);
console.log("Search result:", toolCallResult.content[0].text);
물론, 서버 없이 로컬에서 직접 구현체를 만들 수도 있습니다. 간단히 로컬 객체의 메서드를 MCP 도구로 노출하는 방식으로 구현할 수 있죠:
// 로컬 서비스 객체
const localServices = {
getWeather(city) {
return `${city}의 날씨는 맑음, 기온 22도입니다`;
},
translate(text, targetLang) {
return `번역된 텍스트: ${text} (${targetLang}로)`;
}
};
// 로컬 서비스를 MCP 도구로 변환하는 Transport
class LocalServicesTransport implements Transport {
async start() {}
async close() {}
async send(message) {
if (!('method' in message)) return;
const req = message;
if (req.method === 'tools/list') {
this.onmessage({
jsonrpc: '2.0',
id: req.id,
result: {
tools: [
{
name: 'getWeather',
description: '도시의 날씨 정보 조회',
inputSchema: {/* 스키마 정의 */}
},
{
name: 'translate',
description: '텍스트 번역',
inputSchema: {/* 스키마 정의 */}
}
]
}
});
}
else if (req.method === 'tools/call') {
const result = localServices[req.params.name](...Object.values(req.params.arguments));
this.onmessage({
jsonrpc: '2.0',
id: req.id,
result: { content: [{ type: 'text', text: result }] }
});
}
}
}
// 로컬 서비스 연결
const localTransport = new LocalServicesTransport();
await client.connect(localTransport);
// 이제 로컬 서비스를 MCP 도구로 호출할 수 있습니다
const weatherResult = await client.request({
method: "tools/call",
params: {
name: "getWeather",
arguments: { city: "서울" }
}
});
console.log(weatherResult.content[0].text); // "서울의 날씨는 맑음, 기온 22도입니다"
MCP의 핵심 가치는 바로 여기에 있습니다. 기존에 존재하는 API나, 로컬 서비스, 또는 새로 구현한 서버 등 어떤 도구나 리소스든 Transport layer만 구현해놓으면, MCP client에서 정해진 메서드로 LLM Function Call에 필요한 내용들을 호출할 수 있습니다.
이것이 바로 '규격화'의 의미입니다. LLM이 외부 세계와 상호작용하는 방식을 일관되게 만들어 주는 것이죠. 개발자는 각 서비스마다 다른 방식의 함수 정의를 작성할 필요 없이, MCP 프로토콜만 따르면 됩니다.
Function Call을 사용할 때는 각 API마다 고유한 함수 정의를 작성해야 했지만, MCP를 사용하면 모든 도구와 리소스에 단일한 인터페이스로 접근할 수 있습니다. 이는 마치 USB 표준이 다양한 하드웨어를 연결하는 방식을 표준화한 것과 유사합니다.
MCP는 다음과 같은 이점을 제공합니다:
이러한 이점을 통해 MCP는 LLM과 외부 도구 간의 통신을 더욱 효율적으로 만들어 줍니다. Function Call이 첫걸음이었다면, MCP는 그 다음 단계로의 도약을 의미합니다.
지금까지 MCP가 얼마나 멋진 프로토콜인지 살펴봤습니다. 표준화된 인터페이스로 외부 도구와 리소스에 접근하는 방법을 제공한다니, 정말 훌륭하죠? 하지만 이쯤에서 한 가지 중요한 사실을 짚고 넘어가야 합니다.
이쯤 되면 눈치채셨을지도 모르겠습니다. MCP에는 LLM이 없습니다. 그저 외부 리소스를 연결하는 일관된 인터페이스를 제공할 뿐이죠. 즉, LLM과의 연결, 대화 관리, 그리고 실제 도구 호출의 로직은 모두 호스트 앱에서 직접 구현해야 합니다.
다시 말해, MCP는 도구를 표준화된 방식으로 노출하는 방법을 제공하지만, 그 도구를 언제, 어떻게 사용할지는 호스트 앱의 책임입니다. 이 부분이 바로 Claude Desktop, Cursor, 그리고 다양한 AI 서비스들이 독자적으로 구현해야 하는 부분입니다.
호스트 앱에서 구현해야 하는 주요 로직을 살펴보겠습니다:
// 기본 구조
class MCPHostApp {
constructor() {
this.mcpClient = new MCPClient(); // MCP 클라이언트
this.llm = new LLMClient(); // LLM 클라이언트 (Claude, GPT 등)
}
async processUserQuery(query) {
// 1. MCP 서버에서 도구 목록 가져오기
const tools = await this.mcpClient.request({ method: "tools/list" });
// 2. LLM에 도구 목록 전달하고 쿼리 처리 요청
const llmResponse = await this.llm.processQuery(query, tools);
// 3. LLM이 도구 호출을 요청했다면
if (llmResponse.hasFunctionCall) {
// 4. MCP를 통해 도구 호출
const result = await this.mcpClient.request({
method: "tools/call",
params: {
name: llmResponse.functionName,
arguments: llmResponse.functionArgs
}
});
// 5. 도구 호출 결과를 LLM에 다시 전달
return await this.llm.processFunctionResult(result);
}
// 도구 호출이 없다면 LLM 응답 그대로 반환
return llmResponse.text;
}
}
이 단순한 코드에서도 호스트 앱이 해야 할 일이 많다는 것을 알 수 있습니다:
이것이 바로 Claude Desktop, Cursor, Visual Studio Code 확장 프로그램과 같은 AI 도구들이 모두 독자적으로 구현해야 하는 부분입니다. MCP는 단지 도구와 리소스에 접근하는 표준화된 방법만 제공할 뿐, 실제로 유용한 AI 경험을 만들기 위한 로직은 호스트 앱이 담당해야 합니다.
요약하자면, MCP는 혼자서는 아무것도 할 수 없습니다. 실제로 가치를 발휘하려면 잘 설계된 호스트 앱이 필요하죠. 그래서 다양한 AI 서비스들이 각자의 방식으로 호스트 앱을 구현하고 있는 것입니다.
MCP 서버가 도구를 제공하고 LLM이 이를 호출한다면 모든 문제가 해결될까요? 단순히 Function Call 목록을 LLM에게 전달하고 호출하게 하면 모든 것을 완벽하게 수행할 수 있을까요?
현실은 그렇게 간단하지 않습니다. LLM의 진정한 능력을 끌어내기 위해서는 효과적인 LLM 워크플로우 설계가 필수적입니다.
가장 단순한 예시부터 생각해 봅시다. MCP를 통해 100개 또는 1000개의 도구를 사용할 수 있다고 가정해 보겠습니다. 이 모든 도구 정의를 한 번에 LLM에게 전달하면 어떻게 될까요?
// ❌ 비효율적인 방식: 모든 도구를 한 번에 LLM에 전달
const allTools = await mcpClient.request({ method: "tools/list" });
const llmResponse = await llm.processQuery(query, allTools); // 수많은 도구로 LLM이 혼란!
이런 접근 방식은 두 가지 문제를 일으킵니다:
효과적인 호스트 앱은 이 문제를 다음과 같이 해결할 수 있습니다:
// ✅ 효율적인 방식: 쿼리 기반으로 관련 도구만 선택하여 제공
async function processWithRelevantTools(query) {
// 1. 모든 도구 목록 가져오기
const allTools = await mcpClient.request({ method: "tools/list" });
// 2. 쿼리 임베딩 생성
const queryEmbedding = await generateEmbedding(query);
// 3. 도구 설명의 임베딩과 비교하여 관련 도구만 선택
const relevantTools = selectRelevantTools(allTools, queryEmbedding);
// 4. 선택된 도구만 LLM에 제공
return await llm.processQuery(query, relevantTools);
}
이것은 호스트 앱이 단순한 중계자가 아니라 지능형 조정자로서 작동해야 함을 보여주는 한 가지 예시일 뿐입니다.
LLM을 운용하는 방식은 아주 다양한 방법론이 있습니다. 그중 가장 널리 알려진 패턴 중 하나는 ReAct(Reasoning + Acting)입니다.
ReAct 패턴에서 LLM은:
이 과정은 반복적으로 수행되며, 각 단계에서 LLM은 자신의 행동을 설명하고 결과를 평가합니다.
그러나 ReAct는 시작일 뿐입니다. 다양한 워크플로우 패턴이 있습니다:
Windsurf가 Cursor라는 경쟁자를 제치고 인기를 얻은 이유 중 하나는 이러한 LLM 에이전트 협업 관계를 효과적으로 설계했기 때문입니다.
최근 OpenAI, Perplexity, Grok과 같은 서비스에서 볼 수 있는 딥 리서치 기능도 잘 설계된 워크플로우의 한 예입니다:
이 워크플로우에서는:
이런 방식은 단순한 질의응답보다 훨씬 더 정확하고 최신 정보를 제공할 수 있습니다.
이러한 복잡한 워크플로우를 구현하기 위한 다양한 프레임워크가 존재합니다:
// AutoGen 스타일의 다중 에이전트 구현 예시 (수도 코드)
const codeWriter = createAgent({
role: "코드 작성자",
systemPrompt: "당신은 사용자의 요구사항에 맞는 코드를 작성하는 전문가입니다..."
});
const codeReviewer = createAgent({
role: "코드 리뷰어",
systemPrompt: "당신은 코드의 품질과 보안을 검토하는 전문가입니다..."
});
// 에이전트 간 협업 워크플로우 설정
setupWorkflow([
codeWriter.writeInitialCode,
codeReviewer.reviewCode,
codeWriter.improveBadedOnFeedback,
// ...
]);
저희 팀에서도 글쓰기 워크플로우를 구현하기 위해 독자적으로 프레임워크를 만들어서 사용하고 있습니다.
이 모든 맥락에서 MCP의 역할은 무엇일까요? MCP는 외부 세계와 연결될 단일 인터페이스를 제공하는 것일 뿐입니다. MCP 자체가 멀티에이전트 시스템이나 에이전트 워크플로우를 구성하지는 않습니다.
각 MCP 호스트(Claude Desktop, Cursor, Contiue 등)는 자신만의 고유한 워크플로우를 개발하여 LLM의 능력을 최대한 끌어올립니다:
결론적으로, MCP는 매우 유용한 표준화 프로토콜이지만, 진정한 가치는 이를 활용하는 호스트 앱의 워크플로우 설계에 있습니다. LLM의 능력을 최대한 활용하려면, 단순히 도구를 연결하는 것을 넘어서 지능적인 워크플로우를 설계해야 합니다. 이것이 바로 호스트 앱이 전부인 이유입니다.
지금까지 우리는 MCP가 tools/list
와 tools/call
을 통해 LLM에게 도구를 제공하는 방법에 대해 살펴봤습니다. 하지만 MCP의 야망은 이에 그치지 않습니다. 단순한 도구 호출을 넘어 더 복잡하고 정교한 워크플로우까지 MCP로 구현할 수 있는 가능성이 열리고 있습니다.
MCP 프로토콜을 자세히 들여다보면, 도구 호출만이 유일한 기능이 아님을 알 수 있습니다. 다양한 JSON-RPC 메서드가 정의되어 있는데, 그중에는 이런 것들도 있습니다:
**프롬프트 관련**
* `prompts/get`: 특정 프롬프트 가져오기
* `prompts/list`: 사용 가능한 프롬프트 목록 조회
**리소스 관련**
* `resources/list`: 리소스 목록 조회
* `resources/templates/list`: 리소스 템플릿 목록 조회
* `resources/read`: 리소스 읽기
* `resources/subscribe`: 리소스 변경 구독
* `resources/unsubscribe`: 리소스 구독 취소
눈여겨봐야 할 부분은 바로 prompts/get
과 prompts/list
입니다. 이는 MCP 서버가 단순히 도구만 제공하는 것이 아니라, 프롬프트도 제공할 수 있음을 의미합니다. MCP 서버에서 제공하는 프롬프트를 호스트 앱이 LLM에게 전달하는 것이죠.
더 놀라운 것은 sampling/createMessage
와 같은 메서드의 존재입니다. 이 메서드를 통해 MCP 서버가 호스트 앱에게 LLM 생성을 요청할 수 있습니다:
{
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "What files are in the current directory?"
}
}
],
"systemPrompt": "You are a helpful file system assistant.",
"includeContext": "thisServer",
"maxTokens": 100
}
}
이것은 무엇을 의미할까요? MCP 서버가 단순한 도구 제공자를 넘어서서, 적극적으로 대화에 참여하는 에이전트가 될 수 있다는 것입니다. 서버가 질문을 하고, 호스트는 LLM을 통해 답변을 제공하며, 필요하다면 사용자를 루프에 포함시킬 수도 있습니다(Human-in-the-loop).
이러한 기능들이 시사하는 바는 명확합니다. 복잡한 멀티에이전트 워크플로우를 구성하는 다양한 전략들이 이제 MCP 서버로 이동할 수 있습니다. 여러 MCP 서버들이 각자의 역할을 담당하고, prompts/list
와 prompts/get
으로 프롬프트를 제공하며, sampling/createMessage
로 호스트에 LLM 생성을 요청하는 방식으로 워크플로우를 구성할 수 있습니다.
이런 접근 방식에서는:
prompts/list
와 prompts/get
으로 자신의 역할에 맞는 프롬프트를 제공합니다sampling/createMessage
를 통해 호스트 앱에 LLM 생성을 요청합니다{
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "현재 디렉토리에 어떤 파일들이 있나요?"
}
}
],
"systemPrompt": "당신은 도움이 되는 파일 시스템 어시스턴트입니다.",
"includeContext": "thisServer",
"maxTokens": 100
}
}
이 기능이 특별한 이유는 Human-in-the-loop 설계를 자연스럽게 지원하기 때문입니다. 샘플링 요청이 들어오면:
이를 통해 사용자는 LLM이 무엇을 보고 생성하는지에 대한 통제권을 유지하면서도, MCP 서버가 복잡한 에이전트 행동을 수행할 수 있게 됩니다.
이처럼 멀티에이전트 워크플로우의 핵심 전략들을 MCP 서버로 옮기면, 동일한 워크플로우를 다양한 호스트 앱에서 재사용할 수 있게 됩니다.
더 나아가, 복잡한 워크플로우 자체를 MCP 서버로 패키징하여 공유 가능한 형태로 제공할 수도 있습니다. 기존에는 호스트 앱에서 구현해야 했던 워크플로우 로직을 MCP 서버로 옮겨서, 다른 호스트 앱들이 쉽게 이용할 수 있게 만드는 것입니다.
예를 들어, 앞서 살펴본 딥 리서치 워크플로우가 호스트 앱에 구현되어 있었다면, 이제는 이를 MCP 서버로 패키징할 수 있습니다:
이 구조에서 딥 리서치 MCP 서버는 복잡한 워크플로우 로직을 모두 내부적으로 처리하지만, 외부로는 단순한 tools/list
와 tools/call
인터페이스만 노출합니다. 호스트 앱은 deepResearch
라는 단일 도구만 보게 되며, 복잡한 내부 구현은 숨겨집니다.
실제로는 딥 리서치 서버 내부에서 다양한 방식으로 구현될 수 있습니다. 하드코딩된 로직일 수도 있고, MCP 프로토콜을 십분 활용한 정교한 구현일 수도 있습니다. 이 서버는 다음과 같은 복잡한 작업을 수행합니다:
하지만 호스트 앱의 관점에서는 그저 deepResearch
라는 단일 도구만 호출할 뿐입니다. 이처럼 MCP를 통해 복잡한 워크플로우를 캡슐화하고 재사용 가능한 형태로 제공할 수 있습니다. 다른 개발자들은 내부 구현을 이해할 필요 없이 자신의 호스트 앱에서 이 기능을 쉽게 활용할 수 있게 됩니다.
이러한 접근 방식의 가치는 명확합니다. 개발자들은 자신의 입맛에 맞는 워크플로우를 MCP 서버로 공개하고, 다른 개발자들은 이를 조합하여 새로운 AI 경험을 만들 수 있습니다. 마치 맛집 레시피를 공유하듯, AI 워크플로우 레시피를 공유하는 생태계가 형성될 수 있는 것이죠.
예를 들어:
이렇게 되면 AI 개발의 미래는 호스트 앱을 처음부터 개발하는 것이 아니라, 다양한 MCP 서버 조합을 찾는 것만으로 구성하는 방향으로 진화할 수 있습니다.
지금까지 MCP(Model Context Protocol)의 현재를 살펴봤다면, 이제는 미래를 상상해 볼 차례입니다. AI 개발 패러다임이 어떻게 변화할지, MCP가 어떤 역할을 할지 생각해보면 흥미로운 가능성이 펼쳐집니다.
가장 먼저 떠오르는 미래는 'AI 개발의 레고화'입니다. 오픈소스 커뮤니티와 상업 업체들이 다양한 MCP 서버 블록을 만들기 시작하면 어떻게 될까요? 개발자들은 이 블록들을 조합하여 복잡한 AI 애플리케이션을 구축할 수 있을 것입니다.
예를 들어:
이들을 조합하면 "AI 연구 어시스턴트"나 "코드 리팩토링 도우미" 같은 특화된 애플리케이션을 빠르게 만들 수 있습니다. 마치 레고 블록을 조립하듯, 필요한 MCP 서버들을 연결하기만 하면 되는 세상이 오는 것이죠.
두 번째로, AI 워크플로우 오케스트레이션이 새로운 경쟁의 장이 될 것입니다. 현재는 도구를 만들거나 호스트 앱을 개발하는 데 초점이 맞춰져 있지만, 미래에는 다양한 MCP 서버들을 조율하는 '오케스트레이터'가 중요해질 수 있습니다.
이러한 오케스트레이터는:
언젠가는 "이 오케스트레이터가 더 효율적인 워크플로우를 구성해요"라는 경쟁이 일어날지도 모릅니다.
세 번째로, MCP 서버 마켓플레이스가 등장할 가능성이 큽니다. 마치 앱스토어처럼, 전문화된 MCP 서버를 제공하는 생태계가 형성될 수 있습니다. 법률, 의료, 금융 등 각 분야별로 특화된 MCP 서버가, 무료부터 구독형까지 다양한 모델로 제공될 것입니다.
이는 AI 개발의 민주화로 이어질 수 있습니다. 기술적 지식이 없는 사람들도 호스트 앱을 통해 필요한 MCP 서버를 구독하고 조합하여 자신만의 AI 워크플로우를 구성할 수 있게 될 테니까요.
네 번째로, 어쩌면 호스트 앱과 MCP 서버의 경계가 점점 흐려질 수도 있습니다. 사용자들은 특정 앱을 사용하는 것이 아니라, 필요에 따라 다양한 AI 워크플로우를 선택하게 될 수 있습니다.
"오늘은 연구 논문을 읽어야 하니 학술 워크플로우를 활성화해야겠다"
"지금은 코딩 중이니 개발 워크플로우로 전환해야겠다"
앱 단위가 아닌 '워크플로우 단위'로 AI 경험을 소비하는 세계가 오지 않을까요?
MCP는 아직 발전 중인 기술이며, 앞으로 어떻게 진화할지는 커뮤니티와 개발자들의 손에 달려 있습니다. 현재 시점에서 MCP를 탐색한다면 고려해볼 만한 방향은 다음과 같습니다:
표준의 진화 지켜보기: MCP는 계속 발전하고 있습니다. 이 표준이 어떻게 진화하는지 지켜보는 것은 AI 개발의 미래 방향을 이해하는 데 도움이 될 수 있습니다.
모듈성 실험하기: 솔루션을 설계할 때 모듈화를 고려해보는 것은 언제나 좋은 접근법입니다. 어떤 부분이 독립적인 MCP 서버로 분리될 수 있을지 실험해볼 가치가 있습니다.
커뮤니티 참여하기: MCP와 관련된 오픈소스 프로젝트와 커뮤니티에 참여하면 다양한 아이디어와 사용 사례를 접할 수 있습니다.
MCP는 단순한 프로토콜이 아닐 수 있습니다. 이는 AI 개발의 새로운 패러다임, 더 모듈화되고, 더 협업적이며, 더 접근하기 쉬운 AI 생태계를 향한 한 걸음일지도 모릅니다.
너무 상세하고 맛있는 설명에 mcp의 이해도가 확실히 올라갔습니다. 진심으로 감사드립니다.