gajae-code · MCP 통합 (외부 도구 서버)
한 줄 요약
MCP는 “AI가 외부 도구를 꽂아 쓰는 표준 콘센트”이고, gajae-code는 그 콘센트에 꽂힌 깃허브·브라우저·DB 같은 도구를 자기 내부 도구처럼 자연스럽게 빌려 쓴다. 왜 배우나 — 에이전트의 능력을 코드 수정 없이 외부 서버로 확장하는 표준 통로라서, 이걸 알면 “어떤 도구든 붙일 수 있는 에이전트”를 설계할 수 있다.
그림
flowchart TD A["설정 파일 발견<br/>.mcp.json / mcp.json"] --> B["연결 준비<br/>인증 토큰·환경변수 주입"] B --> C["서버에 접속<br/>initialize 인사 → 준비 완료"] C --> D["도구 목록 받기<br/>tools/list"] D --> E["도구 포장<br/>mcp__서버명_도구명 으로 이름 붙이기"] E --> F["이름순 정렬<br/>(프롬프트 캐시 안정성)"] F --> G["에이전트 도구 시스템에 등록<br/>모델에게 노출"] D -. "250ms 안에 응답 못 하면" .-> H["디스크 캐시에서 옛 목록 꺼내기<br/>(설정 해시로 신선도 검증)"] H --> I["나중에 연결 버전<br/>DeferredMCPTool 먼저 노출"] I --> F G --> J["모델이 mcp__... 도구 호출"] J --> K["execute → tools/call 실행<br/>끊기면 재연결 후 1회 재시도"]
쉽게 풀기
MCP(Model Context Protocol)를 가전제품 비유로 풀어 보자.
- 콘센트 규격(MCP) — 나라마다 콘센트 모양이 다르면 가전을 못 꽂는다. MCP는 “도구를 꽂는 표준 모양”을 정한 약속이다. 깃허브든 브라우저든 이 규격만 맞추면 에이전트가 꽂아 쓸 수 있다.
- 벽에 붙은 콘센트(MCP 서버) — 외부 기능 하나하나가 “MCP 서버”라는 작은 프로그램으로 떠 있다. 각 서버는 자기가 제공하는 도구 목록을 들고 있다.
- 어떤 콘센트가 있는지 찾기(디스커버리) — 에이전트는 먼저
.mcp.json같은 설정 파일을 읽어 “이 집에 어떤 콘센트(서버)가 있나”를 파악한다. - 플러그 꽂고 인사하기(연결) — 서버에 접속해
initialize라는 인사 메시지를 보내고, 서버가 답하면 연결이 성립된다. 이때 인증 토큰이나 환경변수도 같이 건넨다. - 빌려온 도구에 이름표 붙이기(어댑터) — 서버가 내놓은 도구 하나하나를 에이전트가 부를 수 있는 형태(
MCPTool)로 포장하고,mcp__서버명_도구명형식의 이름표를 붙인다. 이러면 “이건 puppeteer 서버에서 빌려온 screenshot 도구”라는 게 이름만 봐도 드러난다. - 느린 콘센트 대비(지연 노출) — 어떤 서버는 켜지는 데 시간이 걸린다. 시작할 때 250ms 안에 연결이 안 되면, 지난번에 저장해 둔 도구 목록(캐시)으로 “일단 이름만 띄우고, 실제 연결은 막상 호출할 때 하는” 임시 버전(
DeferredMCPTool)을 먼저 보여준다. 그래야 느린 서버 하나 때문에 전체 시작이 멈추지 않는다.
도구가 너무 많을 때
서버 수십 개면 도구가 수백 개가 된다. 이걸 전부 모델에게 보여주면 토큰(설명문 비용)이 폭발한다. 그래서 검색 모드가 켜지면 모든 도구를 펼치는 대신
search_tool_bm25라는 “도구 검색 도구” 하나만 올려두고, 모델이 자연어로 검색하면 그때 관련 도구만 골라 보여준다.
핵심 정리
.mcp.json 서버 항목의 주요 필드 (discovery/mcp-json.ts, runtime-mcp/types.ts):
| 필드 | 의미 | 비고 |
|---|---|---|
| (키) 서버이름 | 서버 식별자 | 도구 이름 prefix·캐시 키가 됨 |
type | 연결 방식 | stdio(기본) / sse / http |
command·args | 실행할 명령 | stdio일 때 필수 |
url·headers | 원격 엔드포인트 | http/sse일 때 url 필수 |
auth | 인증 설정 | OAuth / API키 |
부품별 역할 분담:
- loader.ts — 진입점. MCP 세계와 에이전트의 “커스텀 툴” 세계를 잇는 어댑터 경계.
- manager.ts — 라이프사이클 두뇌. 설정 로드 → 병렬 연결 → 목록 받기 → 래핑 → 재연결/재시도. 도구를 이름순 정렬해 프롬프트 캐시가 깨지지 않게 한다.
- client.ts — 프로토콜 담당.
initialize/tools/list/tools/call및 서버→클라 요청(ping등) 처리. 프로토콜 버전2025-03-26. - transports/ — 바이트 계층.
stdio.ts(자식 프로세스 입출력),http.ts(Streamable HTTP+SSE, 401/403 시 토큰 갱신 후 재시도).
안전장치 3종
(a) 이름 충돌 방지 —
createMCPToolName이 서버 prefix를 붙이고 중복 prefix는 제거한다. (b) 캐시 신선도 — 설정의 SHA-256 해시로 검증해, 설정이 바뀌면 옛 캐시를 자동 무효화한다. (c) 잘못된 설정 격리 —validateServerConfig가 틀린 항목을 연결 전에 errors로 걸러낸다.
실제 예시
최소 .mcp.json (로컬 stdio 서버 + 원격 OAuth 서버):
{
"$schema": "https://raw.githubusercontent.com/can1357/gajae-code/main/packages/coding-agent/src/config/mcp-schema.json",
"mcpServers": {
"puppeteer": {
"type": "stdio",
"command": "bunx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"],
"env": { "PUPPETEER_HEADLESS": "true" },
"timeout": 30000
},
"remote-api": {
"type": "http",
"url": "https://mcp.example.com/v1",
"headers": { "X-Tenant": "${TENANT_ID}" },
"auth": {
"type": "oauth",
"credentialId": "remote-api-oauth",
"tokenUrl": "https://auth.example.com/oauth/token",
"clientId": "abc123"
}
}
}
}MCP 도구를 에이전트 도구(AgentTool)로 포장하는 어댑터의 핵심:
// packages/coding-agent/src/runtime-mcp/tool-bridge.ts
export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
readonly name: string;
readonly label: string;
readonly mcpToolName: string; // 정규화 전 원본 — tools/call 에 그대로 전달
readonly mcpServerName: string;
constructor(
private connection: MCPServerConnection,
private readonly tool: MCPToolDefinition,
private readonly reconnect?: MCPReconnect,
) {
this.name = createMCPToolName(connection.name, tool.name); // mcp__<server>_<tool>
this.label = `${connection.name}/${tool.name}`;
this.description = tool.description ?? `MCP tool from ${connection.name}`;
this.parameters = normalizeSchemaForMCP(tool.inputSchema) as TSchema;
this.mcpToolName = tool.name;
this.mcpServerName = connection.name;
}
async execute(_toolCallId, params, _onUpdate, _ctx, signal) {
throwIfAborted(signal);
const args = normalizeToolArgs(params);
try {
const result = await callTool(this.connection, this.tool.name, args, { signal });
return buildResult(result, this.connection.name, this.tool.name, /*provider*/…);
} catch (error) {
rethrowIfAborted(error, signal);
// 네트워크/세션 끊김(econnreset, "transport closed", HTTP 404/502/503)이면
// reconnect() 후 1회 재시도하고, 새 연결로 this.connection 을 갈아끼운다.
if (this.reconnect && isRetriableConnectionError(error)) { … }
return buildErrorResult(error, this.connection.name, this.tool.name, …);
}
}
}도구 이름 생성 규칙 — 서버명과 도구명이 겹치면 중복 prefix를 떼어낸다:
// packages/coding-agent/src/runtime-mcp/tool-bridge.ts
export function createMCPToolName(serverName: string, toolName: string): string {
const sanitizedServerName = sanitizeMCPToolNamePart(serverName, "server");
const sanitizedToolName = sanitizeMCPToolNamePart(toolName, "tool");
// 서버 "puppeteer" + 도구 "puppeteer_screenshot" → mcp__puppeteer_screenshot (중복 prefix 제거)
const prefixWithUnderscore = `${sanitizedServerName}_`;
let normalizedToolName = sanitizedToolName;
if (sanitizedToolName.startsWith(prefixWithUnderscore)) {
normalizedToolName = sanitizedToolName.slice(prefixWithUnderscore.length);
}
return `mcp__${sanitizedServerName}_${normalizedToolName}`;
}내가 직접 어댑터를 만든다면, 결과에 “영수증”(어느 서버의 어느 도구에서 나왔는지)을 꼭 첨부한다:
// 직접 구현 시 최소 골격
class MyMCPTool {
name: string; // 반드시 mcp__<server>_<tool> 패턴 (search/필터가 prefix로 식별)
label: string; // <server>/<tool>
parameters: TSchema; // normalizeSchemaForMCP(tool.inputSchema)
mcpToolName: string; // 정규화 전 원본 — tools/call 에 그대로 전달
mcpServerName: string;
async execute(id, params, onUpdate, ctx, signal) {
const result = await callTool(this.connection, this.mcpToolName, params ?? {}, { signal });
// 영수증: 결과에 serverName/mcpToolName/isError/rawContent 첨부
return { content: [{ type: "text", text: formatContent(result.content) }],
details: { serverName: this.mcpServerName, mcpToolName: this.mcpToolName, isError: result.isError, rawContent: result.content } };
}
}직접 만들 때 체크리스트:
- 도구 이름은
mcp__<server>_<tool>—isMCPToolName/isMCPBridgeTool이 prefix로 식별하므로 필수. - 서버명이 도구명 prefix와 겹치면 중복 제거(
createMCPToolName규칙) 적용. -
inputSchema는normalizeSchemaForMCP로 변환해parameters에 넣을 것. - 결과·에러 모두
details(영수증)에serverName·mcpToolName·isError첨부. - 재시도: 네트워크/세션 끊김 패턴(
isRetriableConnectionError)만reconnect()후 1회. abort는 즉시 re-throw. - 시작 지연 대비: 도구 정의를 config-해시 키로 캐시(TTL 30일)하고, 250ms 초과 시
DeferredMCPTool로 노출. - 도구 배열은 매 변경마다 이름순 정렬(프롬프트 캐시 안정성).
- HTTP+OAuth면
transport.onAuthError를 wiring해 401/403 시 토큰 갱신 헤더 반환.
OAuth와 Smithery 레지스트리 (참고)
OAuth 자동화 — HTTP 요청이 401/403이면 헤더·본문에서 인증 서버 위치를 파싱하고, 실패하면
/.well-known/oauth-authorization-server등을 순회 탐색한다. 이후 PKCE(S256) 기반 authorization-code 플로우(필요 시 동적 클라이언트 등록)로 토큰을 받아 agent.db에 저장하고, 만료 5분 전 또는 401 시 선제 갱신한다. (oauth-discovery.ts,oauth-flow.ts) Smithery 레지스트리 —registry.smithery.ai를 검색해 외부 서버를 찾고MCPServerConfig를 자동 생성한다. http 직결이 가능하면 그대로, 아니면bunx -y @smithery/cli run ...stdio로 폴백한다. (smithery-registry.ts)
요약 & 셀프체크
3줄 요약:
- MCP는 외부 도구를 표준 규격으로 꽂아 쓰는 콘센트이고, gajae-code는 서버를 찾아(discovery) → 연결하고(connect) → 도구를
mcp__서버_도구이름의 어댑터로 포장해 모델에 노출한다. - 느린 서버는 250ms 타임아웃 후 디스크 캐시 기반
DeferredMCPTool로 먼저 띄워 시작이 멈추지 않게 하고, 도구가 많으면search_tool_bm25로 지연 검색해 토큰을 아낀다. - 모든 실행 결과에는 “어느 서버의 어느 원본 도구에서 왔고 에러였는지”를 담은 영수증(
MCPToolDetails)이 붙어 추적이 가능하다.
스스로 답해 보기:
- 서버 이름이 “puppeteer”, 도구 이름이 “puppeteer_screenshot”일 때 최종 도구 이름은 무엇이 되고, 왜 그렇게 되나?
- 시작 시 서버가 250ms 안에 응답하지 못하면 무슨 일이 벌어지며, 캐시가 신선한지는 무엇으로 판단하나?
- 도구 실행이 네트워크 문제로 실패했을 때와 사용자가 중단(abort)했을 때, 처리 방식은 어떻게 다른가?
연결
GJ_개요 · GJ_60_tool-system-definition-and-exposure · GJ_40_subagents-and-task-delegation · GJ_70_guardrails-sandbox-permission-gating · GJ_80_gajae-receipts-and-workflow-state · _분석축_루브릭
Codex 교차검증 — 모호성 게이팅·영수증 관점 (원문 분석 보존)
이 MCP 경로 자체에는 별도의 “ambiguity gate” 단계가 없다(게이팅은 상위 deep-interview/plan 레이어 책임). MCP 계층이 제공하는 모호성 차단 메커니즘은 (a) 이름 충돌 방지(
createMCPToolName의 서버 prefix + 중복 prefix 제거), (b) config 해시로 캐시 신선도 보장, (c)validateServerConfig로 잘못된 항목을 연결 전에 errors로 격리하는 것이다. 영수증/감사(receipt) 구조는MCPToolDetails가 담당: 모든 도구 실행 결과에serverName/mcpToolName/isError/rawContent/provider/providerName을 첨부해, “이 결과가 어느 서버의 어느 원본 도구에서 왔고 에러였는지”를 추적·렌더링할 수 있게 한다(buildResult/buildErrorResult).근거 파일
packages/coding-agent/src/runtime-mcp/loader.ts— MCP→LoadedCustomTool변환,path포맷packages/coding-agent/src/runtime-mcp/manager.ts— 디스커버리/연결/재연결/캐시폴백/정렬/OAuth 해석 라이프사이클packages/coding-agent/src/runtime-mcp/client.ts— initialize/tools/list/tools/call, 서버→클라 요청 처리packages/coding-agent/src/runtime-mcp/tool-bridge.ts—MCPTool/DeferredMCPTool어댑터,createMCPToolName, 재시도,MCPToolDetails(영수증)packages/coding-agent/src/runtime-mcp/tool-cache.ts— config-해시 기반 도구 캐시(agent.db, TTL 30일)packages/coding-agent/src/runtime-mcp/discoverable-tool-metadata.ts+tool-discovery/tool-index.ts— BM25 지연 노출/검색 인덱스packages/coding-agent/src/runtime-mcp/types.ts— JSON-RPC·서버 config·프로토콜·트랜스포트 타입packages/coding-agent/src/runtime-mcp/transports/stdio.ts,transports/http.ts— JSON-RPC 트랜스포트(stdio/Streamable HTTP+SSE)packages/coding-agent/src/runtime-mcp/oauth-discovery.ts,oauth-flow.ts— OAuth 엔드포인트 자동 탐지 + PKCE/동적등록 플로우/토큰 갱신packages/coding-agent/src/runtime-mcp/smithery-registry.ts— Smithery 레지스트리 검색→config 생성packages/coding-agent/src/discovery/mcp-json.ts—mcp.json/.mcp.jsonprovider(priority 5) 디스커버리 형식packages/coding-agent/src/extensibility/custom-tools/types.ts—LoadedCustomTool정의