MCP와 코드 실행을 결합해 도구 정의와 중간 결과로 인한 토큰 비용과 지연을 크게 줄이고, 더 많은 도구를 안전하게 조합하는 방법을 사례와 코드로 설명합니다.
Model Context Protocol(MCP)은 AI 에이전트를 외부 시스템과 연결하기 위한 개방형 표준입니다. 전통적으로 에이전트를 도구와 데이터에 연결하려면 페어마다 커스텀 통합이 필요해 단편화와 중복 노력이 발생했고, 이는 진정한 연결형 시스템을 확장하기 어렵게 만들었습니다. MCP는 범용 프로토콜을 제공하여, 개발자가 에이전트에 MCP를 한 번 구현하면 전체 통합 생태계를 활용할 수 있게 해줍니다.
2024년 11월 MCP 출시 이후 채택은 빠르게 진행되었습니다. 커뮤니티는 수천 개의 MCP 서버를 만들었고, 모든 주요 프로그래밍 언어용 SDK가 제공되며, 업계는 에이전트를 도구와 데이터에 연결하는 사실상의 표준으로 MCP를 채택했습니다.
오늘날 개발자들은 수십 개의 MCP 서버에 걸쳐 수백~수천 개의 도구에 접근하는 에이전트를 일상적으로 구축합니다. 그러나 연결된 도구 수가 늘어날수록 모든 도구 정의를 선로드하고 중간 결과를 컨텍스트 윈도를 통해 전달하는 과정이 에이전트를 느리게 하고 비용을 증가시킵니다.
이 글에서는 코드 실행이 어떻게 에이전트가 MCP 서버와 더 효율적으로 상호작용하도록 도와 더 적은 토큰으로 더 많은 도구를 다룰 수 있게 하는지 살펴보겠습니다.
MCP 사용이 확장되면서 에이전트 비용과 지연을 키우는 두 가지 일반적인 패턴이 나타납니다.
대부분의 MCP 클라이언트는 모든 도구 정의를 컨텍스트에 직접 선로드하여, 모델이 직접 도구 호출 구문으로 접근하게 합니다. 이러한 도구 정의는 다음과 같을 수 있습니다:
gdrive.getDocument
설명: Google Drive에서 문서를 가져옵니다
매개변수:
documentId (필수, string): 가져올 문서의 ID
fields (선택, string): 반환할 특정 필드
반환: 제목, 본문 내용, 메타데이터, 권한 등으로 구성된 Document 객체
salesforce.updateRecord
설명: Salesforce에서 레코드를 업데이트합니다
매개변수:
objectType (필수, string): Salesforce 객체 유형(Lead, Contact, Account 등)
recordId (필수, string): 업데이트할 레코드의 ID
data (필수, object): 새 값으로 업데이트할 필드들
반환: 확인 정보를 포함한 업데이트된 레코드 객체
도구 설명은 컨텍스트 윈도 공간을 많이 차지해 응답 시간과 비용을 증가시킵니다. 에이전트가 수천 개의 도구와 연결된 경우, 요청을 읽기 전에 수십만 개의 토큰을 처리해야 할 수도 있습니다.
대부분의 MCP 클라이언트는 모델이 MCP 도구를 직접 호출하도록 허용합니다. 예를 들어 에이전트에게 “Google Drive에서 내 미팅 녹취록을 다운로드해서 Salesforce 리드에 첨부해줘.”라고 요청할 수 있습니다.
모델은 다음과 같은 호출을 수행합니다:
TOOL CALL: gdrive.getDocument(documentId: "abc123")
→ "Q4 목표 논의...
[전체 녹취 텍스트]"를 반환
(모델 컨텍스트에 로드됨)
TOOL CALL: salesforce.updateRecord(
objectType: "SalesMeeting",
recordId: "00Q5f000001abcXYZ",
data: { "Notes": "Q4 목표 논의...\n[전체 녹취 텍스트를 다시 작성]" }
)
(모델이 컨텍스트에 전체 녹취를 다시 써야 함)
모든 중간 결과가 반드시 모델을 거쳐야 합니다. 이 예시에서는 전체 녹취가 두 번 흐릅니다. 2시간짜리 영업 미팅이라면 추가로 50,000 토큰을 처리해야 할 수 있습니다. 더 큰 문서는 컨텍스트 윈도 한계를 초과해 워크플로를 깨뜨릴 수도 있습니다.
대용량 문서나 복잡한 데이터 구조의 경우, 모델이 도구 호출 간 데이터를 복사하는 과정에서 실수를 할 가능성도 커집니다.

MCP 클라이언트는 도구 정의를 모델의 컨텍스트 윈도에 로드하고, 각 도구 호출과 결과가 작업 사이마다 모델을 통과하도록 메시지 루프를 오케스트레이션합니다.
에이전트를 위한 코드 실행 환경이 보편화되면서, MCP 서버를 직접 도구 호출 대신 코드 API로 제시하는 해법이 주목받고 있습니다. 에이전트가 MCP 서버와 상호작용하기 위해 코드를 작성하게 하는 방식입니다. 이 접근은 두 가지 문제를 모두 해결합니다. 에이전트가 필요한 도구만 로드하고, 결과를 모델에 전달하기 전에 실행 환경에서 데이터를 처리할 수 있습니다.
이를 구현하는 방식은 다양합니다. 한 가지 접근은 연결된 MCP 서버의 모든 사용 가능한 도구로 파일 트리를 생성하는 것입니다. 다음은 TypeScript를 사용한 구현 예시입니다:
servers
├── google-drive
│ ├── getDocument.ts
│ ├── ... (기타 도구)
│ └── index.ts
├── salesforce
│ ├── updateRecord.ts
│ ├── ... (기타 도구)
│ └── index.ts
└── ... (기타 서버)
그런 다음 각 도구를 하나의 파일에 대응시킵니다. 예를 들어:
// ./servers/google-drive/getDocument.ts
import { callMCPTool } from "../../../client.js";
interface GetDocumentInput {
documentId: string;
}
interface GetDocumentResponse {
content: string;
}
/* Google Drive에서 문서를 읽습니다 */
export async function getDocument(input: GetDocumentInput): Promise<GetDocumentResponse> {
return callMCPTool<GetDocumentResponse>('google_drive__get_document', input);
}
위에서 든 Google Drive → Salesforce 예시는 다음과 같은 코드가 됩니다:
// Google Docs에서 녹취를 읽어 Salesforce 잠재 고객에 추가
import * as gdrive from './servers/google-drive';
import * as salesforce from './servers/salesforce';
const transcript = (await gdrive.getDocument({ documentId: 'abc123' })).content;
await salesforce.updateRecord({
objectType: 'SalesMeeting',
recordId: '00Q5f000001abcXYZ',
data: { Notes: transcript }
});
에이전트는 파일 시스템을 탐색하여 도구를 발견합니다. 즉, ./servers/ 디렉터리를 나열해 사용 가능한 서버(google-drive, salesforce 등)를 찾고, 필요한 특정 도구 파일(getDocument.ts, updateRecord.ts 등)을 읽어 각 도구의 인터페이스를 파악합니다. 이렇게 하면 현재 작업에 필요한 정의만 로드할 수 있습니다. 이로써 토큰 사용량을 150,000개에서 2,000개로 줄여 시간과 비용을 약 98.7% 절감할 수 있습니다.
Cloudflare는 MCP와의 코드 실행을 “Code Mode”라고 부르며 유사한 결과를 발표했습니다. 핵심 통찰은 같습니다. LLM은 코드를 작성하는 데 능숙하며, 개발자는 이 강점을 활용해 MCP 서버와 더 효율적으로 상호작용하는 에이전트를 구축해야 합니다.
MCP와 코드 실행을 결합하면, 도구를 온디맨드로 로드하고 모델에 전달하기 전에 데이터를 필터링하며, 복잡한 로직을 한 번에 실행할 수 있어 컨텍스트를 더욱 효율적으로 사용할 수 있습니다. 또한 이 접근은 보안과 상태 관리 측면에서도 이점이 있습니다.
모델은 파일 시스템 탐색에 능숙합니다. 도구를 파일 시스템상의 코드로 제시하면, 모든 도구 정의를 미리 읽는 대신 필요할 때만 정의를 읽어들일 수 있습니다.
또는 서버에 search_tools 도구를 추가해 관련 정의를 찾을 수도 있습니다. 예를 들어 위의 가상의 Salesforce 서버를 사용할 때, 에이전트는 "salesforce"를 검색하고 현재 작업에 필요한 도구만 로드합니다. search_tools 도구에 세부 수준 매개변수를 포함시켜(예: 이름만, 이름+설명, 스키마가 포함된 전체 정의 등) 에이전트가 필요한 상세 수준을 선택하게 하면 컨텍스트를 절약하고 도구를 효율적으로 찾는 데 도움이 됩니다.
대용량 데이터셋을 다룰 때, 에이전트는 모델에 반환하기 전에 코드에서 결과를 필터링하거나 변환할 수 있습니다. 10,000행짜리 스프레드시트를 가져오는 경우를 생각해 보세요:
// 코드 실행 없이 - 모든 행이 컨텍스트로 흘러들어옴
TOOL CALL: gdrive.getSheet(sheetId: 'abc123')
→ 10,000행을 컨텍스트로 반환, 수동 필터링 필요
// 코드 실행 사용 - 실행 환경에서 필터링
const allRows = await gdrive.getSheet({ sheetId: 'abc123' });
const pendingOrders = allRows.filter(row =>
row["Status"] === 'pending'
);
console.log(`보류 중 주문은 ${pendingOrders.length}건입니다`);
console.log(pendingOrders.slice(0, 5)); // 검토를 위해 처음 5개만 로그
에이전트는 10,000개 대신 5개의 행만 보게 됩니다. 이와 유사한 패턴은 집계, 여러 데이터 소스 간 조인, 특정 필드 추출에도 적용할 수 있으며, 컨텍스트 윈도를 부풀리지 않습니다.
반복, 조건, 오류 처리를 개별 도구 호출 체인 대신 익숙한 코드 패턴으로 수행할 수 있습니다. 예를 들어 배포 완료 알림을 Slack에서 확인해야 한다면, 에이전트는 다음과 같이 작성할 수 있습니다:
let found = false;
while (!found) {
const messages = await slack.getChannelHistory({ channel: 'C123456' });
found = messages.some(m => m.text.includes('deployment complete'));
if (!found) await new Promise(r => setTimeout(r, 5000));
}
console.log('배포 완료 알림을 수신했습니다');
이 접근은 MCP 도구 호출과 대기(sleep) 명령을 에이전트 루프에서 번갈아 실행하는 것보다 더 효율적입니다.
또한, 실행될 조건 분기를 코드로 작성해두면 “첫 토큰까지의 시간” 지연도 줄어듭니다. 모델이 if 문을 평가하기를 기다릴 필요 없이, 코드 실행 환경이 이를 처리할 수 있기 때문입니다.
에이전트가 MCP와 코드 실행을 사용할 때, 중간 결과는 기본적으로 실행 환경에 머무릅니다. 즉, 명시적으로 로그를 남기거나 반환하지 않는 한 에이전트는 해당 데이터를 볼 수 없으며, 모델의 컨텍스트에 들어가지 않고도 민감한 데이터가 워크플로를 통과할 수 있습니다.
더 민감한 워크로드의 경우, 에이전트 하니스가 민감 데이터를 자동으로 토큰화할 수 있습니다. 예를 들어 스프레드시트에서 고객 연락처 정보를 Salesforce로 가져와야 한다고 가정해 봅니다. 에이전트는 다음과 같이 작성합니다:
const sheet = await gdrive.getSheet({ sheetId: 'abc123' });
for (const row of sheet.rows) {
await salesforce.updateRecord({
objectType: 'Lead',
recordId: row.salesforceId,
data: {
Email: row.email,
Phone: row.phone,
Name: row.name
}
});
}
console.log(`총 ${sheet.rows.length}개의 리드를 업데이트했습니다`);
MCP 클라이언트는 모델에 도달하기 전에 데이터를 가로채 PII를 토큰화합니다:
[
{ salesforceId: '00Q...', email: '[EMAIL_1]', phone: '[PHONE_1]', name: '[NAME_1]' },
{ salesforceId: '00Q...', email: '[EMAIL_2]', phone: '[PHONE_2]', name: '[NAME_2]' },
...
]
그런 다음 데이터가 다른 MCP 도구 호출에서 공유될 때 MCP 클라이언트의 조회를 통해 비토큰화됩니다. 실제 이메일 주소, 전화번호, 이름은 Google Sheets에서 Salesforce로 흐르지만 모델을 통과하지는 않습니다. 이를 통해 에이전트가 실수로 민감 데이터를 로그로 남기거나 처리하는 일을 방지할 수 있습니다. 또한 데이터를 어디에서 어디로 보낼 수 있는지 결정적 보안 규칙을 정의하는 데에도 활용할 수 있습니다.
파일 시스템 접근이 가능한 코드 실행은 에이전트가 작업 간 상태를 유지하도록 해줍니다. 에이전트는 중간 결과를 파일에 기록해 작업을 재개하고 진행 상황을 추적할 수 있습니다:
const leads = await salesforce.query({
query: 'SELECT Id, Email FROM Lead LIMIT 1000'
});
const csvData = leads.map(l => `${l.Id},${l.Email}`).join('\n');
await fs.writeFile('./workspace/leads.csv', csvData);
// 이후 실행에서 이어서 진행
const saved = await fs.readFile('./workspace/leads.csv', 'utf-8');
에이전트는 자신의 코드를 재사용 가능한 함수로 저장할 수도 있습니다. 한 번 어떤 작업을 위한 동작하는 코드를 만들면, 그 구현을 저장해 두고 나중에 다시 사용할 수 있습니다:
// ./skills/save-sheet-as-csv.ts
import * as gdrive from './servers/google-drive';
export async function saveSheetAsCsv(sheetId: string) {
const data = await gdrive.getSheet({ sheetId });
const csv = data.map(row => row.join(',')).join('\n');
await fs.writeFile(`./workspace/sheet-${sheetId}.csv`, csv);
return `./workspace/sheet-${sheetId}.csv`;
}
// 이후, 어떤 에이전트 실행에서도:
import { saveSheetAsCsv } from './skills/save-sheet-as-csv';
const csvPath = await saveSheetAsCsv('abc123');
이는 스킬(Skills) 개념과 밀접하게 맞닿아 있습니다. 스킬은 모델이 특수화된 작업에서 성능을 높이기 위해 참조하고 사용할 수 있는 재사용 가능한 지침, 스크립트, 리소스로 구성된 폴더입니다. 이러한 저장 함수에 SKILL.md 파일을 추가하면 모델이 참조하고 사용할 수 있는 구조화된 스킬이 됩니다. 시간이 지남에 따라 에이전트는 더 높은 수준의 기능을 담은 도구 상자를 구축하며, 가장 효과적으로 작업하는 데 필요한 발판(scaffolding)을 발전시킬 수 있습니다.
코드 실행은 자체적인 복잡성도 도입한다는 점에 유의하세요. 에이전트가 생성한 코드를 실행하려면 적절한 샌드박싱, 리소스 제한, 모니터링을 갖춘 안전한 실행 환경이 필요합니다. 이러한 인프라 요구사항은 운영상의 오버헤드와 보안 고려 사항을 추가하며, 직접 도구 호출에서는 피할 수 있습니다. 코드 실행의 이점(토큰 비용 절감, 지연 감소, 도구 조합 개선)은 이러한 구현 비용과 저울질해야 합니다.
MCP는 에이전트가 많은 도구와 시스템에 연결될 수 있도록 하는 기초 프로토콜을 제공합니다. 그러나 너무 많은 서버가 연결되면 도구 정의와 결과가 과도한 토큰을 소비해 에이전트 효율을 떨어뜨릴 수 있습니다.
여기서 다룬 많은 문제들—컨텍스트 관리, 도구 조합, 상태 지속성—은 새로워 보이지만 소프트웨어 공학에 이미 해법이 있습니다. 코드 실행은 이러한 정립된 패턴을 에이전트에 적용해, 익숙한 프로그래밍 구문으로 MCP 서버와 더 효율적으로 상호작용하게 합니다. 이 접근을 구현한다면, MCP 커뮤니티와 경험을 공유해 주시기 바랍니다.
이 글은 Adam Jones와 Conor Kelly가 작성했습니다. 초안에 피드백을 제공해 준 Jeremy Fox, Jerome Swannack, Stuart Ritchie, Molly Vorwerck, Matt Samuels, Maggie Vo께 감사드립니다.