툴 포맷 어댑터
MCP 모듈은 MCP 서버가 제공하는 툴을 각 LLM 프로바이더가 요구하는 포맷으로 변환해야 합니다. 이 과정은 두 가지 컴포넌트가 담당합니다.
- ToolsLoader — MCPTool을 OpenAI/Anthropic/Gemini 등 프로바이더용 툴 정의 포맷으로 변환
- McpToolAdapter — MCPTool을 TinyElf 엔진용
ITool인터페이스 구현체로 래핑
ToolsLoader
파일 경로: packages/desktop/app/main/services/capabilities/tools/mcp-users/ToolsLoader.ts
역할
ToolsLoader는 MCP 툴 로딩 및 포맷 변환 파이프라인 전체를 제공합니다.
graph LR
fetchMcpTools --> MCPTool_Array["MCPTool[]"]
MCPTool_Array --> convertOpenAI["convertMcpToolsToOpenAI"]
MCPTool_Array --> convertAnthropic["convertMcpToolsToAnthropic"]
MCPTool_Array --> convertGemini["convertMcpToolsToGemini"]
convertOpenAI --> OpenAI_Format["OpenAITool[]"]
convertAnthropic --> Anthropic_Format["AnthropicTool[]"]
convertGemini --> Gemini_Format["GeminiTools"]
진입 함수: setupMcpTools
async function setupMcpTools(
mcpService: McpService,
config?: {
enabled?: boolean;
mode?: 'auto' | 'manual';
selectedServers?: string[];
},
apiFormat?: 'openai' | 'anthropic' | 'google'
): Promise<{ tools: OpenAITool[] | AnthropicTool[] | GeminiTools; mcpTools: MCPTool[] } | undefined>
실행 흐름:
fetchMcpTools()를 호출하여 원시 MCPTool 목록 취득- 사용 가능한 툴이 없으면
undefined반환 - 서버별로 그룹화하여 로그 출력
apiFormat에 따라 해당 변환 함수 호출- 변환된 툴 정의와 원시 MCPTool 목록 반환
fetchMcpTools — 툴 가져오기
async function fetchMcpTools(
mcpService: McpService,
config?: { enabled?: boolean; mode?: 'auto' | 'manual'; selectedServers?: string[] }
): Promise<MCPTool[]>
설정에 따라 로딩 전략을 결정합니다.
| 조건 | 동작 |
|---|---|
enabled가 false이거나 설정되지 않음 | 빈 배열 반환 |
mode가 auto이거나 설정되지 않음 | mcpService.listAllActiveServerTools() 호출 |
mode가 manual | selectedServers를 순회하며 각각 mcpService.listServerTools() 호출 |
출력 포맷 비교
OpenAI 포맷
interface OpenAITool {
type: 'function';
function: {
name: string; // MCPTool.id
description: string; // MCPTool.description
parameters: { // 처리된 JSON Schema
type: 'object';
properties: Record<string, any>;
required: string[];
};
};
}
Anthropic 포맷
interface AnthropicTool {
name: string; // MCPTool.id
description: string; // MCPTool.description
input_schema: { // 처리된 JSON Schema
type: 'object';
properties: Record<string, any>;
required: string[];
};
}
Gemini 포맷
type GeminiTools = Array<{
functionDeclarations: Array<{
name: string; // MCPTool.id
description: string; // MCPTool.description
parameters: { // 처리된 JSON Schema
type: 'object';
properties: Record<string, any>;
required: string[];
};
}>;
}>;
Gemini 포맷 특징: 모든 툴이 functionDeclarations 배열로 래핑되며, 해당 배열이 다시 외부 배열로 감싸집니다.
processJsonSchema — 스키마 처리
function processJsonSchema(schema: any): any
다양한 LLM API와의 JSON Schema 호환성을 보장합니다.
- 스키마가 비어 있거나 객체가 아니면 기본 빈 객체 스키마 반환
- 이미
type: 'object'가 있으면properties,required,description추출 - 그 외의 경우 객체 타입으로 래핑
McpToolAdapter
파일 경로: packages/desktop/app/main/services/agent-core/engine/tinyelf/tools/McpToolAdapter.ts
역할
MCPTool을 TinyElf 엔진용 ITool 인터페이스 구현체로 래핑하여, TinyElf가 MCP 툴을 직접 호출할 수 있게 합니다.
ITool 인터페이스 매핑
class McpToolAdapter implements ITool {
readonly name: string; // ← MCPTool.id
readonly description: string; // ← MCPTool.description
readonly parameters: JsonSchema; // ← MCPTool.inputSchema
constructor(
private mcpTool: MCPTool,
private mcpService: McpService
);
async execute(params: Record<string, unknown>): Promise<string>;
}
execute 메서드
sequenceDiagram
participant TinyElf
participant Adapter as McpToolAdapter
participant Service as McpService
participant Server as MCP Server
TinyElf->>Adapter: execute(params)
Adapter->>Adapter: callId 생성
Adapter->>Service: callTool(serverId, toolName, args, callId)
Note over Adapter,Service: Promise.race([callTool, timeout(120s)])
Service->>Server: client.callTool()
Server-->>Service: MCPCallToolResponse
Service-->>Adapter: { isError, content[] }
Adapter->>Adapter: flattenContent(content)
Adapter-->>TinyElf: 문자열 결과
타임아웃 처리: Promise.race()를 사용하여 120초 타임아웃을 구현하며, ShellTool의 타임아웃과 동일합니다.
flattenContent — 콘텐츠 평탄화
MCPToolResponseContent[]를 단일 문자열로 변환합니다.
function flattenContent(content: MCPToolResponseContent[]): string
| 콘텐츠 타입 | 변환 방식 |
|---|---|
text | c.text 직접 사용 |
image | [Image: ${mimeType}] 플레이스홀더 출력 |
audio | [Audio: ${mimeType}] 플레이스홀더 출력 |
여러 콘텐츠 항목은 \n으로 연결됩니다.
loadMcpTools — 일괄 로딩
async function loadMcpTools(
mcpService: McpService,
serverIds: string[]
): Promise<McpToolAdapter[]>
서버 ID 목록을 순회하며 각 서버에 대해 listServerTools()를 호출하고, 각 MCPTool을 McpToolAdapter로 래핑합니다. 개별 서버 실패는 다른 서버에 영향을 주지 않습니다.
DirectMcpToolAdapter
목적: Agent 프리셋의 로컬 MCP 서비스와 같은 내장 MCP 서버에 사용되며, McpService를 거치지 않고 Client 참조를 직접 보유합니다.
McpToolAdapter와의 차이점
| 특성 | McpToolAdapter | DirectMcpToolAdapter |
|---|---|---|
| 연결 관리 | McpService에 위임 | Client 직접 참조 |
| 사용 사례 | 사용자 설정 MCP 서버 | 내장/프리셋 MCP 서버 |
| 라이프사이클 | McpService를 따름 | 세션을 따름 (세션 레벨) |
| 툴 이름 형식 | mcp__serverName__toolName | serverName__toolName |
| 프로세스 추적 | 불필요 | McpProcessTracker를 통해 |
loadDirectMcpTools — 직접 연결 로딩
async function loadDirectMcpTools(
servers: ResolvedMcpServer[]
): Promise<{
tools: DirectMcpToolAdapter[];
connections: DirectMcpConnection[];
}>
실행 흐름:
- 각 서버에 대해
StdioClientTransport생성 - MCP
Client를 생성하고 연결 - PID 추적을 위해
McpProcessTracker에 등록 - 툴을 탐색하고
DirectMcpToolAdapter인스턴스 생성 - 툴 목록과 연결 핸들(세션 종료 시 정리용) 반환
McpProcessTracker
파일 경로: packages/desktop/app/main/services/agent-core/engine/tinyelf/tools/McpProcessTracker.ts
역할
DirectMcpToolAdapter가 생성한 stdio 자식 프로세스 PID를 추적하는 글로벌 싱글턴으로, 비정상 종료 시에도 정리를 보장합니다.
안전 계층
| 계층 | 메서드 | 설명 |
|---|---|---|
| 정상 정리 | closeConnection() | 정상 종료(3초 타임아웃) → 강제 종료 |
| 일괄 정리 | closeAll() | 추적 중인 모든 연결 종료 |
| 안전망 | process.on('exit') | 프로세스 종료 시 모든 PID 동기 강제 종료 |
사용법
// 추적 등록
McpProcessTracker.getInstance().track(connection);
// 단일 정리
await McpProcessTracker.getInstance().closeConnection(connection);
// 전체 정리
await McpProcessTracker.getInstance().closeAll();
통합 지점
CompletionService 통합
CompletionService는 각 채팅 요청 전에 setupMcpTools()를 통해 MCP 툴을 로드합니다.
const mcpResult = await setupMcpTools(
this.mcpService,
{ enabled: true, mode: 'auto' },
'openai' // 현재 프로바이더에 따라 선택
);
if (mcpResult) {
requestPayload.tools = mcpResult.tools;
}
TinyElf 엔진 통합
TinyElf 엔진은 loadMcpTools()와 loadDirectMcpTools()를 통해 툴을 로드합니다.
// 사용자 설정 MCP 서버
const mcpTools = await loadMcpTools(mcpService, serverIds);
// 내장 MCP 서버
const { tools: directTools, connections } = await loadDirectMcpTools(builtinServers);
// ToolRegistry에 등록
for (const tool of [...mcpTools, ...directTools]) {
toolRegistry.register(tool);
}